shrine 2.19.4 → 3.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of shrine might be problematic. Click here for more details.

Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +299 -11
  3. data/README.md +9 -3
  4. data/doc/advantages.md +1 -1
  5. data/doc/carrierwave.md +4 -4
  6. data/doc/creating_persistence_plugins.md +172 -0
  7. data/doc/creating_plugins.md +1 -1
  8. data/doc/creating_storages.md +3 -1
  9. data/doc/design.md +2 -2
  10. data/doc/direct_s3.md +0 -22
  11. data/doc/paperclip.md +3 -3
  12. data/doc/plugins/activerecord.md +211 -42
  13. data/doc/plugins/atomic_helpers.md +153 -0
  14. data/doc/plugins/column.md +90 -0
  15. data/doc/plugins/derivation_endpoint.md +54 -62
  16. data/doc/plugins/derivatives.md +752 -0
  17. data/doc/plugins/entity.md +204 -0
  18. data/doc/plugins/infer_extension.md +8 -8
  19. data/doc/plugins/instrumentation.md +33 -13
  20. data/doc/plugins/keep_files.md +5 -15
  21. data/doc/plugins/model.md +157 -0
  22. data/doc/plugins/presign_endpoint.md +2 -1
  23. data/doc/plugins/refresh_metadata.md +44 -7
  24. data/doc/plugins/sequel.md +190 -33
  25. data/doc/plugins/{default_url_options.md → url_options.md} +5 -5
  26. data/doc/processing.md +1 -1
  27. data/doc/release_notes/1.1.0.md +2 -2
  28. data/doc/release_notes/2.15.0.md +1 -1
  29. data/doc/storage/s3.md +2 -2
  30. data/doc/testing.md +1 -1
  31. data/lib/shrine.rb +72 -138
  32. data/lib/shrine/attacher.rb +272 -176
  33. data/lib/shrine/attachment.rb +2 -42
  34. data/lib/shrine/plugins/activerecord.rb +103 -26
  35. data/lib/shrine/plugins/add_metadata.rb +9 -10
  36. data/lib/shrine/plugins/atomic_helpers.rb +111 -0
  37. data/lib/shrine/plugins/attacher_options.rb +55 -0
  38. data/lib/shrine/plugins/backgrounding.rb +147 -115
  39. data/lib/shrine/plugins/cached_attachment_data.rb +6 -9
  40. data/lib/shrine/plugins/column.rb +104 -0
  41. data/lib/shrine/plugins/data_uri.rb +35 -38
  42. data/lib/shrine/plugins/default_storage.rb +18 -12
  43. data/lib/shrine/plugins/default_url.rb +11 -21
  44. data/lib/shrine/plugins/default_url_options.rb +3 -30
  45. data/lib/shrine/plugins/delete_raw.rb +9 -13
  46. data/lib/shrine/plugins/derivation_endpoint.rb +75 -114
  47. data/lib/shrine/plugins/derivatives.rb +576 -0
  48. data/lib/shrine/plugins/determine_mime_type.rb +3 -15
  49. data/lib/shrine/plugins/download_endpoint.rb +83 -131
  50. data/lib/shrine/plugins/dynamic_storage.rb +4 -8
  51. data/lib/shrine/plugins/entity.rb +128 -0
  52. data/lib/shrine/plugins/form_assign.rb +107 -0
  53. data/lib/shrine/plugins/included.rb +4 -3
  54. data/lib/shrine/plugins/infer_extension.rb +10 -17
  55. data/lib/shrine/plugins/instrumentation.rb +45 -25
  56. data/lib/shrine/plugins/keep_files.rb +2 -12
  57. data/lib/shrine/plugins/metadata_attributes.rb +15 -14
  58. data/lib/shrine/plugins/model.rb +137 -0
  59. data/lib/shrine/plugins/module_include.rb +2 -0
  60. data/lib/shrine/plugins/presign_endpoint.rb +1 -15
  61. data/lib/shrine/plugins/pretty_location.rb +5 -5
  62. data/lib/shrine/plugins/processing.rb +21 -6
  63. data/lib/shrine/plugins/rack_file.rb +1 -39
  64. data/lib/shrine/plugins/rack_response.rb +14 -7
  65. data/lib/shrine/plugins/recache.rb +5 -2
  66. data/lib/shrine/plugins/refresh_metadata.rb +12 -8
  67. data/lib/shrine/plugins/remote_url.rb +44 -53
  68. data/lib/shrine/plugins/remove_attachment.rb +7 -2
  69. data/lib/shrine/plugins/remove_invalid.rb +8 -4
  70. data/lib/shrine/plugins/restore_cached_data.rb +12 -4
  71. data/lib/shrine/plugins/sequel.rb +115 -27
  72. data/lib/shrine/plugins/signature.rb +2 -7
  73. data/lib/shrine/plugins/store_dimensions.rb +13 -27
  74. data/lib/shrine/plugins/upload_endpoint.rb +14 -15
  75. data/lib/shrine/plugins/upload_options.rb +9 -8
  76. data/lib/shrine/plugins/url_options.rb +33 -0
  77. data/lib/shrine/plugins/validation.rb +87 -0
  78. data/lib/shrine/plugins/validation_helpers.rb +33 -54
  79. data/lib/shrine/plugins/versions.rb +106 -84
  80. data/lib/shrine/storage/file_system.rb +32 -57
  81. data/lib/shrine/storage/linter.rb +9 -1
  82. data/lib/shrine/storage/memory.rb +42 -0
  83. data/lib/shrine/storage/s3.rb +38 -146
  84. data/lib/shrine/uploaded_file.rb +22 -29
  85. data/lib/shrine/version.rb +4 -4
  86. data/shrine.gemspec +2 -3
  87. metadata +27 -54
  88. data/doc/plugins/backup.md +0 -31
  89. data/doc/plugins/copy.md +0 -24
  90. data/doc/plugins/delete_promoted.md +0 -12
  91. data/doc/plugins/direct_upload.md +0 -172
  92. data/doc/plugins/hooks.md +0 -58
  93. data/doc/plugins/logging.md +0 -42
  94. data/doc/plugins/migration_helpers.md +0 -60
  95. data/doc/plugins/moving.md +0 -19
  96. data/doc/plugins/multi_delete.md +0 -20
  97. data/doc/plugins/parallelize.md +0 -16
  98. data/doc/plugins/parsed_json.md +0 -23
  99. data/lib/shrine/plugins/background_helpers.rb +0 -5
  100. data/lib/shrine/plugins/backup.rb +0 -90
  101. data/lib/shrine/plugins/copy.rb +0 -50
  102. data/lib/shrine/plugins/delete_promoted.rb +0 -20
  103. data/lib/shrine/plugins/direct_upload.rb +0 -217
  104. data/lib/shrine/plugins/hooks.rb +0 -90
  105. data/lib/shrine/plugins/logging.rb +0 -142
  106. data/lib/shrine/plugins/migration_helpers.rb +0 -70
  107. data/lib/shrine/plugins/moving.rb +0 -57
  108. data/lib/shrine/plugins/multi_delete.rb +0 -32
  109. data/lib/shrine/plugins/parallelize.rb +0 -78
  110. data/lib/shrine/plugins/parsed_json.rb +0 -29
