shrine 2.19.4 → 3.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +485 -43
- data/LICENSE.txt +1 -1
- data/README.md +81 -977
- data/doc/advantages.md +231 -204
- data/doc/attacher.md +304 -153
- data/doc/carrierwave.md +297 -226
- data/doc/changing_derivatives.md +308 -0
- data/doc/changing_location.md +102 -21
- data/doc/changing_storage.md +110 -0
- data/doc/creating_persistence_plugins.md +132 -0
- data/doc/creating_plugins.md +43 -23
- data/doc/creating_storages.md +19 -5
- data/doc/design.md +147 -97
- data/doc/direct_s3.md +38 -28
- data/doc/external/articles.md +63 -0
- data/doc/external/extensions.md +53 -0
- data/doc/external/misc.md +32 -0
- data/doc/getting_started.md +1115 -0
- data/doc/metadata.md +190 -109
- data/doc/multiple_files.md +62 -34
- data/doc/paperclip.md +384 -262
- data/doc/plugins/activerecord.md +177 -46
- data/doc/plugins/add_metadata.md +139 -38
- data/doc/plugins/atomic_helpers.md +217 -0
- data/doc/plugins/backgrounding.md +156 -98
- data/doc/plugins/cached_attachment_data.md +7 -5
- data/doc/plugins/column.md +121 -0
- data/doc/plugins/data_uri.md +23 -22
- data/doc/plugins/default_storage.md +36 -10
- data/doc/plugins/default_url.md +30 -13
- data/doc/plugins/delete_raw.md +4 -2
- data/doc/plugins/derivation_endpoint.md +162 -101
- data/doc/plugins/derivatives.md +829 -0
- data/doc/plugins/determine_mime_type.md +4 -2
- data/doc/plugins/download_endpoint.md +64 -8
- data/doc/plugins/dynamic_storage.md +5 -3
- data/doc/plugins/entity.md +263 -0
- data/doc/plugins/form_assign.md +55 -0
- data/doc/plugins/included.md +31 -8
- data/doc/plugins/infer_extension.md +21 -10
- data/doc/plugins/instrumentation.md +38 -16
- data/doc/plugins/keep_files.md +14 -17
- data/doc/plugins/metadata_attributes.md +42 -13
- data/doc/plugins/mirroring.md +118 -0
- data/doc/plugins/model.md +210 -0
- data/doc/plugins/module_include.md +4 -2
- data/doc/plugins/multi_cache.md +24 -0
- data/doc/plugins/persistence.md +101 -0
- data/doc/plugins/presign_endpoint.md +9 -4
- data/doc/plugins/pretty_location.md +16 -3
- data/doc/plugins/processing.md +4 -2
- data/doc/plugins/rack_file.md +8 -2
- data/doc/plugins/rack_response.md +6 -2
- data/doc/plugins/recache.md +4 -2
- data/doc/plugins/refresh_metadata.md +49 -9
- data/doc/plugins/remote_url.md +84 -47
- data/doc/plugins/remove_attachment.md +27 -6
- data/doc/plugins/remove_invalid.md +21 -6
- data/doc/plugins/restore_cached_data.md +11 -3
- data/doc/plugins/sequel.md +159 -35
- data/doc/plugins/signature.md +16 -5
- data/doc/plugins/store_dimensions.md +14 -2
- data/doc/plugins/tempfile.md +4 -2
- data/doc/plugins/type_predicates.md +96 -0
- data/doc/plugins/upload_endpoint.md +13 -13
- data/doc/plugins/upload_options.md +6 -4
- data/doc/plugins/{default_url_options.md → url_options.md} +9 -7
- data/doc/plugins/validation.md +97 -0
- data/doc/plugins/validation_helpers.md +16 -13
- data/doc/plugins/versions.md +15 -19
- data/doc/processing.md +438 -221
- data/doc/refile.md +185 -167
- data/doc/release_notes/1.0.0.md +4 -0
- data/doc/release_notes/1.1.0.md +6 -2
- data/doc/release_notes/1.2.0.md +4 -0
- data/doc/release_notes/1.3.0.md +4 -0
- data/doc/release_notes/1.4.0.md +4 -0
- data/doc/release_notes/1.4.1.md +4 -0
- data/doc/release_notes/1.4.2.md +4 -0
- data/doc/release_notes/2.0.0.md +4 -0
- data/doc/release_notes/2.0.1.md +4 -0
- data/doc/release_notes/2.1.0.md +4 -0
- data/doc/release_notes/2.1.1.md +4 -0
- data/doc/release_notes/2.10.0.md +4 -0
- data/doc/release_notes/2.10.1.md +4 -0
- data/doc/release_notes/2.11.0.md +4 -0
- data/doc/release_notes/2.12.0.md +4 -0
- data/doc/release_notes/2.13.0.md +4 -0
- data/doc/release_notes/2.14.0.md +5 -1
- data/doc/release_notes/2.15.0.md +11 -7
- data/doc/release_notes/2.16.0.md +4 -0
- data/doc/release_notes/2.17.0.md +4 -0
- data/doc/release_notes/2.18.0.md +4 -0
- data/doc/release_notes/2.19.0.md +6 -3
- data/doc/release_notes/2.2.0.md +4 -0
- data/doc/release_notes/2.3.0.md +4 -0
- data/doc/release_notes/2.3.1.md +4 -0
- data/doc/release_notes/2.4.0.md +4 -0
- data/doc/release_notes/2.4.1.md +4 -0
- data/doc/release_notes/2.5.0.md +4 -0
- data/doc/release_notes/2.6.0.md +4 -0
- data/doc/release_notes/2.6.1.md +4 -0
- data/doc/release_notes/2.7.0.md +4 -0
- data/doc/release_notes/2.8.0.md +4 -0
- data/doc/release_notes/2.9.0.md +4 -0
- data/doc/release_notes/3.0.0.md +981 -0
- data/doc/release_notes/3.0.1.md +22 -0
- data/doc/release_notes/3.1.0.md +73 -0
- data/doc/release_notes/3.2.0.md +96 -0
- data/doc/release_notes/3.2.1.md +31 -0
- data/doc/release_notes/3.2.2.md +14 -0
- data/doc/release_notes/3.3.0.md +105 -0
- data/doc/release_notes/3.4.0.md +35 -0
- data/doc/retrieving_uploads.md +4 -1
- data/doc/securing_uploads.md +60 -37
- data/doc/storage/file_system.md +20 -3
- data/doc/storage/memory.md +19 -0
- data/doc/storage/s3.md +117 -83
- data/doc/testing.md +124 -144
- data/doc/upgrading_to_3.md +710 -0
- data/doc/validation.md +54 -90
- data/lib/shrine/attacher.rb +287 -171
- data/lib/shrine/attachment.rb +13 -46
- data/lib/shrine/plugins/_persistence.rb +93 -0
- data/lib/shrine/plugins/activerecord.rb +77 -34
- data/lib/shrine/plugins/add_metadata.rb +25 -17
- data/lib/shrine/plugins/atomic_helpers.rb +119 -0
- data/lib/shrine/plugins/backgrounding.rb +77 -113
- data/lib/shrine/plugins/cached_attachment_data.rb +6 -15
- data/lib/shrine/plugins/column.rb +102 -0
- data/lib/shrine/plugins/data_uri.rb +38 -36
- data/lib/shrine/plugins/default_storage.rb +45 -15
- data/lib/shrine/plugins/default_url.rb +12 -24
- data/lib/shrine/plugins/default_url_options.rb +3 -30
- data/lib/shrine/plugins/delete_raw.rb +10 -16
- data/lib/shrine/plugins/derivation_endpoint.rb +89 -134
- data/lib/shrine/plugins/derivatives.rb +637 -0
- data/lib/shrine/plugins/determine_mime_type.rb +9 -21
- data/lib/shrine/plugins/download_endpoint.rb +109 -133
- data/lib/shrine/plugins/dynamic_storage.rb +5 -11
- data/lib/shrine/plugins/entity.rb +152 -0
- data/lib/shrine/plugins/form_assign.rb +108 -0
- data/lib/shrine/plugins/included.rb +6 -6
- data/lib/shrine/plugins/infer_extension.rb +13 -20
- data/lib/shrine/plugins/instrumentation.rb +54 -42
- data/lib/shrine/plugins/keep_files.rb +3 -15
- data/lib/shrine/plugins/metadata_attributes.rb +28 -19
- data/lib/shrine/plugins/mirroring.rb +142 -0
- data/lib/shrine/plugins/model.rb +158 -0
- data/lib/shrine/plugins/module_include.rb +3 -3
- data/lib/shrine/plugins/multi_cache.rb +27 -0
- data/lib/shrine/plugins/presign_endpoint.rb +18 -22
- data/lib/shrine/plugins/pretty_location.rb +15 -9
- data/lib/shrine/plugins/processing.rb +22 -9
- data/lib/shrine/plugins/rack_file.rb +2 -42
- data/lib/shrine/plugins/rack_response.rb +15 -10
- data/lib/shrine/plugins/recache.rb +6 -5
- data/lib/shrine/plugins/refresh_metadata.rb +13 -11
- data/lib/shrine/plugins/remote_url.rb +49 -49
- data/lib/shrine/plugins/remove_attachment.rb +10 -6
- data/lib/shrine/plugins/remove_invalid.rb +19 -8
- data/lib/shrine/plugins/restore_cached_data.rb +13 -7
- data/lib/shrine/plugins/sequel.rb +86 -36
- data/lib/shrine/plugins/signature.rb +10 -16
- data/lib/shrine/plugins/store_dimensions.rb +35 -40
- data/lib/shrine/plugins/tempfile.rb +1 -3
- data/lib/shrine/plugins/type_predicates.rb +113 -0
- data/lib/shrine/plugins/upload_endpoint.rb +25 -23
- data/lib/shrine/plugins/upload_options.rb +14 -15
- data/lib/shrine/plugins/url_options.rb +31 -0
- data/lib/shrine/plugins/validation.rb +80 -0
- data/lib/shrine/plugins/validation_helpers.rb +34 -57
- data/lib/shrine/plugins/versions.rb +107 -87
- data/lib/shrine/plugins.rb +22 -0
- data/lib/shrine/storage/file_system.rb +46 -64
- data/lib/shrine/storage/linter.rb +42 -7
- data/lib/shrine/storage/memory.rb +49 -0
- data/lib/shrine/storage/s3.rb +154 -158
- data/lib/shrine/uploaded_file.rb +28 -30
- data/lib/shrine/version.rb +3 -3
- data/lib/shrine.rb +86 -149
- data/shrine.gemspec +9 -10
- metadata +79 -83
- data/doc/migrating_storage.md +0 -76
- data/doc/plugins/backup.md +0 -31
- data/doc/plugins/copy.md +0 -24
- data/doc/plugins/delete_promoted.md +0 -12
- data/doc/plugins/direct_upload.md +0 -172
- data/doc/plugins/hooks.md +0 -58
- data/doc/plugins/logging.md +0 -42
- data/doc/plugins/migration_helpers.md +0 -60
- data/doc/plugins/moving.md +0 -19
- data/doc/plugins/multi_delete.md +0 -20
- data/doc/plugins/parallelize.md +0 -16
- data/doc/plugins/parsed_json.md +0 -23
- data/doc/regenerating_versions.md +0 -143
- data/lib/shrine/plugins/background_helpers.rb +0 -5
- data/lib/shrine/plugins/backup.rb +0 -90
- data/lib/shrine/plugins/copy.rb +0 -50
- data/lib/shrine/plugins/delete_promoted.rb +0 -20
- data/lib/shrine/plugins/direct_upload.rb +0 -217
- data/lib/shrine/plugins/hooks.rb +0 -90
- data/lib/shrine/plugins/logging.rb +0 -142
- data/lib/shrine/plugins/migration_helpers.rb +0 -70
- data/lib/shrine/plugins/moving.rb +0 -57
- data/lib/shrine/plugins/multi_delete.rb +0 -32
- data/lib/shrine/plugins/parallelize.rb +0 -78
- data/lib/shrine/plugins/parsed_json.rb +0 -29
data/lib/shrine/attacher.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Shrine
|
4
|
-
# Core class
|
5
|
-
#
|
4
|
+
# Core class that handles attaching files. It uses Shrine and
|
5
|
+
# Shrine::UploadedFile objects internally.
|
6
6
|
class Attacher
|
7
7
|
@shrine_class = ::Shrine
|
8
8
|
|
@@ -18,195 +18,319 @@ class Shrine
|
|
18
18
|
"#{shrine_class.inspect}::Attacher"
|
19
19
|
end
|
20
20
|
|
21
|
-
#
|
22
|
-
# validation. Example:
|
21
|
+
# Initializes the attacher from a data hash generated from `Attacher#data`.
|
23
22
|
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
define_method(:validate_block, &block)
|
31
|
-
private :validate_block
|
23
|
+
# attacher = Attacher.from_data({ "id" => "...", "storage" => "...", "metadata" => { ... } })
|
24
|
+
# attacher.file #=> #<Shrine::UploadedFile>
|
25
|
+
def from_data(data, **options)
|
26
|
+
attacher = new(**options)
|
27
|
+
attacher.load_data(data)
|
28
|
+
attacher
|
32
29
|
end
|
33
30
|
end
|
34
31
|
|
35
32
|
module InstanceMethods
|
36
|
-
# Returns the
|
37
|
-
attr_reader :
|
38
|
-
|
39
|
-
# Returns the uploader that is used for the permanent storage.
|
40
|
-
attr_reader :store
|
33
|
+
# Returns the attached uploaded file.
|
34
|
+
attr_reader :file
|
41
35
|
|
42
|
-
# Returns
|
43
|
-
#
|
44
|
-
# uploader.
|
36
|
+
# Returns options that are automatically forwarded to the uploader.
|
37
|
+
# Can be modified with additional data.
|
45
38
|
attr_reader :context
|
46
39
|
|
47
|
-
#
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
@
|
54
|
-
@store = shrine_class.new(store)
|
55
|
-
@context = { record: record, name: name }
|
56
|
-
@errors = []
|
40
|
+
# Initializes the attached file, temporary and permanent storage.
|
41
|
+
def initialize(file: nil, cache: :cache, store: :store)
|
42
|
+
@file = file
|
43
|
+
@cache = cache
|
44
|
+
@store = store
|
45
|
+
@context = {}
|
46
|
+
@previous = nil
|
57
47
|
end
|
58
48
|
|
59
|
-
# Returns the
|
60
|
-
def
|
49
|
+
# Returns the temporary storage identifier.
|
50
|
+
def cache_key; @cache.to_sym; end
|
51
|
+
# Returns the permanent storage identifier.
|
52
|
+
def store_key; @store.to_sym; end
|
61
53
|
|
62
|
-
# Returns the
|
63
|
-
def
|
54
|
+
# Returns the uploader that is used for the temporary storage.
|
55
|
+
def cache; shrine_class.new(cache_key); end
|
56
|
+
# Returns the uploader that is used for the permanent storage.
|
57
|
+
def store; shrine_class.new(store_key); end
|
64
58
|
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
#
|
59
|
+
# Calls #attach_cached, but skips if value is an empty string (this is
|
60
|
+
# useful when the uploaded file comes from form fields). Forwards any
|
61
|
+
# additional options to #attach_cached.
|
62
|
+
#
|
63
|
+
# attacher.assign(File.open(...))
|
64
|
+
# attacher.assign(File.open(...), metadata: { "foo" => "bar" })
|
65
|
+
# attacher.assign('{"id":"...","storage":"cache","metadata":{...}}')
|
66
|
+
# attacher.assign({ "id" => "...", "storage" => "cache", "metadata" => {} })
|
67
|
+
#
|
68
|
+
# # ignores the assignment when a blank string is given
|
69
|
+
# attacher.assign("")
|
69
70
|
def assign(value, **options)
|
70
|
-
if value
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
uploaded_file = cache!(value, action: :cache, **options) if value
|
75
|
-
set(uploaded_file)
|
71
|
+
return if value == "" # skip empty hidden field
|
72
|
+
|
73
|
+
if value.is_a?(Hash) || value.is_a?(String)
|
74
|
+
return if uploaded_file(value) == file # skip assignment for current file
|
76
75
|
end
|
76
|
+
|
77
|
+
attach_cached(value, **options)
|
77
78
|
end
|
78
79
|
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
80
|
+
# Sets an existing cached file, or uploads an IO object to temporary
|
81
|
+
# storage and sets it via #attach. Forwards any additional options to
|
82
|
+
# #attach.
|
83
|
+
#
|
84
|
+
# # upload file to temporary storage and set the uploaded file.
|
85
|
+
# attacher.attach_cached(File.open(...))
|
86
|
+
#
|
87
|
+
# # foward additional options to the uploader
|
88
|
+
# attacher.attach_cached(File.open(...), metadata: { "foo" => "bar" })
|
89
|
+
#
|
90
|
+
# # sets an existing cached file from JSON data
|
91
|
+
# attacher.attach_cached('{"id":"...","storage":"cache","metadata":{...}}')
|
92
|
+
#
|
93
|
+
# # sets an existing cached file from Hash data
|
94
|
+
# attacher.attach_cached({ "id" => "...", "storage" => "cache", "metadata" => {} })
|
95
|
+
def attach_cached(value, **options)
|
96
|
+
if value.is_a?(String) || value.is_a?(Hash)
|
97
|
+
change(cached(value, **options))
|
98
|
+
else
|
99
|
+
attach(value, storage: cache_key, action: :cache, **options)
|
100
|
+
end
|
87
101
|
end
|
88
102
|
|
89
|
-
#
|
90
|
-
|
91
|
-
|
92
|
-
|
103
|
+
# Uploads given IO object and changes the uploaded file.
|
104
|
+
#
|
105
|
+
# # uploads the file to permanent storage
|
106
|
+
# attacher.attach(io)
|
107
|
+
#
|
108
|
+
# # uploads the file to specified storage
|
109
|
+
# attacher.attach(io, storage: :other_store)
|
110
|
+
#
|
111
|
+
# # forwards additional options to the uploader
|
112
|
+
# attacher.attach(io, upload_options: { acl: "public-read" }, metadata: { "foo" => "bar" })
|
113
|
+
#
|
114
|
+
# # removes the attachment
|
115
|
+
# attacher.attach(nil)
|
116
|
+
def attach(io, storage: store_key, **options)
|
117
|
+
file = upload(io, storage, **options) if io
|
118
|
+
|
119
|
+
change(file)
|
93
120
|
end
|
94
121
|
|
95
|
-
#
|
96
|
-
|
97
|
-
|
122
|
+
# Deletes any previous file and promotes newly attached cached file.
|
123
|
+
# It also clears any dirty tracking.
|
124
|
+
#
|
125
|
+
# # promoting cached file
|
126
|
+
# attacher.assign(io)
|
127
|
+
# attacher.cached? #=> true
|
128
|
+
# attacher.finalize
|
129
|
+
# attacher.stored?
|
130
|
+
#
|
131
|
+
# # deleting previous file
|
132
|
+
# previous_file = attacher.file
|
133
|
+
# previous_file.exists? #=> true
|
134
|
+
# attacher.assign(io)
|
135
|
+
# attacher.finalize
|
136
|
+
# previous_file.exists? #=> false
|
137
|
+
#
|
138
|
+
# # clearing dirty tracking
|
139
|
+
# attacher.assign(io)
|
140
|
+
# attacher.changed? #=> true
|
141
|
+
# attacher.finalize
|
142
|
+
# attacher.changed? #=> false
|
143
|
+
def finalize
|
144
|
+
destroy_previous
|
145
|
+
promote_cached
|
146
|
+
@previous = nil
|
98
147
|
end
|
99
|
-
alias attached? changed?
|
100
148
|
|
101
|
-
# Plugins can override this if they want something to be done
|
102
|
-
# save.
|
149
|
+
# Plugins can override this if they want something to be done in a
|
150
|
+
# "before save" callback.
|
103
151
|
def save
|
104
152
|
end
|
105
153
|
|
106
|
-
#
|
107
|
-
#
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
154
|
+
# If a new cached file has been attached, uploads it to permanent storage.
|
155
|
+
# Any additional options are forwarded to #promote.
|
156
|
+
#
|
157
|
+
# attacher.assign(io)
|
158
|
+
# attacher.cached? #=> true
|
159
|
+
# attacher.promote_cached
|
160
|
+
# attacher.stored? #=> true
|
161
|
+
def promote_cached(**options)
|
162
|
+
promote(**options) if promote?
|
113
163
|
end
|
114
164
|
|
115
|
-
#
|
116
|
-
|
117
|
-
|
165
|
+
# Uploads current file to permanent storage and sets the stored file.
|
166
|
+
#
|
167
|
+
# attacher.cached? #=> true
|
168
|
+
# attacher.promote
|
169
|
+
# attacher.stored? #=> true
|
170
|
+
def promote(storage: store_key, **options)
|
171
|
+
set upload(file, storage, action: :store, **options)
|
118
172
|
end
|
119
173
|
|
120
|
-
#
|
121
|
-
#
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
174
|
+
# Delegates to `Shrine.upload`, passing the #context.
|
175
|
+
#
|
176
|
+
# # upload file to specified storage
|
177
|
+
# attacher.upload(io, :store) #=> #<Shrine::UploadedFile>
|
178
|
+
#
|
179
|
+
# # pass additional options for the uploader
|
180
|
+
# attacher.upload(io, :store, metadata: { "foo" => "bar" })
|
181
|
+
def upload(io, storage = store_key, **options)
|
182
|
+
shrine_class.upload(io, storage, **context, **options)
|
126
183
|
end
|
127
184
|
|
128
|
-
#
|
129
|
-
#
|
130
|
-
|
131
|
-
|
132
|
-
|
185
|
+
# If a new file was attached, deletes previously attached file if any.
|
186
|
+
#
|
187
|
+
# previous_file = attacher.file
|
188
|
+
# attacher.attach(file)
|
189
|
+
# attacher.destroy_previous
|
190
|
+
# previous_file.exists? #=> false
|
191
|
+
def destroy_previous
|
192
|
+
@previous.destroy_attached if changed?
|
133
193
|
end
|
134
194
|
|
135
|
-
#
|
136
|
-
#
|
137
|
-
|
138
|
-
|
195
|
+
# Destroys the attached file if it exists and is uploaded to permanent
|
196
|
+
# storage.
|
197
|
+
#
|
198
|
+
# attacher.file.exists? #=> true
|
199
|
+
# attacher.destroy_attached
|
200
|
+
# attacher.file.exists? #=> false
|
201
|
+
def destroy_attached
|
202
|
+
destroy if destroy?
|
139
203
|
end
|
140
204
|
|
141
|
-
#
|
142
|
-
#
|
205
|
+
# Destroys the attachment.
|
206
|
+
#
|
207
|
+
# attacher.file.exists? #=> true
|
208
|
+
# attacher.destroy
|
209
|
+
# attacher.file.exists? #=> false
|
143
210
|
def destroy
|
144
|
-
file
|
145
|
-
_delete(file, action: :destroy) if file && !cached?(file)
|
211
|
+
file&.delete
|
146
212
|
end
|
147
213
|
|
148
|
-
#
|
149
|
-
|
150
|
-
|
214
|
+
# Sets the uploaded file with dirty tracking, and runs validations.
|
215
|
+
#
|
216
|
+
# attacher.change(uploaded_file)
|
217
|
+
# attacher.file #=> #<Shrine::UploadedFile>
|
218
|
+
# attacher.changed? #=> true
|
219
|
+
def change(file)
|
220
|
+
@previous = dup if change?(file)
|
221
|
+
set(file)
|
222
|
+
end
|
223
|
+
|
224
|
+
# Sets the uploaded file.
|
225
|
+
#
|
226
|
+
# attacher.set(uploaded_file)
|
227
|
+
# attacher.file #=> #<Shrine::UploadedFile>
|
228
|
+
# attacher.changed? #=> false
|
229
|
+
def set(file)
|
230
|
+
self.file = file
|
231
|
+
end
|
232
|
+
|
233
|
+
# Returns the attached file.
|
234
|
+
#
|
235
|
+
# # when a file is attached
|
236
|
+
# attacher.get #=> #<Shrine::UploadedFile>
|
237
|
+
#
|
238
|
+
# # when no file is attached
|
239
|
+
# attacher.get #=> nil
|
240
|
+
def get
|
241
|
+
file
|
151
242
|
end
|
152
243
|
|
153
|
-
#
|
154
|
-
#
|
244
|
+
# If a file is attached, returns the uploaded file URL, otherwise returns
|
245
|
+
# nil. Any options are forwarded to the storage.
|
246
|
+
#
|
247
|
+
# attacher.file = file
|
248
|
+
# attacher.url #=> "https://..."
|
249
|
+
#
|
250
|
+
# attacher.file = nil
|
251
|
+
# attacher.url #=> nil
|
155
252
|
def url(**options)
|
156
|
-
|
253
|
+
file&.url(**options)
|
157
254
|
end
|
158
255
|
|
159
|
-
# Returns
|
160
|
-
|
161
|
-
|
256
|
+
# Returns whether the attachment has changed.
|
257
|
+
#
|
258
|
+
# attacher.changed? #=> false
|
259
|
+
# attacher.attach(file)
|
260
|
+
# attacher.changed? #=> true
|
261
|
+
def changed?
|
262
|
+
!!@previous
|
162
263
|
end
|
163
264
|
|
164
|
-
# Returns
|
165
|
-
|
166
|
-
|
265
|
+
# Returns whether a file is attached.
|
266
|
+
#
|
267
|
+
# attacher.attach(io)
|
268
|
+
# attacher.attached? #=> true
|
269
|
+
#
|
270
|
+
# attacher.attach(nil)
|
271
|
+
# attacher.attached? #=> false
|
272
|
+
def attached?
|
273
|
+
!!file
|
167
274
|
end
|
168
275
|
|
169
|
-
# Returns
|
170
|
-
#
|
171
|
-
|
172
|
-
|
276
|
+
# Returns whether the file is uploaded to temporary storage.
|
277
|
+
#
|
278
|
+
# attacher.cached? # checks current file
|
279
|
+
# attacher.cached?(file) # checks given file
|
280
|
+
def cached?(file = self.file)
|
281
|
+
uploaded?(file, cache_key)
|
173
282
|
end
|
174
283
|
|
175
|
-
#
|
176
|
-
#
|
177
|
-
|
178
|
-
|
179
|
-
|
284
|
+
# Returns whether the file is uploaded to permanent storage.
|
285
|
+
#
|
286
|
+
# attacher.stored? # checks current file
|
287
|
+
# attacher.stored?(file) # checks given file
|
288
|
+
def stored?(file = self.file)
|
289
|
+
uploaded?(file, store_key)
|
180
290
|
end
|
181
291
|
|
182
|
-
#
|
183
|
-
|
184
|
-
|
185
|
-
|
292
|
+
# Generates serializable data for the attachment.
|
293
|
+
#
|
294
|
+
# attacher.data #=> { "id" => "...", "storage" => "...", "metadata": { ... } }
|
295
|
+
def data
|
296
|
+
file&.data
|
186
297
|
end
|
187
298
|
|
188
|
-
#
|
189
|
-
|
190
|
-
|
191
|
-
|
299
|
+
# Loads the uploaded file from data generated by `Attacher#data`.
|
300
|
+
#
|
301
|
+
# attacher.file #=> nil
|
302
|
+
# attacher.load_data({ "id" => "...", "storage" => "...", "metadata" => { ... } })
|
303
|
+
# attacher.file #=> #<Shrine::UploadedFile>
|
304
|
+
def load_data(data)
|
305
|
+
@file = data && uploaded_file(data)
|
192
306
|
end
|
193
307
|
|
194
|
-
#
|
195
|
-
|
196
|
-
|
197
|
-
|
308
|
+
# Saves the given uploaded file to an instance variable.
|
309
|
+
#
|
310
|
+
# attacher.file = uploaded_file
|
311
|
+
# attacher.file #=> #<Shrine::UploadedFile>
|
312
|
+
def file=(file)
|
313
|
+
unless file.is_a?(Shrine::UploadedFile) || file.nil?
|
314
|
+
fail ArgumentError, "expected file to be a Shrine::UploadedFile or nil, got #{file.inspect}"
|
315
|
+
end
|
316
|
+
|
317
|
+
@file = file
|
198
318
|
end
|
199
319
|
|
200
|
-
#
|
201
|
-
|
202
|
-
|
203
|
-
shrine_class.uploaded_file(object, &block)
|
320
|
+
# Returns attached file or raises an exception if no file is attached.
|
321
|
+
def file!
|
322
|
+
file or fail Error, "no file is attached"
|
204
323
|
end
|
205
324
|
|
206
|
-
#
|
207
|
-
#
|
208
|
-
|
209
|
-
|
325
|
+
# Converts JSON or Hash data into a Shrine::UploadedFile object.
|
326
|
+
#
|
327
|
+
# attacher.uploaded_file('{"id":"...","storage":"...","metadata":{...}}')
|
328
|
+
# #=> #<Shrine::UploadedFile ...>
|
329
|
+
#
|
330
|
+
# attacher.uploaded_file({ "id" => "...", "storage" => "...", "metadata" => {} })
|
331
|
+
# #=> #<Shrine::UploadedFile ...>
|
332
|
+
def uploaded_file(value)
|
333
|
+
shrine_class.uploaded_file(value)
|
210
334
|
end
|
211
335
|
|
212
336
|
# Returns the Shrine class that this attacher's class is namespaced
|
@@ -217,54 +341,46 @@ class Shrine
|
|
217
341
|
|
218
342
|
private
|
219
343
|
|
220
|
-
#
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
#
|
226
|
-
#
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
def _set(uploaded_file)
|
239
|
-
data = convert_to_data(uploaded_file) if uploaded_file
|
240
|
-
write(data ? convert_before_write(data) : nil)
|
241
|
-
end
|
344
|
+
# Converts a String or Hash value into an UploadedFile object and ensures
|
345
|
+
# it's uploaded to temporary storage.
|
346
|
+
#
|
347
|
+
# # from JSON data
|
348
|
+
# attacher.cached('{"id":"...","storage":"cache","metadata":{...}}')
|
349
|
+
# #=> #<Shrine::UploadedFile>
|
350
|
+
#
|
351
|
+
# # from Hash data
|
352
|
+
# attacher.cached({ "id" => "...", "storage" => "cache", "metadata" => { ... } })
|
353
|
+
# #=> #<Shrine::UploadedFile>
|
354
|
+
def cached(value, **)
|
355
|
+
uploaded_file = uploaded_file(value)
|
356
|
+
|
357
|
+
# reject files not uploaded to temporary storage, because otherwise
|
358
|
+
# attackers could hijack other users' attachments
|
359
|
+
unless cached?(uploaded_file)
|
360
|
+
fail Shrine::Error, "expected cached file, got #{uploaded_file.inspect}"
|
361
|
+
end
|
242
362
|
|
243
|
-
|
244
|
-
def write(value)
|
245
|
-
record.send(:"#{data_attribute}=", value)
|
363
|
+
uploaded_file
|
246
364
|
end
|
247
365
|
|
248
|
-
#
|
249
|
-
def
|
250
|
-
|
366
|
+
# Whether attached file should be uploaded to permanent storage.
|
367
|
+
def promote?
|
368
|
+
changed? && cached?
|
251
369
|
end
|
252
370
|
|
253
|
-
#
|
254
|
-
def
|
255
|
-
|
371
|
+
# Whether attached file should be deleted.
|
372
|
+
def destroy?
|
373
|
+
attached? && !cached?
|
256
374
|
end
|
257
375
|
|
258
|
-
#
|
259
|
-
def
|
260
|
-
|
376
|
+
# Whether assigning the given file is considered a change.
|
377
|
+
def change?(file)
|
378
|
+
@file != file
|
261
379
|
end
|
262
380
|
|
263
|
-
#
|
264
|
-
def
|
265
|
-
|
266
|
-
options[:action] = options[:phase] if options.key?(:phase)
|
267
|
-
options
|
381
|
+
# Returns whether the file is uploaded to specified storage.
|
382
|
+
def uploaded?(file, storage_key)
|
383
|
+
file&.storage_key == storage_key
|
268
384
|
end
|
269
385
|
end
|
270
386
|
|
data/lib/shrine/attachment.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Shrine
|
4
|
-
# Core class
|
5
|
-
#
|
6
|
-
#
|
4
|
+
# Core class that provides an attachment interface for a specified attribute
|
5
|
+
# name, which can be added to model/entity classes. The model/entity plugins
|
6
|
+
# define the main interface, which delegates to a Shrine::Attacher object.
|
7
7
|
class Attachment < Module
|
8
8
|
@shrine_class = ::Shrine
|
9
9
|
|
@@ -18,6 +18,13 @@ class Shrine
|
|
18
18
|
def inspect
|
19
19
|
"#{shrine_class.inspect}::Attachment"
|
20
20
|
end
|
21
|
+
|
22
|
+
# Shorthand for `Attachment.new`.
|
23
|
+
#
|
24
|
+
# Shrine::Attachment[:image]
|
25
|
+
def [](*args, **options)
|
26
|
+
new(*args, **options)
|
27
|
+
end
|
21
28
|
end
|
22
29
|
|
23
30
|
module InstanceMethods
|
@@ -27,40 +34,6 @@ class Shrine
|
|
27
34
|
def initialize(name, **options)
|
28
35
|
@name = name.to_sym
|
29
36
|
@options = options
|
30
|
-
|
31
|
-
define_attachment_methods!
|
32
|
-
end
|
33
|
-
|
34
|
-
# Defines attachment methods for the specified attachment name. These
|
35
|
-
# methods will be added to any model that includes this module.
|
36
|
-
def define_attachment_methods!
|
37
|
-
attachment = self
|
38
|
-
name = attachment_name
|
39
|
-
|
40
|
-
define_method :"#{name}_attacher" do |**options|
|
41
|
-
if !instance_variable_get(:"@#{name}_attacher") || options.any?
|
42
|
-
instance_variable_set(:"@#{name}_attacher", attachment.build_attacher(self, options))
|
43
|
-
else
|
44
|
-
instance_variable_get(:"@#{name}_attacher")
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
define_method :"#{name}=" do |value|
|
49
|
-
send(:"#{name}_attacher").assign(value)
|
50
|
-
end
|
51
|
-
|
52
|
-
define_method :"#{name}" do
|
53
|
-
send(:"#{name}_attacher").get
|
54
|
-
end
|
55
|
-
|
56
|
-
define_method :"#{name}_url" do |*args|
|
57
|
-
send(:"#{name}_attacher").url(*args)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
# Creates an instance of the corresponding Attacher subclass.
|
62
|
-
def build_attacher(object, options)
|
63
|
-
shrine_class::Attacher.new(object, @name, @options.merge(options))
|
64
37
|
end
|
65
38
|
|
66
39
|
# Returns name of the attachment this module provides.
|
@@ -75,17 +48,11 @@ class Shrine
|
|
75
48
|
|
76
49
|
# Returns class name with attachment name included.
|
77
50
|
#
|
78
|
-
# Shrine
|
79
|
-
def to_s
|
80
|
-
"#<#{self.class.inspect}(#{attachment_name})>"
|
81
|
-
end
|
82
|
-
|
83
|
-
# Returns class name with attachment name included.
|
84
|
-
#
|
85
|
-
# Shrine[:image].inspect #=> "#<Shrine::Attachment(image)>"
|
51
|
+
# Shrine::Attachment.new(:image).to_s #=> "#<Shrine::Attachment(image)>"
|
86
52
|
def inspect
|
87
|
-
"#<#{self.class.inspect}(#{
|
53
|
+
"#<#{self.class.inspect}(#{@name})>"
|
88
54
|
end
|
55
|
+
alias to_s inspect
|
89
56
|
|
90
57
|
# Returns the Shrine class that this attachment's class is namespaced
|
91
58
|
# under.
|