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,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