@@ -18,253 +18,349 @@ class Shrine
18
18
  "#{shrine_class.inspect}::Attacher"
19
19
  end
20
20
 
21
- # Block that is executed in context of Shrine::Attacher during
22
- # validation. Example:
23
- #
24
- # Shrine::Attacher.validate do
25
- # if get.size > 5*1024*1024
26
- # errors << "is too big (max is 5 MB)"
27
- # end
28
- # end
29
- def validate(&block)
30
- define_method(:validate_block, &block)
31
- private :validate_block
21
+ # Initializes the attacher from a data hash generated from `Attacher#data`.
22
+ #
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 uploader that is used for the temporary storage.
37
- attr_reader :cache
33
+ # Returns the attached uploaded file.
34
+ attr_reader :file
38
35
 
39
- # Returns the uploader that is used for the permanent storage.
40
- attr_reader :store
41
-
42
- # Returns the context that will be sent to the uploader when uploading
43
- # and deleting. Can be modified with additional data to be sent to the
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
- # Returns an array of validation errors created on file assignment in
48
- # the `Attacher.validate` block.
49
- attr_reader :errors
50
-
51
- # Initializes the necessary attributes.
52
- def initialize(record, name, cache: :cache, store: :store)
53
- @cache = shrine_class.new(cache)
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 = {}
57
46
  end
