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,308 @@
1
+ ---
2
+ id: changing-derivatives
3
+ title: Managing Derivatives
4
+ ---
5
+
6
+ This guide shows how to add, create, update, and remove [derivatives] for an
7
+ app in production already handling file attachments, with zero downtime.
8
+
9
+ Let's assume we have a `Photo` model with an `image` file attachment. The
10
+ examples will be showing image thumbnails, but the advice applies to any kind
11
+ of derivatives.
12
+
13
+ ```rb
14
+ Shrine.plugin :derivatives
15
+ Shrine.plugin :activerecord
16
+ ```
17
+ ```rb
18
+ class ImageUploader < Shrine
19
+ # ...
20
+ end
21
+ ```
22
+ ```rb
23
+ class Photo < ActiveRecord::Base
24
+ include ImageUploader::Attachment(:image)
25
+ end
26
+ ```
27
+
28
+ ## Adding derivatives
29
+
30
+ *Scenario: Your app is currently working only with original files, and you want
31
+ to introduce derivatives.*
32
+
33
+ You'll first want to start creating the derivatives in production, without yet
34
+ generating URLs for them (because existing attachments won't yet have
35
+ derivatives generated). Let's assume you're generating image thumbnails:
36
+
37
+ ```rb
38
+ # Gemfile
39
+ gem "image_processing", "~> 1.8"
40
+ ```
41
+ ```rb
42
+ require "image_processing/mini_magick"
43
+
44
+ class ImageUploader < Shrine
45
+ Attacher.derivatives do |original|
46
+ magick = ImageProcessing::MiniMagick.source(original)
47
+
48
+ # generate the thumbnails you want here
49
+ {
50
+ small: magick.resize_to_limit!(300, 300),
51
+ medium: magick.resize_to_limit!(500, 500),
52
+ large: magick.resize_to_limit!(800, 800),
53
+ }
54
+ end
55
+ end
56
+ ```
57
+ ```rb
58
+ photo = Photo.new(photo_params)
59
+ photo.image_derivatives! # generate derivatives
60
+ photo.save
61
+ ```
62
+
63
+ Once we've deployed this to production, we can run the following script to
64
+ generate derivatives for all existing attachments in production. It fetches the
65
+ records in batches, downloads attachments on permanent storage, creates
66
+ derivatives, and persists the changes.
67
+
68
+ ```rb
69
+ Photo.find_each do |photo|
70
+ attacher = photo.image_attacher
71
+
72
+ next unless attacher.stored?
73
+
74
+ attacher.create_derivatives
75
+
76
+ begin
77
+ attacher.atomic_persist # persist changes if attachment has not changed in the meantime
78
+ rescue Shrine::AttachmentChanged, # attachment has changed
79
+ ActiveRecord::RecordNotFound # record has been deleted
80
+ attacher.delete_derivatives # delete now orphaned derivatives
81
+ end
82
+ end
83
+ ```
84
+
85
+ Now all attachments should have correctly generated derivatives. You can update
86
+ the attachment URLs to use derivatives as needed.
87
+
88
+ ## Reprocessing all derivatives
89
+
90
+ *Scenario: The processing logic has changed for all or most derivatives, and
91
+ now you want to reprocess them for existing attachments.*
92
+
93
+ Let's assume we've made the following change and have deployed it to production:
94
+
95
+ ```diff
96
+ Attacher.derivatives do |original|
97
+ magick = ImageProcessing::MiniMagick.source(original)
98
+ + .saver(quality: 85)
99
+
100
+ {
101
+ small: magick.resize_to_limit!(300, 300),
102
+ medium: magick.resize_to_limit!(500, 500),
103
+ large: magick.resize_to_limit!(800, 800),
104
+ }
105
+ end
106
+ ```
107
+
108
+ We can now run the following script to reprocess derivatives for all existing
109
+ records. It fetches the records in batches, downloads attachments on permanent
110
+ storage, reprocesses new derivatives, persists the changes, and deletes old
111
+ derivatives.
112
+
113
+ ```rb
114
+ Photo.find_each do |photo|
115
+ attacher = photo.image_attacher
116
+
117
+ next unless attacher.stored?
118
+
119
+ old_derivatives = attacher.derivatives
120
+
121
+ attacher.set_derivatives({}) # clear derivatives
122
+ attacher.create_derivatives # reprocess derivatives
123
+
124
+ begin
125
+ attacher.atomic_persist # persist changes if attachment has not changed in the meantime
126
+ attacher.delete_derivatives(old_derivatives) # delete old derivatives
127
+ rescue Shrine::AttachmentChanged, # attachment has changed
128
+ ActiveRecord::RecordNotFound # record has been deleted
129
+ attacher.delete_derivatives # delete now orphaned derivatives
130
+ end
131
+ end
132
+ ```
133
+
134
+ ## Reprocessing certain derivatives
135
+
136
+ *Scenario: The processing logic has changed for specific derivatives, and now
137
+ you want to reprocess them for existing attachments.*
138
+
139
+ Let's assume we've made a following change and have deployed it to production:
140
+
141
+ ```diff
142
+ Attacher.derivatives do |original|
143
+ magick = ImageProcessing::MiniMagick.source(original)
144
+
145
+ {
146
+ small: magick.resize_to_limit!(300, 300),
147
+ - medium: magick.resize_to_limit!(500, 500),
148
+ + medium: magick.resize_to_limit!(600, 600),
149
+ large: magick.resize_to_limit!(800, 800),
150
+ }
151
+ end
152
+ ```
153
+
154
+ We can now run the following script to reprocess the derivative for all
155
+ existing records. It fetches the records in batches, downloads attachments with
156
+ derivatives, reprocesses the specific derivative, persists the change, and
157
+ deletes old derivative.
158
+
159
+ ```rb
160
+ Photo.find_each do |photo|
161
+ attacher = photo.image_attacher
162
+
163
+ next unless attacher.derivatives.key?(:medium)
164
+
165
+ old_medium = attacher.derivatives[:medium]
166
+ new_medium = attacher.file.download do |original|
167
+ ImageProcessing::MiniMagick
168
+ .source(original)
169
+ .resize_to_limit!(600, 600)
170
+ end
171
+
172
+ attacher.add_derivative(:medium, new_medium)
173
+
174
+ begin
175
+ attacher.atomic_persist # persist changes if attachment has not changed in the meantime
176
+ old_medium.delete # delete old derivative
177
+ rescue Shrine::AttachmentChanged, # attachment has changed
178
+ ActiveRecord::RecordNotFound # record has been deleted
179
+ attacher.derivatives[:medium].delete # delete now orphaned derivative
180
+ end
181
+ end
182
+ ```
183
+
184
+ ## Adding new derivatives
185
+
186
+ *Scenario: A new derivative has been added to the processor, and now
187
+ you want to add it to existing attachments.*
188
+
189
+ Let's assume we've made a following change and have deployed it to production:
190
+
191
+ ```diff
192
+ Attacher.derivatives do |original|
193
+ magick = ImageProcessing::MiniMagick.source(original)
194
+
195
+ {
196
+ + square: magick.resize_to_fill!(150, 150),
197
+ small: magick.resize_to_limit!(300, 300),
198
+ medium: magick.resize_to_limit!(600, 600),
199
+ large: magick.resize_to_limit!(800, 800),
200
+ }
201
+ end
202
+ ```
203
+
204
+ We can now run following script to add the new derivative for all existing
205
+ records. It fetches the records in batches, downloads attachments on permanent
206
+ storage, creates the new derivative, and persists the changes.
207
+
208
+ ```rb
209
+ Photo.find_each do |photo|
210
+ attacher = photo.image_attacher
211
+
212
+ next unless attacher.stored?
213
+
214
+ square = attacher.file.download do |original|
215
+ ImageProcessing::MiniMagick
216
+ .source(original)
217
+ .resize_to_fill!(150, 150)
218
+ end
219
+
220
+ attacher.add_derivative(:square, square)
221
+
222
+ begin
223
+ attacher.atomic_persist # persist changes if attachment has not changed in the meantime
224
+ rescue Shrine::AttachmentChanged, # attachment has changed
225
+ ActiveRecord::RecordNotFound # record has been deleted
226
+ attacher.derivatives[:square].delete # delete now orphaned derivative
227
+ end
228
+ end
229
+ ```
230
+
231
+ Now all attachments should have the new derivative and you can start generating
232
+ URLs for it.
233
+
234
+ ## Removing derivatives
235
+
236
+ *Scenario: A derivative isn't being used anymore, so we want to delete it for
237
+ existing attachments.*
238
+
239
+ Let's assume we've made the following change and have deployed it to production:
240
+
241
+ ```diff
242
+ Attacher.derivatives do |original|
243
+ magick = ImageProcessing::MiniMagick.source(original)
244
+
245
+ {
246
+ - square: magick.resize_to_fill!(150, 150),
247
+ small: magick.resize_to_limit!(300, 300),
248
+ medium: magick.resize_to_limit!(600, 600),
249
+ large: magick.resize_to_limit!(800, 800),
250
+ }
251
+ end
252
+ ```
253
+
254
+ We can now run following script to remove the unused derivative for all
255
+ existing record. It fetches the records in batches, removes and deletes the
256
+ unused derivative, and persists the changes.
257
+
258
+ ```rb
259
+ Photo.find_each do |photo|
260
+ attacher = photo.image_attacher
261
+
262
+ next unless attacher.derivatives.key?(:square)
263
+
264
+ attacher.remove_derivative(:square, delete: true)
265
+
266
+ begin
267
+ attacher.atomic_persist # persist changes if attachment has not changed in the meantime
268
+ rescue Shrine::AttachmentChanged, # attachment has changed
269
+ ActiveRecord::RecordNotFound # record has been deleted
270
+ end
271
+ end
272
+ ```
273
+
274
+ ## Backgrounding
275
+
276
+ For faster migration, we can also delay any of the operations above into a
277
+ background job:
278
+
279
+ ```rb
280
+ Photo.find_each do |photo|
281
+ attacher = photo.image_attacher
282
+
283
+ next unless attacher.stored?
284
+
285
+ MakeChangeJob.perform_async(
286
+ attacher.class.name,
287
+ attacher.record.class.name,
288
+ attacher.record.id,
289
+ attacher.name,
290
+ attacher.file_data,
291
+ )
292
+ end
293
+ ```
294
+ ```rb
295
+ class MakeChangeJob
296
+ include Sidekiq::Worker
297
+
298
+ def perform(attacher_class, record_class, record_id, name, file_data)
299
+ attacher_class = Object.const_get(attacher_class)
300
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
301
+
302
+ attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
303
+ # ... make our change ...
304
+ end
305
+ end
306
+ ```
307
+
308
+ [derivatives]: https://shrinerb.com/docs/plugins/derivatives
@@ -1,31 +1,113 @@
1
- # Migrating to Different Location
1
+ ---
2
+ id: changing-location
3
+ title: Migrating File Locations
4
+ ---
2
5
 
