shrine 2.19.4 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (209) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +485 -43
  3. data/LICENSE.txt +1 -1
  4. data/README.md +81 -977
  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 +102 -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 +1115 -0
  20. data/doc/metadata.md +190 -109
  21. data/doc/multiple_files.md +62 -34
  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 +162 -101
  34. data/doc/plugins/derivatives.md +829 -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 +14 -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 +185 -167
  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 +4 -0
  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/retrieving_uploads.md +4 -1
  116. data/doc/securing_uploads.md +60 -37
  117. data/doc/storage/file_system.md +20 -3
  118. data/doc/storage/memory.md +19 -0
  119. data/doc/storage/s3.md +117 -83
  120. data/doc/testing.md +124 -144
  121. data/doc/upgrading_to_3.md +710 -0
  122. data/doc/validation.md +54 -90
  123. data/lib/shrine/attacher.rb +287 -171
  124. data/lib/shrine/attachment.rb +13 -46
  125. data/lib/shrine/plugins/_persistence.rb +93 -0
  126. data/lib/shrine/plugins/activerecord.rb +77 -34
  127. data/lib/shrine/plugins/add_metadata.rb +25 -17
  128. data/lib/shrine/plugins/atomic_helpers.rb +119 -0
  129. data/lib/shrine/plugins/backgrounding.rb +77 -113
  130. data/lib/shrine/plugins/cached_attachment_data.rb +6 -15
  131. data/lib/shrine/plugins/column.rb +102 -0
  132. data/lib/shrine/plugins/data_uri.rb +38 -36
  133. data/lib/shrine/plugins/default_storage.rb +45 -15
  134. data/lib/shrine/plugins/default_url.rb +12 -24
  135. data/lib/shrine/plugins/default_url_options.rb +3 -30
  136. data/lib/shrine/plugins/delete_raw.rb +10 -16
  137. data/lib/shrine/plugins/derivation_endpoint.rb +89 -134
  138. data/lib/shrine/plugins/derivatives.rb +637 -0
  139. data/lib/shrine/plugins/determine_mime_type.rb +9 -21
  140. data/lib/shrine/plugins/download_endpoint.rb +109 -133
  141. data/lib/shrine/plugins/dynamic_storage.rb +5 -11
  142. data/lib/shrine/plugins/entity.rb +152 -0
  143. data/lib/shrine/plugins/form_assign.rb +108 -0
  144. data/lib/shrine/plugins/included.rb +6 -6
  145. data/lib/shrine/plugins/infer_extension.rb +13 -20
  146. data/lib/shrine/plugins/instrumentation.rb +54 -42
  147. data/lib/shrine/plugins/keep_files.rb +3 -15
  148. data/lib/shrine/plugins/metadata_attributes.rb +28 -19
  149. data/lib/shrine/plugins/mirroring.rb +142 -0
  150. data/lib/shrine/plugins/model.rb +158 -0
  151. data/lib/shrine/plugins/module_include.rb +3 -3
  152. data/lib/shrine/plugins/multi_cache.rb +27 -0
  153. data/lib/shrine/plugins/presign_endpoint.rb +18 -22
  154. data/lib/shrine/plugins/pretty_location.rb +15 -9
  155. data/lib/shrine/plugins/processing.rb +22 -9
  156. data/lib/shrine/plugins/rack_file.rb +2 -42
  157. data/lib/shrine/plugins/rack_response.rb +15 -10
  158. data/lib/shrine/plugins/recache.rb +6 -5
  159. data/lib/shrine/plugins/refresh_metadata.rb +13 -11
  160. data/lib/shrine/plugins/remote_url.rb +49 -49
  161. data/lib/shrine/plugins/remove_attachment.rb +10 -6
  162. data/lib/shrine/plugins/remove_invalid.rb +19 -8
  163. data/lib/shrine/plugins/restore_cached_data.rb +13 -7
  164. data/lib/shrine/plugins/sequel.rb +86 -36
  165. data/lib/shrine/plugins/signature.rb +10 -16
  166. data/lib/shrine/plugins/store_dimensions.rb +35 -40
  167. data/lib/shrine/plugins/tempfile.rb +1 -3
  168. data/lib/shrine/plugins/type_predicates.rb +113 -0
  169. data/lib/shrine/plugins/upload_endpoint.rb +25 -23
  170. data/lib/shrine/plugins/upload_options.rb +14 -15
  171. data/lib/shrine/plugins/url_options.rb +31 -0
  172. data/lib/shrine/plugins/validation.rb +80 -0
  173. data/lib/shrine/plugins/validation_helpers.rb +34 -57
  174. data/lib/shrine/plugins/versions.rb +107 -87
  175. data/lib/shrine/plugins.rb +22 -0
  176. data/lib/shrine/storage/file_system.rb +46 -64
  177. data/lib/shrine/storage/linter.rb +42 -7
  178. data/lib/shrine/storage/memory.rb +49 -0
  179. data/lib/shrine/storage/s3.rb +154 -158
  180. data/lib/shrine/uploaded_file.rb +28 -30
  181. data/lib/shrine/version.rb +3 -3
  182. data/lib/shrine.rb +86 -149
  183. data/shrine.gemspec +9 -10
  184. metadata +79 -83
  185. data/doc/migrating_storage.md +0 -76
  186. data/doc/plugins/backup.md +0 -31
  187. data/doc/plugins/copy.md +0 -24
  188. data/doc/plugins/delete_promoted.md +0 -12
  189. data/doc/plugins/direct_upload.md +0 -172
  190. data/doc/plugins/hooks.md +0 -58
  191. data/doc/plugins/logging.md +0 -42
  192. data/doc/plugins/migration_helpers.md +0 -60
  193. data/doc/plugins/moving.md +0 -19
  194. data/doc/plugins/multi_delete.md +0 -20
  195. data/doc/plugins/parallelize.md +0 -16
  196. data/doc/plugins/parsed_json.md +0 -23
  197. data/doc/regenerating_versions.md +0 -143
  198. data/lib/shrine/plugins/background_helpers.rb +0 -5
  199. data/lib/shrine/plugins/backup.rb +0 -90
  200. data/lib/shrine/plugins/copy.rb +0 -50
  201. data/lib/shrine/plugins/delete_promoted.rb +0 -20
  202. data/lib/shrine/plugins/direct_upload.rb +0 -217
  203. data/lib/shrine/plugins/hooks.rb +0 -90
  204. data/lib/shrine/plugins/logging.rb +0 -142
  205. data/lib/shrine/plugins/migration_helpers.rb +0 -70
  206. data/lib/shrine/plugins/moving.rb +0 -57
  207. data/lib/shrine/plugins/multi_delete.rb +0 -32
  208. data/lib/shrine/plugins/parallelize.rb +0 -78
  209. data/lib/shrine/plugins/parsed_json.rb +0 -29