58
47
 
59
- # Returns the model instance associated with the attacher.
60
- def record; context[:record]; end
48
+ # Returns the temporary storage identifier.
49
+ def cache_key; @cache; end
50
+ # Returns the permanent storage identifier.
51
+ def store_key; @store; end
61
52
 
62
- # Returns the attachment name associated with the attacher.
63
- def name; context[:name]; end
53
+ # Returns the uploader that is used for the temporary storage.
54
+ def cache; shrine_class.new(cache_key); end
55
+ # Returns the uploader that is used for the permanent storage.
56
+ def store; shrine_class.new(store_key); end
64
57
 
65
- # Receives the attachment value from the form. It can receive an
66
- # already cached file as a JSON string, otherwise it assumes that it's
67
- # an IO object and uploads it to the temporary storage. The cached file
68
- # is then written to the attachment attribute in the JSON format.
58
+ # Calls #attach_cached, but skips if value is an empty string (this is
59
+ # useful when the uploaded file comes from form fields). Forwards any
60
+ # additional options to #attach_cached.
61
+ #
62
+ # attacher.assign(File.open(...))
63
+ # attacher.assign(File.open(...), metadata: { "foo" => "bar" })
64
+ # attacher.assign('{"id":"...","storage":"cache","metadata":{...}}')
65
+ # attacher.assign({ "id" => "...", "storage" => "cache", "metadata" => {} })
66
+ #
67
+ # # ignores the assignment when a blank string is given
68
+ # attacher.assign("")
69
69
  def assign(value, **options)
70
- if value.is_a?(String)
71
- return if value == "" || !cached?(uploaded_file(value))
72
- assign_cached(uploaded_file(value))
73
- else
74
- uploaded_file = cache!(value, action: :cache, **options) if value
75
- set(uploaded_file)
76
- end
77
- end
70
+ return if value == "" # skip empty hidden field
78
71
 
79
- # Accepts a Shrine::UploadedFile object and writes it to the attachment
80
- # attribute. It then runs file validations, and records that the
81
- # attachment has changed.
82
- def set(uploaded_file)
83
- file = get
84
- @old = file unless uploaded_file == file
85
- _set(uploaded_file)
86
- validate
72
+ attach_cached(value, **options)
87
73
  end
88
74
 
89
- # Runs the validations defined by `Attacher.validate`.
90
- def validate
91
- errors.clear
92
- validate_block if get
75
+ # Sets an existing cached file, or uploads an IO object to temporary
76
+ # storage and sets it via #attach. Forwards any additional options to
77
+ # #attach.
78
+ #
79
+ # # upload file to temporary storage and set the uploaded file.
80
+ # attacher.attach_cached(File.open(...))
81
+ #
82
+ # # foward additional options to the uploader
83
+ # attacher.attach_cached(File.open(...), metadata: { "foo" => "bar" })
84
+ #
85
+ # # sets an existing cached file from JSON data
86
+ # attacher.attach_cached('{"id":"...","storage":"cache","metadata":{...}}')
87
+ #
88
+ # # sets an existing cached file from Hash data
89
+ # attacher.attach_cached({ "id" => "...", "storage" => "cache", "metadata" => {} })
90
+ def attach_cached(value, **options)
91
+ if value.is_a?(String) || value.is_a?(Hash)
92
+ change(cached(value, **options), **options)
93
+ else
94
+ attach(value, storage: cache_key, action: :cache, **options)
95
+ end
93
96
  end