3
- You have a production app with already uploaded attachments. However, you've
4
- realized that the existing store folder structure for attachments isn't working
5
- for you.
6
+ This guide shows how to migrate the location of uploaded files on the same
7
+ storage in production, with zero downtime.
6
8
 
7
- The first step is to change the location, by overriding `#generate_location` or
8
- with the pretty_location plugin, and deploy that change. This will make any new
9
- files upload to the desired location, attachments on old locations will still
10
- continue to work normally.
11
-
12
- The next step is to run a script that will move old files to new locations. The
13
- easiest way to do that is to reupload them and delete them. Shrine has a method
14
- exactly for that, `Attacher#promote`, which also handles the situation when
15
- someone attaches a new file during "moving" (since we're running this script on
16
- live production).
9
+ Let's assume we have a `Photo` model with an `image` file attachment:
17
10
 
18
11
  ```rb
19
- Shrine.plugin :delete_promoted
12
+ Shrine.plugin :activerecord
13
+ ```
14
+ ```rb
15
+ class ImageUploader < Shrine
16
+ # ...
17
+ end
18
+ ```
19
+ ```rb
20
+ class Photo < ActiveRecord::Base
21
+ include ImageUploader::Attachment(:image)
22
+ end
23
+ ```
24
+
25
+ ## 1. Update the location generation
20
26
 