@@ -0,0 +1,981 @@
1
+ ---
2
+ title: Shrine 3.0.0
3
+ ---
4
+
5
+ This guide covers all the changes in the 3.0.0 version of Shrine. If you're
6
+ currently using Shrine 2.x, see **[Upgrading to Shrine 3.x]** for instructions
7
+ on how to upgrade.
8
+
9
+ ## Major features
10
+
11
+ ### Derivatives
12
+
13
+ The new **[`derivatives`][derivatives]** plugin has been added for storing
14
+ additional processed files alongside the main file.
15
+
16
+ ```rb
17
+ Shrine.plugin :derivatives
18
+ ```
19
+ ```rb
20
+ class ImageUploader < Shrine
21
+ Attacher.derivatives_processor do |original|
22
+ magick = ImageProcessing::MiniMagick.source(original)
23
+
24
+ {
25
+ large: magick.resize_to_limit!(800, 800),
26
+ medium: magick.resize_to_limit!(500, 500),
27
+ small: magick.resize_to_limit!(300, 300),
28
+ }
29
+ end
30
+ end
31
+ ```
32
+ ```rb
33
+ photo = Photo.new(photo_params)
34
+ photo.image_derivatives! # creates derivatives
35
+ photo.save
36
+ ```
37
+
38
+ This is a rewrite of the [`versions`][versions] plugin, bringing numerous
39
+ improvements:
40
+
41
+ * processed files are separated from the main file
42
+
43
+ ```rb
44
+ photo.image_data #=>
45
+ # {
46
+ # "id": "original.jpg",
47
+ # "storage": "store",
48
+ # "metadata": { ... },
49
+ # "derivatives": {
50
+ # "large": { "id": "large.jpg", "storage": "store", "metadata": { ... } },
51
+ # "medium": { "id": "medium.jpg", "storage": "store", "metadata": { ... } },
52
+ # "small": { "id": "small.jpg", "storage": "store", "metadata": { ... } }
53
+ # }
54
+ # }
55
+
56
+ photo.image #=> #<Shrine::UploadedFile id="original.jpg" ...>
57
+ photo.image_derivatives #=>
58
+ # {
59
+ # large: #<Shrine::UploadedFile id="large.jpg" ...>,
60
+ # medium: #<Shrine::UploadedFile id="medium.jpg" ...>,
61
+ # small: #<Shrine::UploadedFile id="small.jpg" ...>,
62
+ # }
63
+ ```
64
+
65
+ * processing is decoupled from promotion
66
+
67
+ ```rb
68
+ photo = Photo.create(image: file) # promote original file to permanent storage
69
+ photo.image_derivatives! # generate derivatives after promotion
70
+ photo.save # save derivatives data
71
+ ```
72
+
73
+ - ability to add or remove processed files at any point
74
+
75
+ ```rb
76
+ class ImageUploader < Shrine
77
+ Attacher.derivatives_processor :thumbnails do |original|
78
+ # ...
79
+ end
80
+
81
+ Attacher.derivatives_processor :crop do |original, left:, top:, width:, height:|
82
+ vips = ImageProcessing::Vips.source(original)
83
+
84
+ { cropped: vips.crop!(left, top, width, height) }
85
+ end
86
+ end
87
+ ```
88
+ ```rb
89
+ photo.image_derivatives!(:thumbnails)
90
+ photo.image_derivatives #=> { large: ..., medium: ..., small: ... }
91
+ photo.save
92
+
93
+ # ... sometime later ...
94
+
95
+ photo.image_derivatives!(:crop, left: 0, top: 0, width: 300, height: 300)
96
+ photo.image_derivatives #=> { large: ..., medium: ..., small: ..., cropped: ... }
97
+ photo.save
98
+ ```
99
+
100
+ * possibility of uploading processed files to different storage
101
+
102
+ ```rb
103
+ class ImageUploader < Shrine
104
+ # specify storage for all derivatives
105
+ Attacher.derivatives_storage :other_store
106
+
107
+ # or specify storage per derivative
108
+ Attacher.derivatives_storage { |derivative| :other_store }
109
+ end
110
+ ```
111
+ ```rb
112
+ photo = Photo.create(image: file)
113
+ photo.image.storage_key #=> :store
114
+
115
+ photo.image_derivatives!
116
+ photo.image_derivatives[:large].storage_key #=> :other_store
117
+ ```
118
+
119
+ ### Attacher redesign
120
+
121
+ The [`Shrine::Attacher`][attacher] class has been rewritten and can now be
122
+ used without models:
123
+
124
+ ```rb
125
+ attacher = Shrine::Attacher.new
126
+ attacher.attach(file)
127
+ attacher.file #=> #<Shrine::UploadedFile>
128
+ ```
129
+
130
+ The `Attacher#data`, `Attacher#load_data`, and `Attacher.from_data` methods
131
+ have been added for dumping and loading the attached file:
132
+
133
+ ```rb
134
+ # dump attached file into a serializable Hash
135
+ data = attacher.data #=> { "id" => "abc123.jpg", "storage" => "store", "metadata" => { ... } }
136
+ ```
137
+ ```rb
138
+ # initialize attacher from attached file data...
139
+ attacher = Shrine::Attacher.from_data(data)
140
+ attacher.file #=> #<Shrine::UploadedFile id="abc123.jpg" storage=:store metadata={...}>
141
+
142
+ # ...or load attached file into an existing attacher
143
+ attacher = Shrine::Attacher.new
144
+ attacher.load_data(data)
145
+ attacher.file #=> #<Shrine::UploadedFile>
146
+ ```
147
+
148
+ Several more methods have been added:
149
+
150
+ - `Attacher#attach` – attaches the file directly to permanent storage
151
+ - `Attacher#attach_cached` – extracted from `Attacher#assign`
152
+ - `Attacher#upload` – calls `Shrine#upload`, passing `:record` and `:name` context
153
+ - `Attacher#file` – alias for `Attacher#get`
154
+ - `Attacher#cache_key` – returns temporary storage key (`:cache` by default)
155
+ - `Attacher#store_key` – returns permanent storage key (`:store` by default)
156
+
157
+ #### Column
158
+
159
+ The new [`column`][column] plugin adds the ability to serialize attached file
160
+ data, in format suitable for writing into a database column.
161
+
162
+ ```rb
163
+ Shrine.plugin :column
164
+ ```
165
+ ```rb
166
+ # dump attached file data into a JSON string
167
+ data = attacher.column_data #=> '{"id":"abc123.jpg","storage":"store","metadata":{...}}'
168
+ ```
169
+ ```rb
170
+ # initialize attacher from attached file data...
171
+ attacher = Shrine::Attacher.from_column(data)
172
+ attacher.file #=> #<Shrine::UploadedFile id="abc123.jpg" storage=:store metadata={...}>
173
+
174
+ # ...or load attached file into an existing attacher
175
+ attacher = Shrine::Attacher.new
176
+ attacher.load_column(data)
177
+ attacher.file #=> #<Shrine::UploadedFile>
178
+ ```
179
+
180
+ #### Entity
181
+
182
+ The new [`entity`][entity] plugin adds support for immutable structs, which
183
+ are commonly used with ROM, Hanami and dry-rb.
184
+
185
+ ```rb
186
+ Shrine.plugin :entity
187
+ ```
188
+ ```rb
189
+ class Photo < Hanami::Entity
190
+ include Shrine::Attachment(:image)
191
+ end
192
+ ```
193
+ ```rb
194
+ photo = Photo.new(image_data: '{"id":"abc123.jpg","storage":"store","metadata":{...}}')
195
+ photo.image #=> #<Shrine::UploadedFile id="abc123.jpg" storage=:store ...>
196
+ ```
197
+
198
+ #### Model
199
+
200
+ The new [`model`][model] plugin adds support for mutable structs, which is
201
+ used by `activerecord` and `sequel` plugins.
202
+
203
+ ```rb
204
+ Shrine.plugin :model
205
+ ```
206
+ ```rb
207
+ class Photo < Struct.new(:image_data)
208
+ include Shrine::Attachment(:image)
209
+ end
210
+ ```
211
+ ```rb
212
+ photo = Photo.new
213
+ photo.image = file
214
+ photo.image #=> #<Shrine::UploadedFile id="abc123.jpg" storage=:cache ...>
215
+ photo.image_data #=> #=> '{"id":"abc123.jpg", "storage":"cache", "metadata":{...}}'
216
+ ```
217
+
218
+ ### Backgrounding rewrite
219
+
220
+ * The [`backgrounding`][backgrounding] plugin has been rewritten for more
221
+ flexibility and simplicity. The new usage is much more explicit:
222
+
223
+ ```rb
224
+ Shrine.plugin :backgrounding
225
+ Shrine::Attacher.promote_block do
226
+ PromoteJob.perform_async(self.class.name, record.class.name, record.id, name, file_data)
227
+ end
228
+ Shrine::Attacher.destroy_block do
229
+ DestroyJob.perform_async(self.class.name, data)
230
+ end
231
+ ```
232
+ ```rb
233
+ class PromoteJob
234
+ include Sidekiq::Worker
235
+
236
+ def perform(attacher_class, record_class, record.id, name, file_data)
237
+ attacher_class = Object.const_get(attacher_class)
238
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
239
+
240
+ attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
241
+ attacher.atomic_promote
242
+ rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
243
+ # attachment has changed or the record has been deleted, nothing to do
244
+ end
245
+ end
246
+ ```
247
+ ```rb
248
+ class DestroyJob
249
+ include Sidekiq::Worker
250
+
251
+ def perform(attacher_class, data)
252
+ attacher_class = Object.const_get(attacher_class)
253
+
254
+ attacher = attacher_class.from_data(data)
255
+ attacher.destroy
256
+ end
257
+ end
258
+ ```
259
+
260
+ There are several main differences compared to the old implementation:
261
+
262
+ - we are in charge of passing the record to the background job
263
+ - we can access the attacher before promotion
264
+ - we can react to errors that caused promotion to abort
265
+
266
+ We can now also register backgrounding hooks on an attacher instance, allowing
267
+ us to pass additional parameters to the background job:
268
+
269
+ ```rb
270
+ photo = Photo.new(photo_params)
271
+
272
+ photo.image_attacher.promote_block do |attacher|
273
+ PromoteJob.perform_async(
274
+ attacher.class.name,
275
+ attacher.record.class.name,
276
+ attacher.record.id,
277
+ attacher.name,
278
+ attacher.file_data,
279
+ current_user.id, # <== parameters from the controller
280
+ )
281
+ end
282
+
283
+ photo.save # will call our instance-level backgrounding hook
284
+ ```
285
+
286
+ ### Persistence interface
287
+
288
+ The persistence plugins (`activerecord`, `sequel`) now implement a unified
289
+ [persistence] interface:
290
+
291
+ | Method | Description |
292
+ | :----------------- | :---------- |
293
+ | `Attacher#persist` | persists attachment data |
294
+ | `Attacher#atomic_persist` | persists attachment data if attachment hasn’t changed |
295
+ | `Attacher#atomic_promote` | promotes cached file and atomically persists changes |
296
+
297
+ The "atomic" methods use the new [`atomic_helpers`][atomic_helpers] plugin,
298
+ and are useful for background jobs. For example, this is how we'd use them to
299
+ implement metadata extraction in the background in a concurrency-safe way:
300
+
301
+ ```rb
302
+ MetadataJob.perform_async(
303
+ attacher.class.name,
304
+ attacher.record.class.name,
305
+ attacher.record.id,
306
+ attacher.name,
307
+ attacher.file_data,
308
+ )
309
+ ```
310
+ ```rb
311
+ class MetadataJob
312
+ include Sidekiq::Worker
313
+
314
+ def perform(attacher_class, record_class, record_id, name, file_data)
315
+ attacher_class = Object.const_get(attacher_class)
316
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
317
+
318
+ attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
319
+ attacher.refresh_metadata! # extract metadata
320
+ attacher.atomic_persist # persist if attachment hasn't changed
321
+ rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
322
+ # attachment has changed or record has been deleted, nothing to do
323
+ end
324
+ end
325
+ ```
326
+
327
+ ## Other new plugins
328
+
329
+ * The new [`mirroring`][mirroring] plugin has been added for replicating
330
+ uploads and deletes to other storages.
331
+
332
+ ```rb
333
+ Shrine.storages = { cache: ..., store: ..., backup: ... }
334
+
335
+ Shrine.plugin :mirroring, mirror: { store: :backup }
336
+ ```
337
+ ```rb
338
+ file = Shrine.upload(io, :store) # uploads to :store and :backup
339
+ file.delete # deletes from :store and :backup
340
+ ```
341
+
342
+ * The new [`multi_cache`][multi_cache] plugin has been added for allowing an
343
+ attacher to accept files from additional temporary storages.
344
+
345
+ ```rb
346
+ Shrine.storages = { cache: ..., cache_one: ..., cache_two: ..., store: ... }
347
+
348
+ Shrine.plugin :multi_cache, additional_cache: [:cache_one, :cache_two]
349
+ ```
350
+ ```rb
351
+ photo.image = { "id" => "...", "storage" => "cache", "metadata" => { ... } }
352
+ photo.image.storage_key #=> :cache
353
+ # or
354
+ photo.image = { "id" => "...", "storage" => "cache_one", "metadata" => { ... } }
355
+ photo.image.storage_key #=> :cache_one
356
+ # or
357
+ photo.image = { "id" => "...", "storage" => "cache_two", "metadata" => { ... } }
358
+ photo.image.storage_key #=> :cache_two
359
+ ```
360
+
361
+ * The new [`form_assign`][form_assign] plugin has been added for assigning
362
+ files directly from form params.
363
+
364
+ ```rb
365
+ Shrine.plugin :form_assign
366
+ ```
367
+ ```rb
368
+ attacher = photo.image_attacher
369
+ attacher.form_assign({ "image" => file, "title" => "...", "description" => "..." })
370
+ attacher.file #=> #<Shrine::UploadedFile id="..." storage=:cache ...>
371
+ ```
372
+
373
+ ## Other features
374
+
375
+ * Model file assignment can now be configured to upload directly to permanent
376
+ storage.
377
+
378
+ ```rb
379
+ Shrine.plugin :model, cache: false
380
+ ```
381
+ ```rb
382
+ photo.image = file
383
+ photo.image.storage_key #=> :store (permanent storage)
384
+ ```
385
+
386
+ * New `Shrine.download_response` method has been added to the
387
+ `download_endpoint` plugin for generating file response from the controller.
388
+
389
+ ```rb
390
+ Rails.application.routes.draw do
391
+ get "/attachments" => "files#download"
392
+ end
393
+ ```
394
+ ```rb
395
+ class FilesController < ApplicationController
396
+ def download
397
+ # ... we can now perform things like authentication here ...
398
+ set_rack_response Shrine.download_response(env)
399
+ end
400
+
401
+ private
402
+
403
+ def set_rack_response((status, headers, body))
404
+ self.status = status
405
+ self.headers.merge!(headers)
406
+ self.response_body = body
407
+ end
408
+ end
409
+ ```
410
+
411
+ * The `Attacher#refresh_metadata!` method has been added to `refresh_metadata`
412
+ plugin. It refreshes metadata and writes new attached file data back into the
413
+ data attribute.
414
+
415
+ ```rb
416
+ attacher.file.refresh_metadata!
417
+ attacher.write
418
+ # can now be shortened to
419
+ attacher.refresh_metadata!
420
+ ```
421
+
422
+ * Including a `Shrine::Attachment` module now defines a `.<name>_attacher`
423
+ class method on the target class.
424
+
425
+ ```rb
426
+ class Photo
427
+ include ImageUploader::Attachment(:image)
428
+ end
429
+ ```
430
+ ```rb
431
+ Photo.image_attacher #=> #<ImageUploader::Attacher ...>
432
+ ```
433
+
434
+ * The attachment data serializer is now configurable (by default `JSON`
435
+ standard library is used):
436
+
437
+ ```rb
438
+ require "oj" # https://github.com/ohler55/oj
439
+
440
+ Shrine.plugin :column, serializer: Oj
441
+ ```
442
+
443
+ * It's now possible to pass options to the validate block via the `:validate`
444
+ option:
445
+
446
+ ```rb
447
+ attacher.assign(file, validate: { foo: "bar" })
448
+ ```
449
+ ```rb
450
+ class MyUploader < Shrine
451
+ Attacher.validate do |**options|
452
+ options #=> { foo: "bar" }
453
+ end
454
+ end
455
+ ```
456
+
457
+ * Validation can now be skipped on assignment by passing `validate: false`.
458
+
459
+ ```rb
460
+ attacher.attach(file, validate: false) # skip validation
461
+ ```
462
+
463
+ * Closing the uploaded file can now be prevented by passing `close: false` to
464
+ `Shrine#upload`.
465
+
466
+ * Uploaded file can now be automatically deleted by passing `delete: true` to
467
+ `Shrine#upload`.
468
+
469
+ * New `Attacher#file!` method has been added for retrieving the attached file
470
+ and raising and exception if it doesn't exist.
471
+
472
+ * New `Derivation#opened` method has been added for retrieving an opened
473
+ derivative in `derivation_endpoint` plugin.
474
+
475
+ * New `Storage#delete_prefixed` method has been added for deleting all files
476
+ in specified directory.
477
+
478
+ ```rb
479
+ storage.delete_prefixed("some_directory/")
480
+ ```
481
+
482
+ ## Performance improvements
483
+
484
+ * The attached file is now parsed and loaded from record column only once,
485
+ which can greatly improve performance if the same attached file is being
486
+ accessed multiple times.
487
+
488
+ ```rb
489
+ photo = Photo.find(photo_id)
490
+ photo.image # parses and loads attached file
491
+ photo.image # returns memoized attached file
492
+ ```
493
+
494
+ * The `S3#open` method doesn't perform a `#head_obect` request anymore.
495
+
496
+ * The `derivation_endpoint` plugin doesn't perform a `Storage#exists?` call
497
+ anymore when `:upload` is enabled.
498
+
499
+ * The `download_endpoint` plugin doesn't perform a `Storage#exists?` call
500
+ anymore.
501
+
502
+ ## Other Improvements
503
+
504
+ ### Core improvements
505
+
506
+ * Shrine now works again with MRI 2.3.
507
+
508
+ * The memory storage from the [shrine-memory] gem has been merged into core.
509
+
510
+ ```rb
511
+ # Gemfile
512
+ gem "shrine-memory" # this can be removed
513
+ ```
514
+
515
+ * The `Attacher#assign` method now accepts cached file data as a Hash.
516
+
517
+ ```rb
518
+ photo.image = { "id" => "...", "storage" => "cache", "metadata" => { ... } }
519
+ ````
520
+
521
+ * An `UploadedFile` object can now be initialized with symbol keys.
522
+
523
+ ```rb
524
+ Shrine.uploaded_file(id: "...", storage: :store, metadata: { ... })
525
+ ```
526
+
527
+ * The temporary storage doesn't need to be defined anymore if it's not used.
528
+
529
+ * Any changes to `Shrine.storages` will now be applied to existing `Shrine` and
530
+ `Attacher` instances.
531
+
532
+ * When copying the S3 object to another location, any specified upload options
533
+ will now be applied.
534
+
535
+ * Deprecation of passing unknown options to `FileSystem#open` has been
536
+ reverted. This allows users to continue using `FileSystem` storage in tests
537
+ as a mock storage.
538
+
539
+ * The `Shrine#upload` method now infers file extension from `filename` metadata,
540
+ making possible to use `filename` to specify file extension.
541
+
542
+ ```rb
543
+ file = uploader.upload(StringIO.new("some text"), metadata: { "filename" => "file.txt" })
544
+ file.id #=> "2a2467ee6acbc5cb.txt"
545
+ ```
546
+
547
+ * The `Shrine.opts` hash is now deep-copied on subclassing. This allows plugins
548
+ to freely mutate hashes and arrays in `Shrine.opts`, knowing they won't be
549
+ shared across subclasses.
550
+
551
+ * The `down` dependency has been updated to `~> 5.0`.
552
+
553
+ * The `Shrine::Attachment[]` method has been added as an alternative syntax for
554
+ creating attachment modules.
555
+
556
+ ```rb
557
+ class Photo
558
+ include ImageUploader::Attachment[:image]
559
+ end
560
+ ```
561
+
562
+ ### Plugin improvements
563
+
564
+ * The `activerecord` plugin now works with Active Record 3.
565
+
566
+ * Callback code from `activerecord` and `sequel` plugin has been moved into
567
+ attacher methods, allowing the user to override them.
568
+
569
+ - `Attacher#(activerecord|sequel)_before_save`
570
+ - `Attacher#(activerecord|sequel)_after_save`
571
+ - `Attacher#(activerecord|sequel)_after_destroy`
572
+
573
+ * The `url_options` plugin now allows you to override URL options by deleting
574
+ them.
575
+
576
+ ```rb
577
+ uploaded_file.url(response_content_disposition: "attachment")
578
+ ```
579
+ ```rb
580
+ plugin :url_options, store: -> (io, options) {
581
+ disposition = options.delete(:response_content_disposition, "inline")
582
+
583
+ {
584
+ response_content_disposition: ContentDisposition.format(
585
+ disposition: disposition,
586
+ filename: io.original_filename,
587
+ )
588
+ }
589
+ }
590
+ ```
591
+
592
+ * The `:upload_options` hash passed to the uploader is now merged with any
593
+ options defined with the `upload_options` plugin.
594
+
595
+ * The `default_storage` now evaluates the storage block in context of the
596
+ `Attacher` instance.
597
+
598
+ * New `Attacher.default_cache` and `Attacher.default_store` methods have been
599
+ added to the `default_storage` plugin for declaratively setting default
600
+ storage.
601
+
602
+ ```rb
603
+ Attacher.default_cache { ... }
604
+ Attacher.default_store { ... }
605
+ ```
606
+
607
+ * The `derivation_endpoint` plugin now handles string derivation names.
608
+
609
+ * The `Derivation#upload` method from `derivation_endpoint` plugin now accepts
610
+ additional uploader options
611
+
612
+ * The `Derivation#upload` method from `derivation_endpoint` plugin now accepts
613
+ any IO-like object.
614
+
615
+ * The `derivation_endpoint` plugin doesn't re-open `File` objects returned in
616
+ derivation block anymore.
617
+
618
+ * The `instrumentation` plugin now instruments `UploadedFile#open` calls as a
619
+ new `open.shrine` event. `UploadedFile#download` is still instrumented as
620
+ `download.shrine`.
621
+
622
+ * The `upload.shrine` event now has `:metadata` on the top level in
623
+ `instrumentation` plugin.
624
+
625
+ * The width & height validators in `store_dimensions` plugin don't require
626
+ `UploadedFile#width` and `UploadedFile#height` methods to be defined anymore,
627
+ only that the corresponding metadata exists.
628
+
629
+ * Any options passed to `Attacher#attach_cached` are now forwarded to metadata
630
+ extraction when `restore_cached_data` plugin is loaded.
631
+
632
+ * The `infer_extension` plugin now works correctly with `pretty_location`
633
+ plugin when `pretty_location` was loaded after `infer_extension`.
634
+
635
+ * The `pretty_location` plugin now accepts `:class_underscore` option for
636
+ underscoring class names.
637
+
638
+ ```rb
639
+ plugin :pretty_location
640
+ # "blogpost/aa357797-5845-451b-8662-08eecdc9f762/image/493g82jf23.jpg"
641
+
642
+ plugin :pretty_location, class_underscore: :true
643
+ # "blog_post/aa357797-5845-451b-8662-08eecdc9f762/image/493g82jf23.jpg"
644
+ ```
645
+
646
+ * You can now load multiple persistence plugins simulatenously, and the correct
647
+ one will be activated during persistence.
648
+
649
+ ## Backwards compatibility
650
+
651
+ ### Plugin deprecation and removal
652
+
653
+ * The `backgrounding` plugin has been rewritten and has a new API. While the
654
+ new API works in a similar way, no backwards compatibility has been kept with
655
+ the previous API.
656
+
657
+ * The `versions`, `processing`, `recache`, and `delete_raw` plugins have been
658
+ deprecated in favor of the new `derivatives` plugin.
659
+
660
+ * The `module_include` plugin has been deprecated over overriding core classes
661
+ directly.
662
+
663
+ * The `hooks`, `parallelize`, `parsed_json`, and `delete_promoted` plugins have
664
+ been removed.
665
+
666
+ * The deprecated `copy`, `backup`, `multi_delete`, `moving`, `logging`,
667
+ `direct_upload` `background_helpers`, and `migration_helpers` plugins have
668
+ been removed.
669
+
670
+ * The `default_url_options` plugin has been renamed to `url_options`.
671
+
672
+ ### Attacher API
673
+
674
+ * The `Attacher.new` method now only accepts a hash of options, use
675
+ `Attacher.from_model` for initializing from a model.
676
+
677
+ * If you're changing the attachment data column directly, you'll now need to
678
+ call `Attacher#reload` to make the attacher reflect those changes.
679
+
680
+ * The `Attacher#promote` method now only saves the promoted file in memory,
681
+ it doesn't persist the changes.
682
+
683
+ * The `Attacher#_set` and `Attacher#set` methods have been renamed to
684
+ `Attacher#set` and `Attacher#changed`.
685
+
686
+ * The `Attacher#cache!`, `Attacher#store!`, and `Attacher#delete!` methods have
687
+ been removed.
688
+
689
+ * The `Attacher#_promote` and `Attacher#_delete` methods have been removed.
690
+
691
+ * The `Attacher#swap`, `Attacher#update`, `Attacher#read`, `Attacher#write`,
692
+ `Attacher#convert_to_data`, `Attacher#convert_before_write`, and
693
+ `Attache#convert_after_read` methods have been removed.
694
+
695
+ * The `Attacher.validate`, `Attacher#validate` and `Attacher#errors` methods
696
+ have been extracted into the new `validation` plugin.
697
+
698
+ * The `Attacher#data_attribute` method has been renamed to `Attacher#attribute`.
699
+
700
+ * The `Attacher#replace` method has been renamed to
701
+ `Attacher#destroy_previous`.
702
+
703
+ * The `Attacher#assign` method now raises an exception when non-cached uploaded
704
+ file is assigned.
705
+
706
+ * The `Attacher#attached?` method now returns whether a file is attached,
707
+ regardless of whether it was changed or not.
708
+
709
+ ### Attachment API
710
+
711
+ * The `Shrine::Attachment` module doesn't define any instance methods by itself
712
+ anymore. This has been moved into `entity` and `model` plugins.
713
+
714
+ ### Uploader API
715
+
716
+ * The `:phase` option has been removed.
717
+
718
+ * The `Shrine#process` and `Shrine#processed` methods have been removed.
719
+
720
+ * The `Shrine#store`, `Shrine#_store`, `Shrine#put`, and `Shrine#copy` methods
721
+ have been removed.
722
+
723
+ * The `Shrine#delete`, `Shrine#_delete`, and `Shrine#remove` methods have been
724
+ removed.
725
+
726
+ * The `Shrine#uploaded?` method has been removed.
727
+
728
+ * The `Shrine.uploaded_file` method doesn't yield files anymore by default.
729
+
730
+ * The options for `Shrine#upload` and `Shrine#extract_metadata` are now
731
+ required to have symbol keys.
732
+
733
+ * The `Shrine.uploaded_file` method now raises `ArgumentError` on invalid
734
+ arguments.
735
+
736
+ * The `Shrine#upload` method doesn't rescue exceptions that happen in
737
+ `IO#close` anymore.
738
+
739
+ * The deprecated `Shrine::IO_METHODS` constant has been removed.
740
+
741
+ ### Uploaded File API
742
+
743
+ * The `UploadedFile` method now extracts data from the given hash on
744
+ initialization, it doesn't store the whole hash anymore. This means the
745
+ following potential code won't work anymore:
746
+
747
+ ```rb
748
+ uploaded_file.id #=> "foo"
749
+ uploaded_file.data["id"] = "bar"
750
+ uploaded_file.id #=> "foo"
751
+ ```
752
+
753
+ * The `UploadedFile#storage_key` method now returns a Symbol instead of a
754
+ String.
755
+
756
+ ```rb
757
+ # previous behaviour
758
+ uploaded_file.storage_key #=> "store"
759
+
760
+ # new behaviour
761
+ uploaded_file.storage_key #=> :store
762
+ ```
763
+
764
+ * The `UploadedFile#==` method now requires both uploaded file objects to be
765
+ of the same class.
766
+
767
+ ### Storage API
768
+
769
+ * The `Storage#open` method is now required to accept additional options.
770
+
771
+ ```rb
772
+ # this won't work anymore
773
+ def open(id)
774
+ # ...
775
+ end
776
+
777
+ # this is now required
778
+ def open(id, **options)
779
+ # ...
780
+ end
781
+ ```
782
+
783
+ * The `Storage#open` method is now required to raise `Shrine::FileNotFound`
784
+ exception when the file is missing.
785
+
786
+ ### S3 API
787
+
788
+ * The support for `aws-sdk` 2.x and `aws-sdk-s3` < 1.14 has been removed.
789
+
790
+ * `S3#open` now raises `Shrine::FileNotFound` exception when S3 object doesn't
791
+ exist on the bucket.
792
+
793
+ * The `S3#upload` method will now override any S3 object data when copying
794
+ from another S3 object. If you were relying on S3 object data being inherited
795
+ on copy, you will need to update your code.
796
+
797
+ * The `:host` option in `S3#initialize` has been removed.
798
+
799
+ * The `:download` option in `S3#url` has been removed.
800
+
801
+ * Specifying `:multipart_threshold` as an integer is not supported anymore.
802
+
803
+ * Non URI-escaped `:content_disposition` and `:response_content_disposition`
804
+ values are not supported anymore.
805
+
806
+ * The `S3#presign` method now returns a hash instead of an object for default
807
+ POST method.
808
+
809
+ * The deprecated `S3#stream`, `S3#download`, and `S3#s3` methods have been
810
+ removed.
811
+
812
+ * The `S3#open` method doesn't include an `:object` in `Down::ChunkedIO#data`
813
+ anymore.
814
+
815
+ ### FileSystem API
816
+
817
+ * `FileSystem#open` now raises `Shrine::FileNotFound` exception when file does
818
+ not exist on the filesystem.
819
+
820
+ * The deprecated `:host` option in `FileSystem#initialize` has been removed.
821
+
822
+ * The deprecated `:older_than` option in `FileSystem#clear!` has been removed.
823
+
824
+ * The deprecated `FileSystem#download` method has been removed.
825
+
826
+ * The `FileSystem#movable?` and `FileSystem#move` methods have been made
827
+ private.
828
+
829
+ * The `FileSystem#open` method doesn't accept a block anymore.
830
+
831
+ ### Plugins API
832
+
833
+ * `remote_url`
834
+
835
+ - A custom downloader is now required to raise
836
+ `Shrine::Plugins::RemoteUrl::DownloadError` in order for the exception to
837
+ be converted into a validation error. `Down::NotFound` and `Down::TooLarge`
838
+ exceptions are converted by default. All other exceptions are propagated.
839
+
840
+ * `upload_endpoint`
841
+
842
+ - The deprecated `Shrine::Plugins::UploadEndpoint::App` constant has been
843
+ removed.
844
+
845
+ - The `:request` option holding the `Rack::Request` object isn't passed to
846
+ the uploader anymore.
847
+
848
+ * `presign_endpoint`
849
+
850
+ - `Storage#presign` results that cannot coerce themselves into a Hash are not
851
+ supported anymore.
852
+
853
+ - The deprecated `Shrine::Plugins::PresignEndpoint::App` constant has been
854
+ removed.
855
+
856
+ * `derivation_endpoint`
857
+
858
+ - The derivation block is now evaluated in context of a `Shrine::Derivation`
859
+ instance.
860
+
861
+ - The derivation block can now return only `File` and `Tempfile` objects.
862
+
863
+ - The `:download_errors` option has been removed, as it's now obsolete.
864
+
865
+ - The `:include_uploaded_file` option has been removed, as it's now obsolete.
866
+
867
+ - The source `UploadedFile` is not passed to the derivation block anymore
868
+ on `download: false`.
869
+
870
+ - The `Derivation#upload` method now closes the uploaded file.
871
+
872
+ - The `derivation.upload` instrumentation event payload now includes only
873
+ `:derivation` key.
874
+
875
+ * `download_endpoint`
876
+
877
+ - Support for legacy `/:storage/:id` URLs has been dropped.
878
+
879
+ - The `:storages` plugin option has been removed.
880
+
881
+ - The `Shrine::Plugins::DownloadEndpoint::App` constant has been removed.
882
+
883
+ * `data_uri`
884
+
885
+ - The deprecated `:filename` plugin option has been removed.
886
+
887
+ - The deprecated `Shrine::Plugins::DataUri::DataFile` constant has been
888
+ removed.
889
+
890
+ * `rack_file`
891
+
892
+ - The deprecated `Shrine::Plugins::RackFile::UploadedFile` constant has been
893
+ removed.
894
+
895
+ - Passing a Rack uploaded file hash to `Shrine#upload` is not supported
896
+ anymore.
897
+
898
+ * `cached_attachment_data`
899
+
900
+ - The `#<name>_cached_data=` model method has been removed.
901
+
902
+ - The `Attacher#read_cached` method has been renamed to
903
+ `Attacher#cached_data`.
904
+
905
+ * `default_url`
906
+
907
+ - Passing a block when loading the plugin is not supported anymore.
908
+
909
+ * `determine_mime_type`
910
+
911
+ - The deprecated `:default` analyzer alias has been removed.
912
+
913
+ - The private `Shrine#mime_type_analyzers` method has been removed.
914
+
915
+ * `store_dimensions`
916
+
917
+ - Failing to extract dimensions now prints out a warning by default.
918
+
919
+ - The private `Shrine#extract_dimensions` and `Shrine#dimensions_analyzers`
920
+ methods have been removed.
921
+
922
+ * `infer_extension`
923
+
924
+ - The `:mini_mime` inferrer is now the default inferrer, which requires the
925
+ `mini_mime` gem.
926
+
927
+ - The private `Shrine#infer_extension` method has been removed.
928
+
929
+ * `validation_helpers`
930
+
931
+ - The width & height validators will now raise an exception if `width` or
932
+ `height` metadata is missing.
933
+
934
+ - Support for regexes has been dropped for MIME type and extension
935
+ validators.
936
+
937
+ * `versions`
938
+
939
+ - The deprecated `:version_names`, `Shrine.version_names` and
940
+ `Shrine.version?` have been removed from `versions` plugin.
941
+
942
+ * `keep_files`
943
+
944
+ - The plugin will now always prevent deletion of both replaced and destroyed
945
+ attachments. The `:replaced` and `:destroyed` options don't have effect
946
+ anymore.
947
+
948
+ * `dynamic_storage`
949
+
950
+ - The `Shrine.dynamic_storages` method has been removed.
951
+
952
+ * `instrumentation`
953
+
954
+ - The `:location`, `:upload_options`, and `:metadata` keys have been removed
955
+ from `:options` in `upload.shrine` event payload.
956
+
957
+ - The `:metadata` key has been removed from `metadata.shrine` event payload.
958
+
959
+ * `default_storage`
960
+
961
+ - Passing `record` & `name` arguments to the storage block has been
962
+ deprecated over evaluating the block in context of the attacher instance.
963
+
964
+ * `sequel`
965
+
966
+ - The `:callbacks` plugin option has been renamed to `:hooks`.
967
+
968
+ [derivatives]: https://shrinerb.com/docs/plugins/derivatives
969
+ [versions]: https://shrinerb.com/docs/plugins/versions
970
+ [backgrounding]: https://shrinerb.com/docs/plugins/backgrounding
971
+ [shrine-memory]: https://github.com/shrinerb/shrine-memory
972
+ [atomic_helpers]: https://shrinerb.com/docs/plugins/atomic_helpers
973
+ [attacher]: https://shrinerb.com/docs/attacher
974
+ [column]: https://shrinerb.com/docs/plugins/column
975
+ [entity]: https://shrinerb.com/docs/plugins/entity
976
+ [model]: https://shrinerb.com/docs/plugins/model
977
+ [persistence]: https://shrinerb.com/docs/plugins/persistence
978
+ [mirroring]: https://shrinerb.com/docs/plugins/mirroring
979
+ [form_assign]: https://shrinerb.com/docs/plugins/form_assign
980
+ [multi_cache]: https://shrinerb.com/docs/plugins/multi_cache
981
+ [Upgrading to Shrine 3.x]: https://shrinerb.com/docs/upgrading-to-3