94
97
 
95
- # Returns true if a new file has been attached.
96
- def changed?
97
- instance_variable_defined?(:@old)
98
- end
99
- alias attached? changed?
98
+ # Uploads given IO object and changes the uploaded file.
99
+ #
100
+ # # uploads the file to permanent storage
101
+ # attacher.attach(io)
102
+ #
103
+ # # uploads the file to specified storage
104
+ # attacher.attach(io, storage: :other_store)
105
+ #
106
+ # # forwards additional options to the uploader
107
+ # attacher.attach(io, upload_options: { acl: "public-read" }, metadata: { "foo" => "bar" })
108
+ #
109
+ # # removes the attachment
110
+ # attacher.attach(nil)
111
+ def attach(io, storage: store_key, **options)
112
+ file = upload(io, storage, **options) if io
100
113
 
101
- # Plugins can override this if they want something to be done before
102
- # save.
103
- def save
114
+ change(file, **options)
104
115
  end
105
116
 
106
- # Deletes the old file and promotes the new one. Typically this should
107
- # be called after saving the model instance.
117
+ # Deletes any previous file and promotes newly attached cached file.
118
+ # It also clears any dirty tracking.
119
+ #
120
+ # # promoting cached file
121
+ # attacher.assign(io)
122
+ # attacher.cached? #=> true
123
+ # attacher.finalize
124
+ # attacher.stored?
125
+ #
126
+ # # deleting previous file
127
+ # previous_file = attacher.file
128
+ # previous_file.exists? #=> true
129
+ # attacher.assign(io)
130
+ # attacher.finalize
131
+ # previous_file.exists? #=> false
132
+ #
133
+ # # clearing dirty tracking
134
+ # attacher.assign(io)
135
+ # attacher.changed? #=> true
136
+ # attacher.finalize
137
+ # attacher.changed? #=> false
108
138
  def finalize
109
- return if !instance_variable_defined?(:@old)
110
- replace
111
- remove_instance_variable(:@old)
112
- _promote(action: :store) if cached?
139
+ destroy_previous
140
+ promote_cached
141
+ remove_instance_variable(:@previous) if changed?
113
142
  end
114
143
 
115
- # Delegates to #promote, overriden for backgrounding.
116
- def _promote(uploaded_file = get, **options)
117
- promote(uploaded_file, **options)
144
+ # Plugins can override this if they want something to be done in a
145
+ # "before save" callback.
146
+ def save
118
147
  end
119
148
 
120
- # Uploads the cached file to store, and writes the stored file to the
121
- # attachment attribute.
122
- def promote(uploaded_file = get, **options)
123
- stored_file = store!(uploaded_file, **options)
124
- result = swap(stored_file) or _delete(stored_file, action: :abort)
125
- result
149
+ # If a new cached file has been attached, uploads it to permanent storage.
150
+ # Any additional options are forwarded to #promote.
151
+ #
152
+ # attacher.assign(io)
153
+ # attacher.cached? #=> true
154
+ # attacher.promote_cached
155
+ # attacher.stored? #=> true
156
+ def promote_cached(**options)
157
+ promote(action: :store, **options) if changed? && cached?
126
158
  end
127
159
 
128
- # Calls #update, overriden in ORM plugins, and returns true if the
129
- # attachment was successfully updated.
130
- def swap(uploaded_file)
131
- update(uploaded_file)
132
- uploaded_file if uploaded_file == get
160
+ # Uploads current file to permanent storage and sets the stored file.
161
+ #
162
+ # attacher.cached? #=> true
163
+ # attacher.promote
164
+ # attacher.stored? #=> true
165
+ def promote(storage: store_key, **options)
166
+ set upload(file, storage, **options)
133
167
  end
