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
@@ -0,0 +1,576 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Shrine
4
+ module Plugins
5
+ # Documentation lives in [doc/plugins/derivatives.md] on GitHub.
6
+ #
7
+ # [doc/plugins/derivatives.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/derivatives.md
8
+ module Derivatives
9
+ LOG_SUBSCRIBER = -> (event) do
10
+ Shrine.logger.info "Derivatives (#{event.duration}ms) – #{{
11
+ processor: event[:processor],
12
+ processor_options: event[:processor_options],
13
+ uploader: event[:uploader],
14
+ }.inspect}"
15
+ end
16
+
17
+ def self.load_dependencies(uploader, versions_compatibility: false, **)
18
+ uploader.plugin :default_url
19
+
20
+ AttacherMethods.prepend(VersionsCompatibility) if versions_compatibility
21
+ end
22
+
23
+ def self.configure(uploader, log_subscriber: LOG_SUBSCRIBER, **opts)
24
+ uploader.opts[:derivatives] ||= { processors: {}, storage: proc { store_key } }
25
+ uploader.opts[:derivatives].merge!(opts)
26
+
27
+ # instrumentation plugin integration
28
+ uploader.subscribe(:derivatives, &log_subscriber) if uploader.respond_to?(:subscribe)
29
+ end
30
+
31
+ module AttachmentMethods
32
+ def initialize(name, **options)
33
+ super
34
+
35
+ define_method(:"#{name}_derivatives") do |*args|
36
+ send(:"#{name}_attacher").get_derivatives(*args)
37
+ end
38
+ end
39
+ end
40
+
41
+ module AttacherClassMethods
42
+ # Registers a derivatives processor on the attacher class.
43
+ #
44
+ # Attacher.derivatives_processor :thumbnails do |original|
45
+ # # ...
46
+ # end
47
+ def derivatives_processor(name, &block)
48
+ shrine_class.opts[:derivatives][:processors][name.to_sym] = block
49
+ end
50
+
51
+ # Specifies default storage to which derivatives will be uploaded.
52
+ #
53
+ # Attacher.derivatives_storage :other_store
54
+ # # or
55
+ # Attacher.derivatives_storage do |name|
56
+ # if name == :thumbnail
57
+ # :thumbnail_store
58
+ # else
59
+ # :store
60
+ # end
61
+ # end
62
+ def derivatives_storage(storage_key = nil, &block)
63
+ fail ArgumentError, "storage key or block needs to be provided" unless storage_key || block
64
+
65
+ shrine_class.opts[:derivatives][:storage] = storage_key || block
66
+ end
67
+ end
68
+
69
+ module AttacherMethods
70
+ attr_reader :derivatives
71
+
72
+ # Adds the ability to accept derivatives.
73
+ def initialize(derivatives: {}, **options)
74
+ super(**options)
75
+
76
+ @derivatives = derivatives
77
+ @derivatives_mutex = Mutex.new
78
+ end
79
+
80
+ # Convenience method for accessing derivatives.
81
+ #
82
+ # photo.image_derivatives[:thumb] #=> #<Shrine::UploadedFile>
83
+ # # can be shortened to
84
+ # photo.image(:thumb) #=> #<Shrine::UploadedFile>
85
+ def get(*path)
86
+ return super if path.empty?
87
+
88
+ get_derivatives(*path)
89
+ end
90
+
91
+ # Convenience method for accessing derivatives.
92
+ #
93
+ # photo.image_derivatives.dig(:thumbnails, :large)
94
+ # # can be shortened to
95
+ # photo.image_derivatives(:thumbnails, :large)
96
+ def get_derivatives(*path)
97
+ return derivatives if path.empty?
98
+
99
+ path = derivative_path(path)
100
+
101
+ derivatives.dig(*path)
102
+ end
103
+
104
+ # Allows generating a URL to the derivative by passing the derivative
105
+ # name.
106
+ #
107
+ # attacher.add_derivatives(thumb: thumb)
108
+ # attacher.url(:thumb) #=> "https://example.org/thumb.jpg"
109
+ def url(*path, **options)
110
+ return super if path.empty?
111
+
112
+ path = derivative_path(path)
113
+
114
+ url = derivatives.dig(*path)&.url(**options)
115
+ url ||= default_url(**options, derivative: path)
116
+ url
117
+ end
118
+
119
+ # In addition to promoting the main file, also promotes any cached
120
+ # derivatives. This is useful when these derivatives are being created
121
+ # as part of a direct upload.
122
+ #
123
+ # attacher.assign(io)
124
+ # attacher.add_derivative(:thumb, file, storage: :cache)
125
+ # attacher.promote
126
+ # attacher.stored?(attacher.derivatives[:thumb]) #=> true
127
+ def promote(background: false, **options)
128
+ super
129
+ promote_derivatives unless background
130
+ end
131
+
132
+ # Uploads any cached derivatives to permanent storage.
133
+ def promote_derivatives(**options)
134
+ stored_derivatives = map_derivative(derivatives) do |path, derivative|
135
+ if cached?(derivative)
136
+ upload_derivative(path, derivative, **options)
137
+ else
138
+ derivative
139
+ end
140
+ end
141
+
142
+ set_derivatives(stored_derivatives) unless derivatives == stored_derivatives
143
+ end
144
+
145
+ # In addition to deleting the main file it also deletes any derivatives.
146
+ #
147
+ # attacher.add_derivatives(thumb: thumb)
148
+ # attacher.derivatives[:thumb].exists? #=> true
149
+ # attacher.destroy
150
+ # attacher.derivatives[:thumb].exists? #=> false
151
+ def destroy(background: false, **options)
152
+ super
153
+ delete_derivatives unless background
154
+ end
155
+
156
+ # Calls processor and adds returned derivatives.
157
+ #
158
+ # Attacher.derivatives_processor :my_processor do |original|
159
+ # # ...
160
+ # end
161
+ #
162
+ # attacher.create_derivatives(:my_processor)
163
+ def create_derivatives(processor_name, **options)
164
+ files = process_derivatives(processor_name)
165
+ add_derivatives(files, **options)
166
+ end
167
+
168
+ # Uploads given hash of files and adds uploaded files to the
169
+ # derivatives hash.
170
+ #
171
+ # attacher.derivatives #=>
172
+ # # {
173
+ # # thumb: #<Shrine::UploadedFile>,
174
+ # # }
175
+ # attacher.add_derivatives(cropped: cropped)
176
+ # attacher.derivatives #=>
177
+ # # {
178
+ # # thumb: #<Shrine::UploadedFile>,
179
+ # # cropped: #<Shrine::UploadedFile>,
180
+ # # }
181
+ def add_derivatives(files, **options)
182
+ new_derivatives = upload_derivatives(files, **options)
183
+ merge_derivatives(new_derivatives)
184
+ new_derivatives
185
+ end
186
+
187
+ # Uploads a given file and adds it to the derivatives hash.
188
+ #
189
+ # attacher.derivatives #=>
190
+ # # {
191
+ # # thumb: #<Shrine::UploadedFile>,
192
+ # # }
193
+ # attacher.add_derivative(:cropped, cropped)
194
+ # attacher.derivatives #=>
195
+ # # {
196
+ # # thumb: #<Shrine::UploadedFile>,
197
+ # # cropped: #<Shrine::UploadedFile>,
198
+ # # }
199
+ def add_derivative(name, file, **options)
200
+ add_derivatives({ name => file }, **options)
201
+ derivatives[name]
202
+ end
203
+
204
+ # Uploads given hash of files.
205
+ #
206
+ # hash = attacher.upload_derivatives(thumb: thumb)
207
+ # hash[:thumb] #=> #<Shrine::UploadedFile>
208
+ def upload_derivatives(files, **options)
209
+ map_derivative(files) do |path, file|
210
+ path = derivative_path(path)
211
+
212
+ upload_derivative(path, file, **options)
213
+ end
214
+ end
215
+
216
+ # Uploads the given file and deletes it afterwards.
217
+ #
218
+ # hash = attacher.upload_derivative(:thumb, thumb)
219
+ # hash[:thumb] #=> #<Shrine::UploadedFile>
220
+ def upload_derivative(path, file, storage: nil, **options)
221
+ storage ||= derivative_storage(path)
222
+
223
+ upload(file, storage, derivative: path, delete: true, **options)
224
+ end
225
+
226
+ # Downloads the attached file and calls the specified processor.
227
+ #
228
+ # Attacher.derivatives_processor :thumbnails do |original|
229
+ # processor = ImageProcessing::MiniMagick.source(original)
230
+ #
231
+ # {
232
+ # small: processor.resize_to_limit!(300, 300),
233
+ # medium: processor.resize_to_limit!(500, 500),
234
+ # large: processor.resize_to_limit!(800, 800),
235
+ # }
236
+ # end
237
+ #
238
+ # attacher.process_derivatives(:thumbnails)
239
+ # #=> { small: #<File:...>, medium: #<File:...>, large: #<File:...> }
240
+ def process_derivatives(processor_name, source = nil, **options)
241
+ processor = derivatives_processor(processor_name)
242
+ fetch_source = source ? source.method(:tap) : file!.method(:download)
243
+ result = nil
244
+
245
+ fetch_source.call do |source_file|
246
+ instrument_derivatives(processor_name, options) do
247
+ result = instance_exec(source_file, **options, &processor)
248
+ end
249
+ end
250
+
251
+ unless result.is_a?(Hash)
252
+ fail Error, "expected derivatives processor #{processor_name.inspect} to return a Hash, got #{result.inspect}"
253
+ end
254
+
255
+ result
256
+ end
257
+
258
+ # Deep merges given uploaded derivatives with current derivatives.
259
+ #
260
+ # attacher.derivatives #=> { one: #<Shrine::UploadedFile> }
261
+ # attacher.merge_derivatives(two: uploaded_file)
262
+ # attacher.derivatives #=> { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile> }
263
+ def merge_derivatives(new_derivatives)
264
+ @derivatives_mutex.synchronize do
265
+ merged_derivatives = deep_merge_derivatives(derivatives, new_derivatives)
266
+ set_derivatives(merged_derivatives)
267
+ end
268
+ end
269
+
270
+ # Removes derivatives with specified name from the derivatives hash.
271
+ #
272
+ # attacher.derivatives
273
+ # #=> { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile>, three: #<Shrine::UploadedFile> }
274
+ #
275
+ # attacher.remove_derivatives(:two, :three)
276
+ # #=> [#<Shrine::UploadedFile>, #<Shrine::UploadedFile>] (removed derivatives)
277
+ #
278
+ # attacher.derivatives
279
+ # #=> { one: #<Shrine::UploadedFile> }
280
+ #
281
+ # Nested derivatives are also supported:
282
+ #
283
+ # attacher.derivatives
284
+ # #=> { nested: { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile>, three: #<Shrine::UploadedFile> } }
285
+ #
286
+ # attacher.remove_derivatives([:nested, :two], [:nested, :three])
287
+ # #=> [#<Shrine::UploadedFile>, #<Shrine::UploadedFile>] (removed derivatives)
288
+ #
289
+ # attacher.derivatives
290
+ # #=> { nested: { one: #<Shrine::UploadedFile> } }
291
+ #
292
+ # The :delete option can be passed for deleting removed derivatives:
293
+ #
294
+ # attacher.derivatives
295
+ # #=> { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile>, three: #<Shrine::UploadedFile> }
296
+ #
297
+ # two, three = attacher.remove_derivatives(:two, :three, delete: true)
298
+ #
299
+ # two.exists? #=> false
300
+ # three.exists? #=> false
301
+ def remove_derivatives(*paths, delete: false)
302
+ removed_derivatives = paths.map do |path|
303
+ path = Array(path)
304
+
305
+ if path.one?
306
+ derivatives.delete(path.first)
307
+ else
308
+ derivatives.dig(*path[0..-2]).delete(path[-1])
309
+ end
310
+ end
311
+
312
+ set_derivatives derivatives
313
+
314
+ delete_derivatives(removed_derivatives) if delete
315
+
316
+ removed_derivatives
317
+ end
318
+
319
+ # Removes derivative with specified name from the derivatives hash.
320
+ #
321
+ # attacher.derivatives #=> { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile> }
322
+ # attacher.remove_derivative(:one) #=> #<Shrine::UploadedFile> (removed derivative)
323
+ # attacher.derivatives #=> { two: #<Shrine::UploadedFile> }
324
+ #
325
+ # Nested derivatives are also supported:
326
+ #
327
+ # attacher.derivatives #=> { nested: { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile> } }
328
+ # attacher.remove_derivative([:nested, :one]) #=> #<Shrine::UploadedFile> (removed derivative)
329
+ # attacher.derivatives #=> { nested: { one: #<Shrine::UploadedFile> } }
330
+ #
331
+ # The :delete option can be passed for deleting removed derivative:
332
+ #
333
+ # attacher.derivatives #=> { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile> }
334
+ # derivative = attacher.remove_derivatives(:two, delete: true)
335
+ # derivative.exists? #=> false
336
+ def remove_derivative(path, **options)
337
+ remove_derivatives(path, **options).first
338
+ end
339
+
340
+ # Deletes given hash of uploaded files.
341
+ #
342
+ # attacher.delete_derivatives(thumb: uploaded_file)
343
+ # uploaded_file.exists? #=> false
344
+ def delete_derivatives(derivatives = self.derivatives)
345
+ map_derivative(derivatives) { |_, derivative| derivative.delete }
346
+ end
347
+
348
+ # Sets the given hash of uploaded files as derivatives.
349
+ #
350
+ # attacher.set_derivatives(thumb: uploaded_file)
351
+ # attacher.derivatives #=> { thumb: #<Shrine::UploadedFile> }
352
+ def set_derivatives(derivatives)
353
+ self.derivatives = derivatives
354
+ set file # trigger model writing
355
+ derivatives
356
+ end
357
+
358
+ # Adds derivative data into the hash.
359
+ #
360
+ # attacher.attach(io)
361
+ # attacher.add_derivatives(thumb: thumb)
362
+ # attacher.data
363
+ # #=>
364
+ # # {
365
+ # # "id" => "...",
366
+ # # "storage" => "store",
367
+ # # "metadata" => { ... },
368
+ # # "derivatives" => {
369
+ # # "thumb" => {
370
+ # # "id" => "...",
371
+ # # "storage" => "store",
372
+ # # "metadata" => { ... },
373
+ # # }
374
+ # # }
375
+ # # }
376
+ def data
377
+ result = super
378
+
379
+ if derivatives.any?
380
+ result ||= {}
381
+ result["derivatives"] = map_derivative(derivatives, transform_keys: :to_s) do |_, derivative|
382
+ derivative.data
383
+ end
384
+ end
385
+
386
+ result
387
+ end
388
+
389
+ # Loads derivatives from data generated by `Attacher#data`.
390
+ #
391
+ # attacher.load_data({
392
+ # "id" => "...",
393
+ # "storage" => "store",
394
+ # "metadata" => { ... },
395
+ # "derivatives" => {
396
+ # "thumb" => {
397
+ # "id" => "...",
398
+ # "storage" => "store",
399
+ # "metadata" => { ... },
400
+ # }
401
+ # }
402
+ # })
403
+ # attacher.file #=> #<Shrine::UploadedFile>
404
+ # attacher.derivatives #=> { thumb: #<Shrine::UploadedFile> }
405
+ def load_data(data)
406
+ data ||= {}
407
+ data = data.dup
408
+
409
+ derivatives_data = data.delete("derivatives") || data.delete(:derivatives) || {}
410
+ @derivatives = shrine_class.derivatives(derivatives_data)
411
+
412
+ data = nil if data.empty?
413
+
414
+ super(data)
415
+ end
416
+
417
+ # Clears derivatives when attachment changes.
418
+ #
419
+ # attacher.derivatives #=> { thumb: #<Shrine::UploadedFile> }
420
+ # attacher.change(file)
421
+ # attacher.derivatives #=> {}
422
+ def change(*args)
423
+ result = super
424
+ set_derivatives({})
425
+ result
426
+ end
427
+
428
+ # Sets a hash of derivatives.
429
+ #
430
+ # attacher.derivatives = { thumb: Shrine.uploaded_file(...) }
431
+ # attacher.derivatives #=> { thumb: #<Shrine::UploadedFile ...> }
432
+ def derivatives=(derivatives)
433
+ unless derivatives.is_a?(Hash)
434
+ fail ArgumentError, "expected derivatives to be a Hash, got #{derivatives.inspect}"
435
+ end
436
+
437
+ @derivatives = derivatives
438
+ end
439
+
440
+ # Iterates through nested derivatives and maps results.
441
+ #
442
+ # attacher.map_derivative(derivatives) { |path, file| ... }
443
+ def map_derivative(*args, &block)
444
+ shrine_class.map_derivative(*args, &block)
445
+ end
446
+
447
+ private
448
+
449
+ # Sends a `derivatives.shrine` event for instrumentation plugin.
450
+ def instrument_derivatives(processor_name, processor_options, &block)
451
+ return yield unless shrine_class.respond_to?(:instrument)
452
+
453
+ shrine_class.instrument(
454
+ :derivatives,
455
+ processor: processor_name,
456
+ processor_options: processor_options,
457
+ &block
458
+ )
459
+ end
460
+
461
+ # Retrieves derivatives processor with specified name.
462
+ def derivatives_processor(name)
463
+ shrine_class.opts[:derivatives][:processors][name.to_sym] or
464
+ fail Error, "derivatives processor #{name.inspect} not registered"
465
+ end
466
+
467
+ # Returns symbolized array or single key.
468
+ def derivative_path(path)
469
+ path = path.map { |key| key.is_a?(String) ? key.to_sym : key }
470
+ path = path.first if path.one?
471
+ path
472
+ end
473
+
474
+ # Storage to which derivatives will be uploaded to by default.
475
+ def derivative_storage(path)
476
+ storage = shrine_class.opts[:derivatives][:storage]
477
+ storage = instance_exec(path, &storage) if storage.respond_to?(:call)
478
+ storage
479
+ end
480
+
481
+ # Deep merge nested hashes/arrays.
482
+ def deep_merge_derivatives(o1, o2)
483
+ if o1.is_a?(Hash) && o2.is_a?(Hash)
484
+ o1.merge(o2) { |_, v1, v2| deep_merge_derivatives(v1, v2) }
485
+ elsif o1.is_a?(Array) && o2.is_a?(Array)
486
+ o1 + o2
487
+ else
488
+ o2
489
+ end
490
+ end
491
+ end
492
+
493
+ module ClassMethods
494
+ # Converts data into a Hash of derivatives.
495
+ #
496
+ # Shrine.derivatives('{"thumb":{"id":"foo","storage":"store","metadata":{}}}')
497
+ # #=> { thumb: #<Shrine::UploadedFile @id="foo" @storage_key="store" @metadata={}> }
498
+ #
499
+ # Shrine.derivatives({ "thumb" => { "id" => "foo", "storage" => "store", "metadata" => {} } })
500
+ # #=> { thumb: #<Shrine::UploadedFile @id="foo" @storage_key="store" @metadata={}> }
501
+ #
502
+ # Shrine.derivatives({ thumb: { id: "foo", storage: "store", metadata: {} } })
503
+ # #=> { thumb: #<Shrine::UploadedFile @id="foo" @storage_key="store" @metadata={}> }
504
+ def derivatives(object)
505
+ if object.is_a?(String)
506
+ derivatives JSON.parse(object)
507
+ elsif object.is_a?(Hash) || object.is_a?(Array)
508
+ map_derivative(
509
+ object,
510
+ transform_keys: :to_sym,
511
+ leaf: -> (value) { value.is_a?(Hash) && (value["id"] || value[:id]).is_a?(String) },
512
+ ) { |_, value| uploaded_file(value) }
513
+ else
514
+ fail ArgumentError, "cannot convert #{object.inspect} to derivatives"
515
+ end
516
+ end
517
+
518
+ # Iterates over a nested collection, yielding on each part of the path.
519
+ # If the block returns a truthy value, that branch is terminated
520
+ def map_derivative(object, path = [], transform_keys: :to_sym, leaf: nil, &block)
521
+ return enum_for(__method__, object) unless block_given?
522
+
523
+ if leaf && leaf.call(object)
524
+ yield path, object
525
+ elsif object.is_a?(Hash)
526
+ object.inject({}) do |hash, (key, value)|
527
+ key = key.send(transform_keys)
528
+
529
+ hash.merge! key => map_derivative(
530
+ value, [*path, key],
531
+ transform_keys: transform_keys, leaf: leaf,
532
+ &block
533
+ )
534
+ end
535
+ elsif object.is_a?(Array)
536
+ object.map.with_index do |value, idx|
537
+ map_derivative(
538
+ value, [*path, idx],
539
+ transform_keys: transform_keys, leaf: leaf,
540
+ &block
541
+ )
542
+ end
543
+ else
544
+ yield path, object
545
+ end
546
+ end
547
+ end
548
+
549
+ module FileMethods
550
+ def [](*keys)
551
+ if keys.any? { |key| key.is_a?(Symbol) }
552
+ fail Error, "Shrine::UploadedFile#[] doesn't accept symbol metadata names. Did you happen to call `record.attachment[:derivative_name]` when you meant to call `record.attachment(:derivative_name)`?"
553
+ else
554
+ super
555
+ end
556
+ end
557
+ end
558
+
559
+ # Adds compatibility with how the versions plugin stores processed files.
560
+ module VersionsCompatibility
561
+ def load_data(data)
562
+ return super if data.nil?
563
+ return super if data["derivatives"] || data[:derivatives]
564
+ return super if (data["id"] || data[:id]).is_a?(String)
565
+
566
+ data = data.dup
567
+ original = data.delete("original") || data.delete(:original) || {}
568
+
569
+ super original.merge("derivatives" => data)
570
+ end
571
+ end
572
+ end
573
+
574
+ register_plugin(:derivatives, Derivatives)
575
+ end
576
+ end