21
- User.paged_each do |user|
22
- attacher = user.avatar_attacher
23
- attacher.promote(action: :migrate) if attacher.stored?
24
- # use `attacher._promote(action: :migrate)` if you want promoting to be backgrounded
27
+ Since Shrine generates the location only once during upload, it is safe to change
28
+ the `Shrine#generate_location` method. All the existing files will still continue
29
+ to work with the previously stored urls because the files have not been migrated.
30
+
31
+ ```rb
32
+ class ImageUploader < Shrine
33
+ def generate_location(io, **options)
34
+ # change location generation
35
+ end
25
36
  end
26
37
  ```
27
38
 
28
- The `:action` is not mandatory, it's just for better introspection when
29
- monitoring background jobs and logs.
39
+ We can now deploy this change to production so new file uploads will be stored in
40
+ the new location.
41
+
42
+ ## 2. Move existing files
43
+
44
+ To move existing files to new location, run the following script. It fetches
45
+ the photos in batches, downloads the image, and re-uploads it to the new location.
46
+ We only need to migrate the files in `:store` storage need to be migrated as the files
47
+ in `:cache` storage will be uploaded to the new location on promotion.
48
+
49
+ ```rb
50
+ Photo.find_each do |photo|
51
+ attacher = photo.image_attacher
52
+
53
+ next unless attacher.stored? # move only attachments uploaded to permanent storage
54
+
55
+ old_attacher = attacher.dup
56
+ current_file = old_attacher.file
57
+
58
+ attacher.set attacher.upload(attacher.file) # reupload file
59
+ attacher.set_derivatives attacher.upload_derivatives(attacher.derivatives) # reupload derivatives if you have derivatives
60
+
61
+ begin
62
+ attacher.atomic_persist(current_file) # persist changes if attachment has not changed in the meantime
63
+ old_attacher.destroy_attached # delete files on old location
64
+ rescue Shrine::AttachmentChanged, # attachment has changed during reuploading
65
+ ActiveRecord::RecordNotFound # record has been deleted during reuploading
66
+ attacher.destroy_attached # delete now orphaned files
67
+ end
68
+ end
69
+ ```
30
70
 