134
168
 
135
- # Deletes the previous attachment that was replaced, typically called
136
- # after the model instance is saved with the new attachment.
137
- def replace
138
- _delete(@old, action: :replace) if @old && !cached?(@old)
169
+ # Delegates to `Shrine.upload`, passing the #context.
170
+ #
171
+ # # upload file to specified storage
172
+ # attacher.upload(io, :store) #=> #<Shrine::UploadedFile>
173
+ #
174
+ # # pass additional options for the uploader
175
+ # attacher.upload(io, :store, metadata: { "foo" => "bar" })
176
+ def upload(io, storage = store_key, **options)
177
+ shrine_class.upload(io, storage, **context, **options)
139
178
  end
140
179
 
141
- # Deletes the current attachment, typically called after destroying the
142
- # record.
143
- def destroy
144
- file = get
145
- _delete(file, action: :destroy) if file && !cached?(file)
180
+ # If a new file was attached, deletes previously attached file if any.
181
+ #
182
+ # previous_file = attacher.file
183
+ # attacher.attach(file)
184
+ # attacher.destroy_previous
185
+ # previous_file.exists? #=> false
186
+ def destroy_previous(**options)
187
+ @previous.destroy_attached(**options) if changed?
146
188
  end
147
189
 
148
- # Delegates to #delete!, overriden for backgrounding.
149
- def _delete(uploaded_file, **options)
150
- delete!(uploaded_file, **options)
190
+ # Destroys the attached file if it exists and is uploaded to permanent
191
+ # storage.
192
+ #
193
+ # attacher.file.exists? #=> true
194
+ # attacher.destroy_attached
195
+ # attacher.file.exists? #=> false
196
+ def destroy_attached(**options)
197
+ destroy(**options) if attached? && !cached?
151
198
  end
152
199
 
153
- # Returns the URL to the attached file if it's present. It forwards any
154
- # given URL options to the storage.
155
- def url(**options)
156
- get.url(**options) if read
200
+ # Destroys the attachment.
201
+ #
202
+ # attacher.file.exists? #=> true
203
+ # attacher.destroy
204
+ # attacher.file.exists? #=> false
205
+ def destroy(**options)
206
+ file&.delete
157
207
  end
158
208
 
159
- # Returns true if attachment is present and cached.
160
- def cached?(file = get)
161
- file && cache.uploaded?(file)
209
+ # Sets the uploaded file with dirty tracking, and runs validations.
210
+ #
211
+ # attacher.change(uploaded_file)
212
+ # attacher.file #=> #<Shrine::UploadedFile>
213
+ # attacher.changed? #=> true
214
+ def change(file, **)
215
+ @previous = dup unless @file == file
216
+ set(file)
162
217
  end
163
218
 
164
- # Returns true if attachment is present and stored.
165
- def stored?(file = get)
166
- file && store.uploaded?(file)
219
+ # Sets the uploaded file.
220
+ #
221
+ # attacher.set(uploaded_file)
222
+ # attacher.file #=> #<Shrine::UploadedFile>
223
+ # attacher.changed? #=> false
224
+ def set(file)
225
+ self.file = file
167
226
  end
168
227
 
169
- # Returns a Shrine::UploadedFile instantiated from the data written to
170
- # the attachment attribute.
228
+ # Returns the attached file.
229
+ #
230
+ # # when a file is attached
231
+ # attacher.get #=> #<Shrine::UploadedFile>
232
+ #
233
+ # # when no file is attached
234
+ # attacher.get #=> nil
171
235
  def get
172
- uploaded_file(read) if read
236
+ file
173
237
  end
174
238
 
