shrine 2.19.3 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +523 -41
  3. data/LICENSE.txt +1 -1
  4. data/README.md +83 -979
  5. data/doc/advantages.md +231 -204
  6. data/doc/attacher.md +304 -153
  7. data/doc/carrierwave.md +297 -226
  8. data/doc/changing_derivatives.md +308 -0
  9. data/doc/changing_location.md +103 -21
  10. data/doc/changing_storage.md +110 -0
  11. data/doc/creating_persistence_plugins.md +132 -0
  12. data/doc/creating_plugins.md +43 -23
  13. data/doc/creating_storages.md +19 -5
  14. data/doc/design.md +147 -97
  15. data/doc/direct_s3.md +38 -28
  16. data/doc/external/articles.md +63 -0
  17. data/doc/external/extensions.md +53 -0
  18. data/doc/external/misc.md +32 -0
  19. data/doc/getting_started.md +1156 -0
  20. data/doc/metadata.md +190 -109
  21. data/doc/multiple_files.md +93 -30
  22. data/doc/paperclip.md +384 -262
  23. data/doc/plugins/activerecord.md +177 -46
  24. data/doc/plugins/add_metadata.md +139 -38
  25. data/doc/plugins/atomic_helpers.md +217 -0
  26. data/doc/plugins/backgrounding.md +156 -98
  27. data/doc/plugins/cached_attachment_data.md +7 -5
  28. data/doc/plugins/column.md +121 -0
  29. data/doc/plugins/data_uri.md +23 -22
  30. data/doc/plugins/default_storage.md +36 -10
  31. data/doc/plugins/default_url.md +30 -13
  32. data/doc/plugins/delete_raw.md +4 -2
  33. data/doc/plugins/derivation_endpoint.md +186 -101
  34. data/doc/plugins/derivatives.md +839 -0
  35. data/doc/plugins/determine_mime_type.md +4 -2
  36. data/doc/plugins/download_endpoint.md +64 -8
  37. data/doc/plugins/dynamic_storage.md +5 -3
  38. data/doc/plugins/entity.md +263 -0
  39. data/doc/plugins/form_assign.md +55 -0
  40. data/doc/plugins/included.md +31 -8
  41. data/doc/plugins/infer_extension.md +21 -10
  42. data/doc/plugins/instrumentation.md +38 -16
  43. data/doc/plugins/keep_files.md +16 -17
  44. data/doc/plugins/metadata_attributes.md +42 -13
  45. data/doc/plugins/mirroring.md +118 -0
  46. data/doc/plugins/model.md +210 -0
  47. data/doc/plugins/module_include.md +4 -2
  48. data/doc/plugins/multi_cache.md +24 -0
  49. data/doc/plugins/persistence.md +101 -0
  50. data/doc/plugins/presign_endpoint.md +9 -4
  51. data/doc/plugins/pretty_location.md +16 -3
  52. data/doc/plugins/processing.md +4 -2
  53. data/doc/plugins/rack_file.md +8 -2
  54. data/doc/plugins/rack_response.md +6 -2
  55. data/doc/plugins/recache.md +4 -2
  56. data/doc/plugins/refresh_metadata.md +49 -9
  57. data/doc/plugins/remote_url.md +84 -47
  58. data/doc/plugins/remove_attachment.md +27 -6
  59. data/doc/plugins/remove_invalid.md +21 -6
  60. data/doc/plugins/restore_cached_data.md +11 -3
  61. data/doc/plugins/sequel.md +159 -35
  62. data/doc/plugins/signature.md +16 -5
  63. data/doc/plugins/store_dimensions.md +14 -2
  64. data/doc/plugins/tempfile.md +4 -2
  65. data/doc/plugins/type_predicates.md +96 -0
  66. data/doc/plugins/upload_endpoint.md +13 -13
  67. data/doc/plugins/upload_options.md +6 -4
  68. data/doc/plugins/{default_url_options.md → url_options.md} +9 -7
  69. data/doc/plugins/validation.md +97 -0
  70. data/doc/plugins/validation_helpers.md +16 -13
  71. data/doc/plugins/versions.md +15 -19
  72. data/doc/processing.md +438 -221
  73. data/doc/refile.md +188 -170
  74. data/doc/release_notes/1.0.0.md +4 -0
  75. data/doc/release_notes/1.1.0.md +6 -2
  76. data/doc/release_notes/1.2.0.md +4 -0
  77. data/doc/release_notes/1.3.0.md +4 -0
  78. data/doc/release_notes/1.4.0.md +4 -0
  79. data/doc/release_notes/1.4.1.md +4 -0
  80. data/doc/release_notes/1.4.2.md +4 -0
  81. data/doc/release_notes/2.0.0.md +4 -0
  82. data/doc/release_notes/2.0.1.md +4 -0
  83. data/doc/release_notes/2.1.0.md +5 -1
  84. data/doc/release_notes/2.1.1.md +4 -0
  85. data/doc/release_notes/2.10.0.md +4 -0
  86. data/doc/release_notes/2.10.1.md +4 -0
  87. data/doc/release_notes/2.11.0.md +4 -0
  88. data/doc/release_notes/2.12.0.md +4 -0
  89. data/doc/release_notes/2.13.0.md +4 -0
  90. data/doc/release_notes/2.14.0.md +5 -1
  91. data/doc/release_notes/2.15.0.md +11 -7
  92. data/doc/release_notes/2.16.0.md +4 -0
  93. data/doc/release_notes/2.17.0.md +4 -0
  94. data/doc/release_notes/2.18.0.md +4 -0
  95. data/doc/release_notes/2.19.0.md +6 -3
  96. data/doc/release_notes/2.2.0.md +4 -0
  97. data/doc/release_notes/2.3.0.md +4 -0
  98. data/doc/release_notes/2.3.1.md +4 -0
  99. data/doc/release_notes/2.4.0.md +4 -0
  100. data/doc/release_notes/2.4.1.md +4 -0
  101. data/doc/release_notes/2.5.0.md +4 -0
  102. data/doc/release_notes/2.6.0.md +4 -0
  103. data/doc/release_notes/2.6.1.md +4 -0
  104. data/doc/release_notes/2.7.0.md +4 -0
  105. data/doc/release_notes/2.8.0.md +4 -0
  106. data/doc/release_notes/2.9.0.md +4 -0
  107. data/doc/release_notes/3.0.0.md +981 -0
  108. data/doc/release_notes/3.0.1.md +22 -0
  109. data/doc/release_notes/3.1.0.md +73 -0
  110. data/doc/release_notes/3.2.0.md +96 -0
  111. data/doc/release_notes/3.2.1.md +31 -0
  112. data/doc/release_notes/3.2.2.md +14 -0
  113. data/doc/release_notes/3.3.0.md +105 -0
  114. data/doc/release_notes/3.4.0.md +35 -0
  115. data/doc/release_notes/3.5.0.md +63 -0
  116. data/doc/release_notes/3.6.0.md +23 -0
  117. data/doc/retrieving_uploads.md +5 -2
  118. data/doc/securing_uploads.md +60 -37
  119. data/doc/storage/file_system.md +20 -3
  120. data/doc/storage/memory.md +19 -0
  121. data/doc/storage/s3.md +122 -78
  122. data/doc/testing.md +141 -133
  123. data/doc/upgrading_to_3.md +708 -0
  124. data/doc/validation.md +54 -90
  125. data/lib/shrine/attacher.rb +292 -169
  126. data/lib/shrine/attachment.rb +13 -46
  127. data/lib/shrine/plugins/_persistence.rb +93 -0
  128. data/lib/shrine/plugins/activerecord.rb +77 -34
  129. data/lib/shrine/plugins/add_metadata.rb +25 -17
  130. data/lib/shrine/plugins/atomic_helpers.rb +119 -0
  131. data/lib/shrine/plugins/backgrounding.rb +77 -113
  132. data/lib/shrine/plugins/cached_attachment_data.rb +6 -15
  133. data/lib/shrine/plugins/column.rb +102 -0
  134. data/lib/shrine/plugins/data_uri.rb +38 -36
  135. data/lib/shrine/plugins/default_storage.rb +45 -15
  136. data/lib/shrine/plugins/default_url.rb +12 -24
  137. data/lib/shrine/plugins/default_url_options.rb +3 -30
  138. data/lib/shrine/plugins/delete_raw.rb +10 -16
  139. data/lib/shrine/plugins/derivation_endpoint.rb +130 -171
  140. data/lib/shrine/plugins/derivatives.rb +645 -0
  141. data/lib/shrine/plugins/determine_mime_type.rb +9 -21
  142. data/lib/shrine/plugins/download_endpoint.rb +118 -133
  143. data/lib/shrine/plugins/dynamic_storage.rb +5 -11
  144. data/lib/shrine/plugins/entity.rb +158 -0
  145. data/lib/shrine/plugins/form_assign.rb +108 -0
  146. data/lib/shrine/plugins/included.rb +6 -6
  147. data/lib/shrine/plugins/infer_extension.rb +17 -20
  148. data/lib/shrine/plugins/instrumentation.rb +59 -43
  149. data/lib/shrine/plugins/keep_files.rb +3 -15
  150. data/lib/shrine/plugins/metadata_attributes.rb +28 -19
  151. data/lib/shrine/plugins/mirroring.rb +142 -0
  152. data/lib/shrine/plugins/model.rb +160 -0
  153. data/lib/shrine/plugins/module_include.rb +3 -3
  154. data/lib/shrine/plugins/multi_cache.rb +27 -0
  155. data/lib/shrine/plugins/presign_endpoint.rb +27 -28
  156. data/lib/shrine/plugins/pretty_location.rb +15 -9
  157. data/lib/shrine/plugins/processing.rb +22 -9
  158. data/lib/shrine/plugins/rack_file.rb +2 -42
  159. data/lib/shrine/plugins/rack_response.rb +21 -10
  160. data/lib/shrine/plugins/recache.rb +6 -5
  161. data/lib/shrine/plugins/refresh_metadata.rb +13 -11
  162. data/lib/shrine/plugins/remote_url.rb +49 -49
  163. data/lib/shrine/plugins/remove_attachment.rb +12 -6
  164. data/lib/shrine/plugins/remove_invalid.rb +19 -8
  165. data/lib/shrine/plugins/restore_cached_data.rb +13 -7
  166. data/lib/shrine/plugins/sequel.rb +86 -36
  167. data/lib/shrine/plugins/signature.rb +10 -16
  168. data/lib/shrine/plugins/store_dimensions.rb +35 -40
  169. data/lib/shrine/plugins/tempfile.rb +1 -3
  170. data/lib/shrine/plugins/type_predicates.rb +113 -0
  171. data/lib/shrine/plugins/upload_endpoint.rb +28 -24
  172. data/lib/shrine/plugins/upload_options.rb +14 -15
  173. data/lib/shrine/plugins/url_options.rb +31 -0
  174. data/lib/shrine/plugins/validation.rb +80 -0
  175. data/lib/shrine/plugins/validation_helpers.rb +35 -58
  176. data/lib/shrine/plugins/versions.rb +107 -87
  177. data/lib/shrine/plugins.rb +22 -0
  178. data/lib/shrine/storage/file_system.rb +46 -64
  179. data/lib/shrine/storage/linter.rb +42 -7
  180. data/lib/shrine/storage/memory.rb +49 -0
  181. data/lib/shrine/storage/s3.rb +173 -160
  182. data/lib/shrine/uploaded_file.rb +32 -32
  183. data/lib/shrine/version.rb +3 -3
  184. data/lib/shrine.rb +87 -150
  185. data/shrine.gemspec +11 -12
  186. metadata +92 -82
  187. data/doc/migrating_storage.md +0 -76
  188. data/doc/plugins/backup.md +0 -31
  189. data/doc/plugins/copy.md +0 -24
  190. data/doc/plugins/delete_promoted.md +0 -12
  191. data/doc/plugins/direct_upload.md +0 -172
  192. data/doc/plugins/hooks.md +0 -58
  193. data/doc/plugins/logging.md +0 -42
  194. data/doc/plugins/migration_helpers.md +0 -60
  195. data/doc/plugins/moving.md +0 -19
  196. data/doc/plugins/multi_delete.md +0 -20
  197. data/doc/plugins/parallelize.md +0 -16
  198. data/doc/plugins/parsed_json.md +0 -23
  199. data/doc/regenerating_versions.md +0 -143
  200. data/lib/shrine/plugins/background_helpers.rb +0 -5
  201. data/lib/shrine/plugins/backup.rb +0 -90
  202. data/lib/shrine/plugins/copy.rb +0 -50
  203. data/lib/shrine/plugins/delete_promoted.rb +0 -20
  204. data/lib/shrine/plugins/direct_upload.rb +0 -217
  205. data/lib/shrine/plugins/hooks.rb +0 -90
  206. data/lib/shrine/plugins/logging.rb +0 -142
  207. data/lib/shrine/plugins/migration_helpers.rb +0 -70
  208. data/lib/shrine/plugins/moving.rb +0 -57
  209. data/lib/shrine/plugins/multi_delete.rb +0 -32
  210. data/lib/shrine/plugins/parallelize.rb +0 -78
  211. data/lib/shrine/plugins/parsed_json.rb +0 -29