31
71
  Now all your existing attachments should be happily living on new locations.
72
+
73
+ ### Backgrounding
74
+
75
+ For faster migration, we can also delay moving files into a background job:
76
+
77
+ ```rb
78
+ Photo.find_each do |photo|
79
+ attacher = photo.image_attacher
80
+
81
+ next unless attacher.stored? # move only attachments uploaded to permanent storage
82
+
83
+ MoveFilesJob.perform_async(
84
+ attacher.class.name,
85
+ attacher.record.class.name,
86
+ attacher.record.id,
87
+ attacher.name,
88
+ attacher.file_data,
89
+ )
90
+ end
91
+ ```
92
+ ```rb
93
+ class MoveFilesJob
94
+ include Sidekiq::Worker
95
+
96
+ def perform(attacher_class, record_class, record_id, name, file_data)
97
+ attacher_class = Object.const_get(attacher_class)
98
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
99
+
100
+ attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
101
+ old_attacher = attacher.dup
102
+ current_file = old_attacher.file
103
+
104
+ attacher.set attacher.upload(attacher.file)
105
+ attacher.set_derivatives attacher.upload_derivatives(attacher.derivatives)
106
+
107
+ attacher.atomic_persist(current_file)
108
+ old_attacher.destroy_attached
109
+ rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
110
+ attacher&.destroy_attached
111
+ end
112
+ end
113
+ ```
@@ -0,0 +1,110 @@
1
+ ---
2
+ id: changing-storage
3
+ title: Migrating File Storage
4
+ ---
5
+
6
+ This guides shows how to move file attachments to a different storage in
7
+ production, with zero downtime.
8
+
9
+ Let's assume we have a `Photo` model with an `image` file attachment stored
10
+ in AWS S3 storage:
11
+
12
+ ```rb
13
+ Shrine.storages = {
14
+ cache: Shrine::Storage::S3.new(...),
15
+ store: Shrine::Storage::S3.new(...),
16
+ }
17
+
18
+ Shrine.plugin :activerecord
19
+ ```
20
+ ```rb
21
+ class ImageUploader < Shrine
22
+ # ...
23
+ end
24
+ ```
25
+ ```rb
26
+ class Photo < ActiveRecord::Base
27
+ include ImageUploader::Attachment(:image)
28
+ end
29
+ ```
30
+
31
+ Let's also assume that we're migrating from AWS S3 to Google Cloud Storage, and
32
+ we've added the new storage to `Shrine.storages`:
33
+
34
+ ```rb
35
+ Shrine.storages = {
36
+ ...
37
+ store: Shrine::Storage::S3.new(...),
38
+ gcs: Shrine::Storage::GoogleCloudStorage.new(...),
39
+ }
40
+ ```
41
+
42
+ ## 1. Mirror upload and delete operations
43
+
44
+ The first step is to start mirroring uploads and deletes made on your current
45
+ storage to the new storage. We can do this by loading the `mirroring` plugin:
46
+
47
+ ```rb
48
+ Shrine.plugin :mirroring, mirror: { store: :gcs }
49
+ ```
50
+
51
+ Put the above code in an initializer and deploy it.
52
+
53
+ You can additionally delay the mirroring into a [background job][mirroring
54
+ backgrounding] for better performance.
55
+
56
+ ## 2. Copy the files
57
+
58
+ Next step is to copy all remaining files from current storage into the new
59
+ storage using the following script. It fetches the photos in batches, downloads
60
+ the image, and re-uploads it to the new storage.
61
+
62
+ ```rb
63
+ Photo.find_each do |photo|
64
+ attacher = photo.image_attacher
65
+
66
+ next unless attacher.stored?
67
+
68
+ attacher.file.trigger_mirror_upload
69
+
70
+ # if using derivatives
71
+ attacher.map_derivative(attacher.derivatives) do |_, derivative|
72
+ derivative.trigger_mirror_upload
73
+ end
74
+ end
75
+ ```
76
+
77
+ Now the new storage should have all files the current storage has, and new
78
+ uploads will continue being mirrored to the new storage.
79
+
80
+ ## 3. Update storage
81
+
82
+ Once all the files are copied over to the new storage, everything should be
83
+ ready for us to update the storage in the Shrine configuration. We can keep
84
+ mirroring, in case the change would need to reverted.
85
+
86
+ ```rb
87
+ Shrine.storages = {
88
+ ...
89
+ store: Shrine::Storage::GoogleCloudStorage.new(...),
90
+ s3: Shrine::Storage::S3.new(...),
91
+ }
92
+
93
+ Shrine.plugin :mirroring, mirror: { store: :s3 } # mirror to :s3 storage
94
+ ```
95
+
96
+ ## 4. Remove mirroring
97
+
98
+ Once everything is looking good, we can remove the mirroring:
99
+
100
+ ```diff
101
+ Shrine.storages = {
102
+ ...
103
+ store: Shrine::Storage::GoogleCloudStorage.new(...),
104
+ - s3: Shrine::Storage::S3.new(...),
105
+ }
106
+
107
+ - Shrine.plugin :mirroring, mirror: { store: :s3 } # mirror to :s3 storage
108
+ ```
109
+
110
+ [mirroring backgrounding]: https://shrinerb.com/docs/plugins/mirroring#backgrounding
@@ -0,0 +1,132 @@
1
+ ---
2
+ id: creating-persistence-plugins
3
+ title: Writing a Persistence Plugin
4
+ ---
5
+
6
+ This guide explains some conventions for writing Shrine plugins that integrate
7
+ with persistence libraries such as Active Record, Sequel, ROM and Mongoid. It
8
+ assumes you've read the [Writing a Plugin] guide.
9
+
10
+ Let's say we're writing a plugin for a persistence library called "Raptor":
11
+
12
+ ```rb
13
+ # lib/shrine/plugins/raptor.rb
14
+
15
+ require "raptor"
16
+
17
+ class Shrine
18
+ module Plugins
19
+ module Raptor
20
+ # ...
21
+ end
22
+
23
+ register_plugin(:raptor, Raptor)
24
+ end
25
+ end
26
+ ```
27
+
28
+ ## Attachment
29
+
30
+ If your database library uses the [Active Record pattern], it's recommended to
31
+ load the [`model`][model] plugin as a dependency.
32
+
33
+ ```rb
34
+ module Shrine::Plugins::Raptor
35
+ def self.load_dependencies(uploader, **)
36
+ uploader.plugin :model # for Active Record pattern
37
+ end
38
+ # ...
39
+ end
40
+ ```
41
+
42
+ Otherwise if it uses the [Repository pattern], you can load the
43
+ [`entity`][entity] plugin as a dependency.
44
+
45
+ ```rb
46
+ module Shrine::Plugins::Raptor
47
+ def self.load_dependencies(uploader, **)
48
+ uploader.plugin :entity # for Repository pattern
49
+ end
50
+ # ...
51
+ end
52
+ ```
53
+
54
+ If you want to add library-specific integration when `Shrine::Attachment` is
55
+ included into a model/entity, it's recommended to do this in the `included`
56
+ hook, so that you can perform this logic only for models/entities that belong
57
+ to that persistence library.
58
+
59
+ ```rb
60
+ module Shrine::Plugins::Raptor
61
+ # ...
62
+ module AttachmentMethods
63
+ def included(klass)
64
+ super
65
+
66
+ return unless klass < ::Raptor::Model
67
+
68
+ # library specific integration
69
+ end
70
+ end
71
+ # ...
72
+ end
73
+ ```
74
+
75
+ ## Attacher
76
+
77
+ To help define persistence methods on the `Attacher` according to the
78
+ convention, load the `_persistence` plugin as a dependency:
79
+
80
+ ```rb
81
+ module Shrine::Plugins::Raptor
82
+ def self.load_dependencies(uploader, **)
83
+ # ...
84
+ uploader.plugin :_persistence, plugin: self
85
+ end
86
+ # ...
87
+ end
88
+ ```
89
+
90
+ This will define the following attacher methods:
91
+
92
+ * `Attacher#persist`
93
+ * `Attacher#atomic_persist`
94
+ * `Attacher#atomic_promote`
95
+
96
+ For those methods to work, we'll need to implement the following methods:
97
+
98
+ * `Attacher#<library>_persist`
99
+ * `Attacher#<library>_reload`
100
+ * `Attacher#<library>?`
101
+
102
+ ```rb
103
+ module Shrine::Plugins::Raptor
104
+ # ...
105
+ module AttacherMethods
106
+ # ...
107
+ private
108
+
109
+ def raptor_persist
110
+ # persist attached file to the record
111
+ end
112
+
113
+ def raptor_reload
114
+ # yield reloaded record (see atomic_helpers plugin)
115
+ end
116
+
117
+ def raptor?
118
+ # returns whether current model/entity belongs to Raptor
119
+ end
120
+ end
121
+ end
122
+ ```
123
+
124
+ [Writing a Plugin]: https://shrinerb.com/docs/creating-plugins
125
+ [Active Record pattern]: https://www.martinfowler.com/eaaCatalog/activeRecord.html
126
+ [model]: https://shrinerb.com/docs/plugins/model
127
+ [entity]: https://shrinerb.com/docs/plugins/entity
128
+ [Repository pattern]: https://martinfowler.com/eaaCatalog/repository.html
129
+ [backgrounding]: https://shrinerb.com/docs/plugins/backgrounding
130
+ [atomic_helpers]: https://shrinerb.com/docs/plugins/atomic_helpers
131
+ [activerecord]: /lib/shrine/plugins/activerecord.rb
132
+ [sequel]: /lib/shrine/plugins/sequel.rb