175
- # Reads from the `<attachment>_data` attribute on the model instance.
176
- # It returns nil if the value is blank.
177
- def read
178
- value = record.send(data_attribute)
179
- convert_after_read(value) unless value.nil? || value.empty?
239
+ # If a file is attached, returns the uploaded file URL, otherwise returns
240
+ # nil. Any options are forwarded to the storage.
241
+ #
242
+ # attacher.file = file
243
+ # attacher.url #=> "https://..."
244
+ #
245
+ # attacher.file = nil
246
+ # attacher.url #=> nil
247
+ def url(**options)
248
+ file&.url(**options)
180
249
  end
181
250
 
182
- # Uploads the file using the #cache uploader, passing the #context.
183
- def cache!(io, **options)
184
- Shrine.deprecation("Sending :phase to Attacher#cache! is deprecated and will not be supported in Shrine 3. Use :action instead.") if options[:phase]
185
- cache.upload(io, context.merge(_equalize_phase_and_action(options)))
251
+ # Returns whether the attachment has changed.
252
+ #
253
+ # attacher.changed? #=> false
254
+ # attacher.attach(file)
255
+ # attacher.changed? #=> true
256
+ def changed?
257
+ instance_variable_defined?(:@previous)
186
258
  end
187
259
 
188
- # Uploads the file using the #store uploader, passing the #context.
189
- def store!(io, **options)
190
- Shrine.deprecation("Sending :phase to Attacher#store! is deprecated and will not be supported in Shrine 3. Use :action instead.") if options[:phase]
191
- store.upload(io, context.merge(_equalize_phase_and_action(options)))
260
+ # Returns whether a file is attached.
261
+ #
262
+ # attacher.attach(io)
263
+ # attacher.attached? #=> true
264
+ #
265
+ # attacher.attach(nil)
266
+ # attacher.attached? #=> false
267
+ def attached?
268
+ !!file
192
269
  end
193
270
 
194
- # Deletes the file using the uploader, passing the #context.
195
- def delete!(uploaded_file, **options)
196
- Shrine.deprecation("Sending :phase to Attacher#delete! is deprecated and will not be supported in Shrine 3. Use :action instead.") if options[:phase]
197
- store.delete(uploaded_file, context.merge(_equalize_phase_and_action(options)))
271
+ # Returns whether the file is uploaded to temporary storage.
272
+ #
273
+ # attacher.cached? # checks current file
274
+ # attacher.cached?(file) # checks given file
275
+ def cached?(file = self.file)
276
+ uploaded?(file, cache_key)
198
277
  end
199
278
 
200
- # Enhances `Shrine.uploaded_file` with the ability to recognize uploaded
201
- # files as JSON strings.
202
- def uploaded_file(object, &block)
203
- shrine_class.uploaded_file(object, &block)
279
+ # Returns whether the file is uploaded to permanent storage.
280
+ #
281
+ # attacher.stored? # checks current file
282
+ # attacher.stored?(file) # checks given file
283
+ def stored?(file = self.file)
284
+ uploaded?(file, store_key)
204
285
  end
205
286
 
206
- # The name of the attribute on the model instance that is used to store
207
- # the attachment data. Defaults to `<attachment>_data`.
208
- def data_attribute
209
- :"#{name}_data"
287
+ # Generates serializable data for the attachment.
288
+ #
289
+ # attacher.data #=> { "id" => "...", "storage" => "...", "metadata": { ... } }
290
+ def data
291
+ file&.data
210
292
  end
211
293
 
212
- # Returns the Shrine class that this attacher's class is namespaced
213
- # under.
214
- def shrine_class
215
- self.class.shrine_class
294
+ # Loads the uploaded file from data generated by `Attacher#data`.
295
+ #
296
+ # attacher.file #=> nil
297
+ # attacher.load_data({ "id" => "...", "storage" => "...", "metadata" => { ... } })
298
+ # attacher.file #=> #<Shrine::UploadedFile>
299
+ def load_data(data)
300
+ @file = data && uploaded_file(data)
216
301
  end
217
302
 