@@ -0,0 +1,645 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Shrine
4
+ module Plugins
5
+ # Documentation can be found on https://shrinerb.com/docs/plugins/derivatives
6
+ module Derivatives
7
+ LOG_SUBSCRIBER = -> (event) do
8
+ Shrine.logger.info "Derivatives (#{event.duration}ms) – #{{
9
+ processor: event[:processor],
10
+ processor_options: event[:processor_options],
11
+ uploader: event[:uploader],
12
+ }.inspect}"
13
+ end
14
+
15
+ def self.load_dependencies(uploader, **)
16
+ uploader.plugin :default_url
17
+ end
18
+
19
+ def self.configure(uploader, log_subscriber: LOG_SUBSCRIBER, versions_compatibility: false, **opts)
20
+ uploader.opts[:derivatives] ||= { processors: {}, processor_settings: {}, storage: proc { store_key }, mutex: true }
21
+ uploader.opts[:derivatives].merge!(opts)
22
+
23
+ # instrumentation plugin integration
24
+ uploader.subscribe(:derivatives, &log_subscriber) if uploader.respond_to?(:subscribe)
25
+
26
+ uploader::Attacher.include(VersionsCompatibility) if versions_compatibility
27
+ end
28
+
29
+ module AttachmentMethods
30
+ def define_entity_methods(name)
31
+ super if defined?(super)
32
+
33
+ define_method(:"#{name}_derivatives") do |*args|
34
+ send(:"#{name}_attacher").get_derivatives(*args)
35
+ end
36
+ end
37
+
38
+ def define_model_methods(name)
39
+ super if defined?(super)
40
+
41
+ define_method(:"#{name}_derivatives!") do |*args, **options|
42
+ send(:"#{name}_attacher").create_derivatives(*args, **options)
43
+ end
44
+ end
45
+ end
46
+
47
+ module AttacherClassMethods
48
+ # Registers a derivatives processor on the attacher class.
49
+ #
50
+ # Attacher.derivatives_processor :thumbnails do |original|
51
+ # # ...
52
+ # end
53
+ #
54
+ # By default, Shrine will convert the source IO object into a file
55
+ # before it's passed to the processor block. You can set `download:
56
+ # false` to pass the source IO object to the processor block as is.
57
+ #
58
+ # Attacher.derivatives_processor :thumbnails, download: false do |original|
59
+ # # ...
60
+ # end
61
+ #
62
+ # This can be useful if you'd like to defer or avoid a possibly
63
+ # expensive download operation for processor logic that does not
64
+ # require it.
65
+ def derivatives_processor(name = :default, download: true, &block)
66
+ if block
67
+ shrine_class.derivatives_options[:processors][name.to_sym] = block
68
+ shrine_class.derivatives_options[:processor_settings][name.to_sym] = { download: download }
69
+ else
70
+ shrine_class.derivatives_options[:processors].fetch(name.to_sym) do
71
+ fail Error, "derivatives processor #{name.inspect} not registered" unless name == :default
72
+ end
73
+ end
74
+ end
75
+ alias derivatives derivatives_processor
76
+
77
+ # Returns settings for the given derivatives processor.
78
+ #
79
+ # Attacher.derivatives_processor_settings(:thumbnails) #=> { download: true }
80
+ def derivatives_processor_settings(name)
81
+ shrine_class.derivatives_options[:processor_settings][name.to_sym] || {}
82
+ end
83
+
84
+ # Specifies default storage to which derivatives will be uploaded.
85
+ #
86
+ # Attacher.derivatives_storage :other_store
87
+ # # or
88
+ # Attacher.derivatives_storage do |name|
89
+ # if name == :thumbnail
90
+ # :thumbnail_store
91
+ # else
92
+ # :store
93
+ # end
94
+ # end
95
+ def derivatives_storage(storage_key = nil, &block)
96
+ if storage_key || block
97
+ shrine_class.derivatives_options[:storage] = storage_key || block
98
+ else
99
+ shrine_class.derivatives_options[:storage]
100
+ end
101
+ end
102
+ end
103
+
104
+ module AttacherMethods
105
+ attr_reader :derivatives
106
+
107
+ # Adds the ability to accept derivatives.
108
+ def initialize(derivatives: {}, **options)
109
+ super(**options)
110
+
111
+ @derivatives = derivatives
112
+ @derivatives_mutex = Mutex.new if shrine_class.derivatives_options[:mutex]
113
+ end
114
+
115
+ # Convenience method for accessing derivatives.
116
+ #
117
+ # photo.image_derivatives[:thumb] #=> #<Shrine::UploadedFile>
118
+ # # can be shortened to
119
+ # photo.image(:thumb) #=> #<Shrine::UploadedFile>
120
+ def get(*path)
121
+ return super if path.empty?
122
+
123
+ get_derivatives(*path)
124
+ end
125
+
126
+ # Convenience method for accessing derivatives.
127
+ #
128
+ # photo.image_derivatives.dig(:thumbnails, :large)
129
+ # # can be shortened to
130
+ # photo.image_derivatives(:thumbnails, :large)
131
+ def get_derivatives(*path)
132
+ return derivatives if path.empty?
133
+
134
+ path = derivative_path(path)
135
+
136
+ derivatives.dig(*path)
137
+ end
138
+
139
+ # Allows generating a URL to the derivative by passing the derivative
140
+ # name.
141
+ #
142
+ # attacher.add_derivatives({ thumb: thumb })
143
+ # attacher.url(:thumb) #=> "https://example.org/thumb.jpg"
144
+ def url(*path, **options)
145
+ return super if path.empty?
146
+
147
+ path = derivative_path(path)
148
+
149
+ url = derivatives.dig(*path)&.url(**options)
150
+ url ||= default_url(**options, derivative: path)
151
+ url
152
+ end
153
+
154
+ # In addition to promoting the main file, also promotes any cached
155
+ # derivatives. This is useful when these derivatives are being created
156
+ # as part of a direct upload.
157
+ #
158
+ # attacher.assign(io)
159
+ # attacher.add_derivative(:thumb, file, storage: :cache)
160
+ # attacher.promote
161
+ # attacher.stored?(attacher.derivatives[:thumb]) #=> true
162
+ def promote(**options)
163
+ super
164
+ promote_derivatives
165
+ create_derivatives if create_derivatives_on_promote?
166
+ end
167
+
168
+ # Uploads any cached derivatives to permanent storage.
169
+ def promote_derivatives(**options)
170
+ stored_derivatives = map_derivative(derivatives) do |path, derivative|
171
+ if cached?(derivative)
172
+ upload_derivative(path, derivative, **options)
173
+ else
174
+ derivative
175
+ end
176
+ end
177
+
178
+ set_derivatives(stored_derivatives) unless derivatives == stored_derivatives
179
+ end
180
+
181
+ # In addition to deleting the main file it also deletes any derivatives.
182
+ #
183
+ # attacher.add_derivatives({ thumb: thumb })
184
+ # attacher.derivatives[:thumb].exists? #=> true
185
+ # attacher.destroy
186
+ # attacher.derivatives[:thumb].exists? #=> false
187
+ def destroy
188
+ super
189
+ delete_derivatives
190
+ end
191
+
192
+ # Calls processor and adds returned derivatives.
193
+ #
194
+ # Attacher.derivatives_processor :my_processor do |original|
195
+ # # ...
196
+ # end
197
+ #
198
+ # attacher.create_derivatives(:my_processor)
199
+ def create_derivatives(*args, storage: nil, **options)
200
+ files = process_derivatives(*args, **options)
201
+ add_derivatives(files, storage: storage)
202
+ end
203
+
204
+ # Uploads given hash of files and adds uploaded files to the
205
+ # derivatives hash.
206
+ #
207
+ # attacher.derivatives #=>
208
+ # # {
209
+ # # thumb: #<Shrine::UploadedFile>,
210
+ # # }
211
+ # attacher.add_derivatives({ cropped: cropped })
212
+ # attacher.derivatives #=>
213
+ # # {
214
+ # # thumb: #<Shrine::UploadedFile>,
215
+ # # cropped: #<Shrine::UploadedFile>,
216
+ # # }
217
+ def add_derivatives(files, **options)
218
+ new_derivatives = upload_derivatives(files, **options)
219
+ merge_derivatives(new_derivatives)
220
+ new_derivatives
221
+ end
222
+
223
+ # Uploads a given file and adds it to the derivatives hash.
224
+ #
225
+ # attacher.derivatives #=>
226
+ # # {
227
+ # # thumb: #<Shrine::UploadedFile>,
228
+ # # }
229
+ # attacher.add_derivative(:cropped, cropped)
230
+ # attacher.derivatives #=>
231
+ # # {
232
+ # # thumb: #<Shrine::UploadedFile>,
233
+ # # cropped: #<Shrine::UploadedFile>,
234
+ # # }
235
+ def add_derivative(name, file, **options)
236
+ add_derivatives({ name => file }, **options)
237
+ derivatives[name]
238
+ end
239
+
240
+ # Uploads given hash of files.
241
+ #
242
+ # hash = attacher.upload_derivatives({ thumb: thumb })
243
+ # hash[:thumb] #=> #<Shrine::UploadedFile>
244
+ def upload_derivatives(files, **options)
245
+ map_derivative(files) do |path, file|
246
+ upload_derivative(path, file, **options)
247
+ end
248
+ end
249
+
250
+ # Uploads the given file and deletes it afterwards.
251
+ #
252
+ # hash = attacher.upload_derivative(:thumb, thumb)
253
+ # hash[:thumb] #=> #<Shrine::UploadedFile>
254
+ def upload_derivative(path, file, storage: nil, **options)
255
+ path = derivative_path(path)
256
+ storage ||= derivative_storage(path)
257
+
258
+ file.open if file.is_a?(Tempfile) # refresh file descriptor
259
+ file.binmode if file.respond_to?(:binmode) # ensure binary mode
260
+
261
+ upload(file, storage, derivative: path, delete: true, action: :derivatives, **options)
262
+ end
263
+
264
+ # Downloads the attached file and calls the specified processor.
265
+ #
266
+ # Attacher.derivatives_processor :thumbnails do |original|
267
+ # processor = ImageProcessing::MiniMagick.source(original)
268
+ #
269
+ # {
270
+ # small: processor.resize_to_limit!(300, 300),
271
+ # medium: processor.resize_to_limit!(500, 500),
272
+ # large: processor.resize_to_limit!(800, 800),
273
+ # }
274
+ # end
275
+ #
276
+ # attacher.process_derivatives(:thumbnails)
277
+ # #=> { small: #<File:...>, medium: #<File:...>, large: #<File:...> }
278
+ def process_derivatives(processor_name = :default, source = nil, **options)
279
+ # handle receiving only source file without a processor
280
+ unless processor_name.respond_to?(:to_sym)
281
+ source = processor_name
282
+ processor_name = :default
283
+ end
284
+
285
+ source ||= file!
286
+
287
+ processor_settings = self.class.derivatives_processor_settings(processor_name) || {}
288
+
289
+ if processor_settings[:download]
290
+ shrine_class.with_file(source) do |file|
291
+ _process_derivatives(processor_name, file, **options)
292
+ end
293
+ else
294
+ _process_derivatives(processor_name, source, **options)
295
+ end
296
+ end
297
+
298
+ # Deep merges given uploaded derivatives with current derivatives.
299
+ #
300
+ # attacher.derivatives #=> { one: #<Shrine::UploadedFile> }
301
+ # attacher.merge_derivatives({ two: uploaded_file })
302
+ # attacher.derivatives #=> { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile> }
303
+ def merge_derivatives(new_derivatives)
304
+ derivatives_synchronize do
305
+ merged_derivatives = deep_merge_derivatives(derivatives, new_derivatives)
306
+ set_derivatives(merged_derivatives)
307
+ end
308
+ end
309
+
310
+ # Removes derivatives with specified name from the derivatives hash.
311
+ #
312
+ # attacher.derivatives
313
+ # #=> { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile>, three: #<Shrine::UploadedFile> }
314
+ #
315
+ # attacher.remove_derivatives(:two, :three)
316
+ # #=> [#<Shrine::UploadedFile>, #<Shrine::UploadedFile>] (removed derivatives)
317
+ #
318
+ # attacher.derivatives
319
+ # #=> { one: #<Shrine::UploadedFile> }
320
+ #
321
+ # Nested derivatives are also supported:
322
+ #
323
+ # attacher.derivatives
324
+ # #=> { nested: { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile>, three: #<Shrine::UploadedFile> } }
325
+ #
326
+ # attacher.remove_derivatives([:nested, :two], [:nested, :three])
327
+ # #=> [#<Shrine::UploadedFile>, #<Shrine::UploadedFile>] (removed derivatives)
328
+ #
329
+ # attacher.derivatives
330
+ # #=> { nested: { one: #<Shrine::UploadedFile> } }
331
+ #
332
+ # The :delete option can be passed for deleting removed derivatives:
333
+ #
334
+ # attacher.derivatives
335
+ # #=> { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile>, three: #<Shrine::UploadedFile> }
336
+ #
337
+ # two, three = attacher.remove_derivatives(:two, :three, delete: true)
338
+ #
339
+ # two.exists? #=> false
340
+ # three.exists? #=> false
341
+ def remove_derivatives(*paths, delete: false)
342
+ removed_derivatives = paths.map do |path|
343
+ path = Array(path)
344
+
345
+ if path.one?
346
+ derivatives.delete(path.first)
347
+ else
348
+ derivatives.dig(*path[0..-2]).delete(path[-1])
349
+ end
350
+ end
351
+
352
+ set_derivatives derivatives
353
+
354
+ delete_derivatives(removed_derivatives) if delete
355
+
356
+ removed_derivatives
357
+ end
358
+
359
+ # Removes derivative with specified name from the derivatives hash.
360
+ #
361
+ # attacher.derivatives #=> { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile> }
362
+ # attacher.remove_derivative(:one) #=> #<Shrine::UploadedFile> (removed derivative)
363
+ # attacher.derivatives #=> { two: #<Shrine::UploadedFile> }
364
+ #
365
+ # Nested derivatives are also supported:
366
+ #
367
+ # attacher.derivatives #=> { nested: { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile> } }
368
+ # attacher.remove_derivative([:nested, :one]) #=> #<Shrine::UploadedFile> (removed derivative)
369
+ # attacher.derivatives #=> { nested: { one: #<Shrine::UploadedFile> } }
370
+ #
371
+ # The :delete option can be passed for deleting removed derivative:
372
+ #
373
+ # attacher.derivatives #=> { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile> }
374
+ # derivative = attacher.remove_derivatives(:two, delete: true)
375
+ # derivative.exists? #=> false
376
+ def remove_derivative(path, **options)
377
+ remove_derivatives(path, **options).first
378
+ end
379
+
380
+ # Deletes given hash of uploaded files.
381
+ #
382
+ # attacher.delete_derivatives({ thumb: uploaded_file })
383
+ # uploaded_file.exists? #=> false
384
+ def delete_derivatives(derivatives = self.derivatives)
385
+ map_derivative(derivatives) { |_, derivative| derivative.delete }
386
+ end
387
+
388
+ # Sets the given hash of uploaded files as derivatives.
389
+ #
390
+ # attacher.set_derivatives({ thumb: uploaded_file })
391
+ # attacher.derivatives #=> { thumb: #<Shrine::UploadedFile> }
392
+ def set_derivatives(derivatives)
393
+ self.derivatives = derivatives
394
+ set file # trigger model write
395
+ derivatives
396
+ end
397
+
398
+ # Adds derivative data into the hash.
399
+ #
400
+ # attacher.attach(io)
401
+ # attacher.add_derivatives({ thumb: thumb })
402
+ # attacher.data
403
+ # #=>
404
+ # # {
405
+ # # "id" => "...",
406
+ # # "storage" => "store",
407
+ # # "metadata" => { ... },
408
+ # # "derivatives" => {
409
+ # # "thumb" => {
410
+ # # "id" => "...",
411
+ # # "storage" => "store",
412
+ # # "metadata" => { ... },
413
+ # # }
414
+ # # }
415
+ # # }
416
+ def data
417
+ result = super
418
+
419
+ if derivatives.any?
420
+ result ||= {}
421
+ result["derivatives"] = map_derivative(derivatives, transform_keys: :to_s) do |_, derivative|
422
+ derivative.data
423
+ end
424
+ end
425
+
426
+ result
427
+ end
428
+
429
+ # Loads derivatives from data generated by `Attacher#data`.
430
+ #
431
+ # attacher.load_data({
432
+ # "id" => "...",
433
+ # "storage" => "store",
434
+ # "metadata" => { ... },
435
+ # "derivatives" => {
436
+ # "thumb" => {
437
+ # "id" => "...",
438
+ # "storage" => "store",
439
+ # "metadata" => { ... },
440
+ # }
441
+ # }
442
+ # })
443
+ # attacher.file #=> #<Shrine::UploadedFile>
444
+ # attacher.derivatives #=> { thumb: #<Shrine::UploadedFile> }
445
+ def load_data(data)
446
+ data ||= {}
447
+ data = data.dup
448
+
449
+ derivatives_data = data.delete("derivatives") || data.delete(:derivatives) || {}
450
+ @derivatives = shrine_class.derivatives(derivatives_data)
451
+
452
+ data = nil if data.empty?
453
+
454
+ super(data)
455
+ end
456
+
457
+ # Clears derivatives when attachment changes.
458
+ #
459
+ # attacher.derivatives #=> { thumb: #<Shrine::UploadedFile> }
460
+ # attacher.change(file)
461
+ # attacher.derivatives #=> {}
462
+ def change(*)
463
+ result = super
464
+ set_derivatives({})
465
+ result
466
+ end
467
+
468
+ # Sets a hash of derivatives.
469
+ #
470
+ # attacher.derivatives = { thumb: Shrine.uploaded_file(...) }
471
+ # attacher.derivatives #=> { thumb: #<Shrine::UploadedFile ...> }
472
+ def derivatives=(derivatives)
473
+ unless derivatives.is_a?(Hash)
474
+ fail ArgumentError, "expected derivatives to be a Hash, got #{derivatives.inspect}"
475
+ end
476
+
477
+ @derivatives = derivatives
478
+ end
479
+
480
+ # Iterates through nested derivatives and maps results.
481
+ #
482
+ # attacher.map_derivative(derivatives) { |path, file| ... }
483
+ def map_derivative(derivatives, **options, &block)
484
+ shrine_class.map_derivative(derivatives, **options, &block)
485
+ end
486
+
487
+ private
488
+
489
+ # Calls the derivatives processor with the source file and options.
490
+ def _process_derivatives(processor_name, source, **options)
491
+ processor = self.class.derivatives_processor(processor_name)
492
+
493
+ return {} unless processor
494
+
495
+ result = instrument_derivatives(processor_name, source, options) do
496
+ instance_exec(source, **options, &processor)
497
+ end
498
+
499
+ unless result.is_a?(Hash)
500
+ fail Error, "expected derivatives processor #{processor_name.inspect} to return a Hash, got #{result.inspect}"
501
+ end
502
+
503
+ result
504
+ end
505
+
506
+ # Sends a `derivatives.shrine` event for instrumentation plugin.
507
+ def instrument_derivatives(processor_name, source, processor_options, &block)
508
+ return yield unless shrine_class.respond_to?(:instrument)
509
+
510
+ shrine_class.instrument(:derivatives, {
511
+ processor: processor_name,
512
+ processor_options: processor_options,
513
+ io: source,
514
+ attacher: self,
515
+ }, &block)
516
+ end
517
+
518
+ # Returns symbolized array or single key.
519
+ def derivative_path(path)
520
+ path = Array(path).map { |key| key.is_a?(String) ? key.to_sym : key }
521
+ path = path.first if path.one?
522
+ path
523
+ end
524
+
525
+ # Storage to which derivatives will be uploaded to by default.
526
+ def derivative_storage(path)
527
+ storage = self.class.derivatives_storage
528
+ storage = instance_exec(path, &storage) if storage.respond_to?(:call)
529
+ storage
530
+ end
531
+
532
+ # Deep merge nested hashes/arrays.
533
+ def deep_merge_derivatives(o1, o2)
534
+ if o1.is_a?(Hash) && o2.is_a?(Hash)
535
+ o1.merge(o2) { |_, v1, v2| deep_merge_derivatives(v1, v2) }
536
+ elsif o1.is_a?(Array) && o2.is_a?(Array)
537
+ o1 + o2
538
+ else
539
+ o2
540
+ end
541
+ end
542
+
543
+ # Whether to automatically create derivatives on promotion
544
+ def create_derivatives_on_promote?
545
+ shrine_class.derivatives_options[:create_on_promote]
546
+ end
547
+
548
+ def derivatives_synchronize
549
+ if @derivatives_mutex
550
+ @derivatives_mutex.synchronize { yield }
551
+ else
552
+ yield
553
+ end
554
+ end
555
+ end
556
+
557
+ module ClassMethods
558
+ # Converts data into a Hash of derivatives.
559
+ #
560
+ # Shrine.derivatives('{"thumb":{"id":"foo","storage":"store","metadata":{}}}')
561
+ # #=> { thumb: #<Shrine::UploadedFile id="foo" storage=:store metadata={}> }
562
+ #
563
+ # Shrine.derivatives({ "thumb" => { "id" => "foo", "storage" => "store", "metadata" => {} } })
564
+ # #=> { thumb: #<Shrine::UploadedFile id="foo" storage=:store metadata={}> }
565
+ #
566
+ # Shrine.derivatives({ thumb: { id: "foo", storage: "store", metadata: {} } })
567
+ # #=> { thumb: #<Shrine::UploadedFile id="foo" storage=:store metadata={}> }
568
+ def derivatives(object)
569
+ if object.is_a?(String)
570
+ derivatives JSON.parse(object)
571
+ elsif object.is_a?(Hash) || object.is_a?(Array)
572
+ map_derivative(
573
+ object,
574
+ transform_keys: :to_sym,
575
+ leaf: -> (value) { value.is_a?(Hash) && (value["id"] || value[:id]).is_a?(String) },
576
+ ) { |_, value| uploaded_file(value) }
577
+ else
578
+ fail ArgumentError, "cannot convert #{object.inspect} to derivatives"
579
+ end
580
+ end
581
+
582
+ # Iterates over a nested collection, yielding on each part of the path.
583
+ # If the block returns a truthy value, that branch is terminated
584
+ def map_derivative(object, path = [], transform_keys: :to_sym, leaf: nil, &block)
585
+ return enum_for(__method__, object) unless block_given?
586
+
587
+ if leaf && leaf.call(object)
588
+ yield path, object
589
+ elsif object.is_a?(Hash)
590
+ object.inject({}) do |hash, (key, value)|
591
+ key = key.send(transform_keys)
592
+
593
+ hash.merge! key => map_derivative(
594
+ value, [*path, key],
595
+ transform_keys: transform_keys, leaf: leaf,
596
+ &block
597
+ )
598
+ end
599
+ elsif object.is_a?(Array)
600
+ object.map.with_index do |value, idx|
601
+ map_derivative(
602
+ value, [*path, idx],
603
+ transform_keys: transform_keys, leaf: leaf,
604
+ &block
605
+ )
606
+ end
607
+ else
608
+ yield path, object
609
+ end
610
+ end
611
+
612
+ # Returns derivatives plugin options.
613
+ def derivatives_options
614
+ opts[:derivatives]
615
+ end
616
+ end
617
+
618
+ module FileMethods
619
+ def [](*keys)
620
+ if keys.any? { |key| key.is_a?(Symbol) }
621
+ 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)`?"
622
+ else
623
+ super
624
+ end
625
+ end
626
+ end
627
+
628
+ # Adds compatibility with how the versions plugin stores processed files.
629
+ module VersionsCompatibility
630
+ def load_data(data)
631
+ return super if data.nil?
632
+ return super if data["derivatives"] || data[:derivatives]
633
+ return super if (data["id"] || data[:id]).is_a?(String)
634
+
635
+ data = data.dup
636
+ original = data.delete("original") || data.delete(:original) || {}
637
+
638
+ super original.merge("derivatives" => data)
639
+ end
640
+ end
641
+ end
642
+
643
+ register_plugin(:derivatives, Derivatives)
644
+ end
645
+ end