218
- private
219
-
220
- # Assigns a cached file.
221
- def assign_cached(cached_file)
222
- set(cached_file)
223
- end
303
+ # Saves the given uploaded file to an instance variable.
304
+ #
305
+ # attacher.file = uploaded_file
306
+ # attacher.file #=> #<Shrine::UploadedFile>
307
+ def file=(file)
308
+ unless file.is_a?(Shrine::UploadedFile) || file.nil?
309
+ fail ArgumentError, "expected file to be a Shrine::UploadedFile or nil, got #{file.inspect}"
310
+ end
224
311
 
225
- # Writes the uploaded file to the attachment attribute. Overriden in ORM
226
- # plugins to additionally save the model instance.
227
- def update(uploaded_file)
228
- _set(uploaded_file)
312
+ @file = file
229
313
  end
230
314
 
231
- # Performs validation actually.
232
- # This method is redefined with `Attacher.validate`.
233
- def validate_block
315
+ # Returns attached file or raises an exception if no file is attached.
316
+ def file!
317
+ file or fail Error, "no file is attached"
234
318
  end
235
319
 
236
- # Converts the UploadedFile to a data hash and writes it to the
237
- # attribute.
238
- def _set(uploaded_file)
239
- data = convert_to_data(uploaded_file) if uploaded_file
240
- write(data ? convert_before_write(data) : nil)
320
+ # Converts JSON or Hash data into a Shrine::UploadedFile object.
321
+ #
322
+ # attacher.uploaded_file('{"id":"...","storage":"...","metadata":{...}}')
323
+ # #=> #<Shrine::UploadedFile ...>
324
+ #
325
+ # attacher.uploaded_file({ "id" => "...", "storage" => "...", "metadata" => {} })
326
+ # #=> #<Shrine::UploadedFile ...>
327
+ def uploaded_file(value)
328
+ shrine_class.uploaded_file(value)
241
329
  end
242
330
 
243
- # Writes to the `<attachment>_data` attribute on the model instance.
244
- def write(value)
245
- record.send(:"#{data_attribute}=", value)
331
+ # Returns the Shrine class that this attacher's class is namespaced
332
+ # under.
333
+ def shrine_class
334
+ self.class.shrine_class
246
335
  end
247
336
 
248
- # Returns the data hash of the given UploadedFile.
249
- def convert_to_data(uploaded_file)
250
- uploaded_file.data
251
- end
337
+ private
252
338
 
253
- # Returns the hash value dumped to JSON.
254
- def convert_before_write(value)
255
- value.to_json
256
- end
339
+ # Converts a String or Hash value into an UploadedFile object and ensures
340
+ # it's uploaded to temporary storage.
341
+ #
342
+ # # from JSON data
343
+ # attacher.cached('{"id":"...","storage":"cache","metadata":{...}}')
344
+ # #=> #<Shrine::UploadedFile>
345
+ #
346
+ # # from Hash data
347
+ # attacher.cached({ "id" => "...", "storage" => "cache", "metadata" => { ... } })
348
+ # #=> #<Shrine::UploadedFile>
349
+ def cached(value, **)
350
+ uploaded_file = uploaded_file(value)
351
+
352
+ # reject files not uploaded to temporary storage, because otherwise
353
+ # attackers could hijack other users' attachments
354
+ unless cached?(uploaded_file)
355
+ fail Shrine::Error, "expected cached file, got #{value.inspect}"
356
+ end
257
357
 
258
- # Returns the read value unchanged.
259
- def convert_after_read(value)
260
- value
358
+ uploaded_file
261
359
  end
262
360
 
263
- # Temporary method used for transitioning from :phase to :action.
264
- def _equalize_phase_and_action(options)
265
- options[:phase] = options[:action] if options.key?(:action)
266
- options[:action] = options[:phase] if options.key?(:phase)
267
- options
361
+ # Returns whether the file is uploaded to specified storage.
362
+ def uploaded?(file, storage_key)
363
+ file&.storage_key == storage_key
268
364
  end
269
365
  end
270
366