shrine 3.4.0 → 3.10.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +96 -0
  3. data/README.md +11 -11
  4. data/doc/advantages.md +1 -1
  5. data/doc/attacher.md +1 -1
  6. data/doc/carrierwave.md +2 -2
  7. data/doc/changing_derivatives.md +2 -1
  8. data/doc/changing_location.md +19 -6
  9. data/doc/design.md +4 -4
  10. data/doc/external/articles.md +16 -16
  11. data/doc/external/extensions.md +1 -1
  12. data/doc/getting_started.md +73 -30
  13. data/doc/multiple_files.md +57 -22
  14. data/doc/plugins/activerecord.md +91 -0
  15. data/doc/plugins/backgrounding.md +30 -4
  16. data/doc/plugins/data_uri.md +15 -0
  17. data/doc/plugins/derivation_endpoint.md +39 -1
  18. data/doc/plugins/derivatives.md +13 -2
  19. data/doc/plugins/download_endpoint.md +16 -4
  20. data/doc/plugins/dynamic_storage.md +15 -0
  21. data/doc/plugins/fallback_storage.md +90 -0
  22. data/doc/plugins/keep_files.md +6 -4
  23. data/doc/plugins/rack_response.md +9 -0
  24. data/doc/plugins/refresh_metadata.md +20 -0
  25. data/doc/plugins/sequel.md +57 -0
  26. data/doc/plugins/signature.md +8 -6
  27. data/doc/plugins/tempfile.md +14 -6
  28. data/doc/plugins/upload_options.md +12 -0
  29. data/doc/plugins/url_options.md +12 -0
  30. data/doc/plugins/validation_helpers.md +1 -1
  31. data/doc/processing.md +5 -3
  32. data/doc/refile.md +3 -3
  33. data/doc/release_notes/2.1.0.md +1 -1
  34. data/doc/release_notes/3.10.0.md +30 -0
  35. data/doc/release_notes/3.5.0.md +63 -0
  36. data/doc/release_notes/3.6.0.md +23 -0
  37. data/doc/release_notes/3.7.0.md +75 -0
  38. data/doc/release_notes/3.7.1.md +31 -0
  39. data/doc/release_notes/3.8.0.md +37 -0
  40. data/doc/release_notes/3.9.0.md +42 -0
  41. data/doc/retrieving_uploads.md +1 -1
  42. data/doc/storage/s3.md +10 -0
  43. data/doc/testing.md +45 -17
  44. data/doc/upgrading_to_3.md +3 -5
  45. data/lib/shrine/attacher.rb +41 -29
  46. data/lib/shrine/attachment.rb +2 -2
  47. data/lib/shrine/plugins/_persistence.rb +1 -1
  48. data/lib/shrine/plugins/_urlsafe_serialization.rb +4 -4
  49. data/lib/shrine/plugins/activerecord.rb +25 -4
  50. data/lib/shrine/plugins/add_metadata.rb +2 -4
  51. data/lib/shrine/plugins/atomic_helpers.rb +7 -7
  52. data/lib/shrine/plugins/backgrounding.rb +10 -10
  53. data/lib/shrine/plugins/column.rb +7 -5
  54. data/lib/shrine/plugins/data_uri.rb +28 -3
  55. data/lib/shrine/plugins/default_url.rb +4 -4
  56. data/lib/shrine/plugins/delete_raw.rb +2 -2
  57. data/lib/shrine/plugins/derivation_endpoint.rb +65 -59
  58. data/lib/shrine/plugins/derivatives.rb +35 -21
  59. data/lib/shrine/plugins/download_endpoint.rb +72 -11
  60. data/lib/shrine/plugins/entity.rb +15 -9
  61. data/lib/shrine/plugins/fallback_storage.rb +51 -0
  62. data/lib/shrine/plugins/infer_extension.rb +5 -1
  63. data/lib/shrine/plugins/instrumentation.rb +13 -9
  64. data/lib/shrine/plugins/metadata_attributes.rb +1 -1
  65. data/lib/shrine/plugins/mirroring.rb +10 -10
  66. data/lib/shrine/plugins/model.rb +12 -10
  67. data/lib/shrine/plugins/presign_endpoint.rb +13 -10
  68. data/lib/shrine/plugins/pretty_location.rb +2 -2
  69. data/lib/shrine/plugins/processing.rb +3 -3
  70. data/lib/shrine/plugins/rack_file.rb +2 -2
  71. data/lib/shrine/plugins/rack_response.rb +12 -6
  72. data/lib/shrine/plugins/refresh_metadata.rb +6 -6
  73. data/lib/shrine/plugins/remote_url.rb +4 -4
  74. data/lib/shrine/plugins/remove_attachment.rb +2 -0
  75. data/lib/shrine/plugins/remove_invalid.rb +4 -0
  76. data/lib/shrine/plugins/restore_cached_data.rb +3 -3
  77. data/lib/shrine/plugins/sequel.rb +15 -1
  78. data/lib/shrine/plugins/signature.rb +2 -2
  79. data/lib/shrine/plugins/store_dimensions.rb +2 -2
  80. data/lib/shrine/plugins/tempfile.rb +0 -2
  81. data/lib/shrine/plugins/upload_endpoint.rb +7 -5
  82. data/lib/shrine/plugins/upload_options.rb +18 -4
  83. data/lib/shrine/plugins/url_options.rb +17 -3
  84. data/lib/shrine/plugins/validation.rb +8 -8
  85. data/lib/shrine/plugins/validation_helpers.rb +2 -2
  86. data/lib/shrine/plugins/versions.rb +10 -10
  87. data/lib/shrine/plugins.rb +6 -14
  88. data/lib/shrine/storage/file_system.rb +15 -19
  89. data/lib/shrine/storage/linter.rb +8 -8
  90. data/lib/shrine/storage/memory.rb +1 -3
  91. data/lib/shrine/storage/s3.rb +60 -40
  92. data/lib/shrine/uploaded_file.rb +23 -19
  93. data/lib/shrine/version.rb +1 -1
  94. data/lib/shrine.rb +24 -20
  95. data/shrine.gemspec +10 -8
  96. metadata +52 -26
@@ -3,6 +3,9 @@ id: multiple-files
3
3
  title: Multiple Files
4
4
  ---
5
5
 
6
+ import Tabs from '@theme/Tabs';
7
+ import TabItem from '@theme/TabItem';
8
+
6
9
  There are times when you want to allow users to attach multiple files to a
7
10
  single resource, like an album having many photos or a playlist having many
8
11
  songs. Some file attachment libraries provide a special interface for multiple
@@ -67,8 +70,9 @@ files (or attachments) table will be the photos table.
67
70
  Let's create a table for the main resource and attachments, and add a foreign
68
71
  key in the attachment table for the main table:
69
72
 
70
- <!--DOCUSAURUS_CODE_TABS-->
71
- <!--Sequel-->
73
+ <Tabs>
74
+ <TabItem value="sequel" label="Sequel">
75
+
72
76
  ```rb
73
77
  Sequel.migration do
74
78
  change do
@@ -87,7 +91,10 @@ Sequel.migration do
87
91
  end
88
92
  end
89
93
  ```
90
- <!--ActiveRecord-->
94
+
95
+ </TabItem>
96
+ <TabItem value="activerecord" label="Active Record">
97
+
91
98
  ```rb
92
99
  class CreateAlbumsAndPhotos < ActiveRecord::Migration
93
100
  def change
@@ -104,25 +111,33 @@ class CreateAlbumsAndPhotos < ActiveRecord::Migration
104
111
  end
105
112
  end
106
113
  ```
107
- <!--END_DOCUSAURUS_CODE_TABS-->
114
+
115
+ </TabItem>
116
+ </Tabs>
108
117
 
109
118
  In the Photo model, create a Shrine attachment attribute named `image`
110
119
  (`:image` matches the `_data` column prefix above):
111
120
 
112
- <!--DOCUSAURUS_CODE_TABS-->
113
- <!--Sequel-->
121
+ <Tabs>
122
+ <TabItem value="sequel" label="Sequel">
123
+
114
124
  ```rb
115
125
  class Photo < Sequel::Model
116
126
  include ImageUploader::Attachment(:image)
117
127
  end
118
128
  ```
119
- <!--ActiveRecord-->
129
+
130
+ </TabItem>
131
+ <TabItem value="activerecord" label="Active Record">
132
+
120
133
  ```rb
121
134
  class Photo < ActiveRecord::Base
122
135
  include ImageUploader::Attachment(:image)
123
136
  end
124
137
  ```
125
- <!--END_DOCUSAURUS_CODE_TABS-->
138
+
139
+ </TabItem>
140
+ </Tabs>
126
141
 
127
142
  ### 2. Declare nested attributes
128
143
 
@@ -131,8 +146,9 @@ Using nested attributes is the easiest way to implement any dynamic
131
146
  relationship to the photos table, and allow it to directly accept attributes
132
147
  for the associated photo records by enabling nested attributes:
133
148
 
134
- <!--DOCUSAURUS_CODE_TABS-->
135
- <!--Sequel-->
149
+ <Tabs>
150
+ <TabItem value="sequel" label="Sequel">
151
+
136
152
  ```rb
137
153
  class Album < Sequel::Model
138
154
  one_to_many :photos
@@ -142,14 +158,20 @@ class Album < Sequel::Model
142
158
  nested_attributes :photos, destroy: true
143
159
  end
144
160
  ```
145
- <!--ActiveRecord-->
161
+
162
+ </TabItem>
163
+ <TabItem value="activerecord" label="Active Record">
164
+
146
165
  ```rb
147
166
  class Album < ActiveRecord::Base
148
167
  has_many :photos, dependent: :destroy
149
168
  accepts_nested_attributes_for :photos, allow_destroy: true
150
169
  end
151
170
  ```
152
- <!--Mongoid-->
171
+
172
+ </TabItem>
173
+ <TabItem value="mongoid" label="Mongoid">
174
+
153
175
  ```rb
154
176
  class Album
155
177
  include Mongoid::Document
@@ -157,7 +179,9 @@ class Album
157
179
  accepts_nested_attributes_for :photos
158
180
  end
159
181
  ```
160
- <!--END_DOCUSAURUS_CODE_TABS-->
182
+
183
+ </TabItem>
184
+ </Tabs>
161
185
 
162
186
  Documentation on nested attributes:
163
187
 
@@ -174,13 +198,14 @@ already created photos, so that the same form can be used for updating the
174
198
  album/photos as well (they will be submitted under the
175
199
  `album[photos_attributes]` parameter).
176
200
 
177
- <!--DOCUSAURUS_CODE_TABS-->
178
- <!--Rails form builder-->
201
+ <Tabs>
202
+ <TabItem value="rails" label="Rails form builder">
203
+
179
204
  ```rb
180
205
  form_for @album, html: { enctype: "multipart/form-data" } do |f|
181
206
  f.text_field :title
182
207
  f.fields_for :photos do |p| # adds new `album[photos_attributes]` parameter
183
- p.hidden_field :image, value: p.object.cached_image_data
208
+ p.hidden_field :image, value: p.object.cached_image_data, id: nil
184
209
  p.file_field :image
185
210
  p.check_box :_destroy unless p.object.new_record?
186
211
  end
@@ -188,7 +213,10 @@ form_for @album, html: { enctype: "multipart/form-data" } do |f|
188
213
  f.submit "Create"
189
214
  end
190
215
  ```
191
- <!--Forme-->
216
+
217
+ </TabItem>
218
+ <TabItem value="forme" label="Forme">
219
+
192
220
  ```rb
193
221
  form @album, action: "/photos", enctype: "multipart/form-data" do |f|
194
222
  f.input :title
@@ -201,7 +229,9 @@ form @album, action: "/photos", enctype: "multipart/form-data" do |f|
201
229
  f.button "Create"
202
230
  end
203
231
  ```
204
- <!--END_DOCUSAURUS_CODE_TABS-->
232
+
233
+ </TabItem>
234
+ </Tabs>
205
235
 
206
236
  In your controller you should still be able to assign all the attributes to the
207
237
  album, just remember to whitelist the new parameter for the nested attributes,
@@ -286,21 +316,26 @@ class ImageUploader < Shrine
286
316
  end
287
317
  end
288
318
  ```
289
- <!--DOCUSAURUS_CODE_TABS-->
290
- <!--Sequel-->
319
+ <Tabs>
320
+ <TabItem value="sequel" label="Sequel">
321
+
291
322
  ```rb
292
323
  class Album < Sequel::Model
293
324
  # ... (nested_attributes already enables validating associated photos) ...
294
325
  end
295
326
  ```
296
- <!--ActiveRecord-->
327
+
328
+ </TabItem>
329
+ <TabItem value="activerecord" label="Active Record">
330
+
297
331
  ```rb
298
332
  class Album < ActiveRecord::Base
299
333
  # ...
300
334
  validates_associated :photos
301
335
  end
302
336
  ```
303
- <!--END_DOCUSAURUS_CODE_TABS-->
337
+ </TabItem>
338
+ </Tabs>
304
339
 
305
340
  Note that by default only metadata set on the client side will be available for
306
341
  validations. Shrine will not automatically run metadata extraction for directly
@@ -80,6 +80,62 @@ Active Record currently has a [bug with transaction callbacks], so if you have
80
80
  any "after commit" callbacks, make sure to include Shrine's attachment module
81
81
  *after* they have all been defined.
82
82
 
83
+ #### Duplicating records
84
+
85
+ Since a record being created can't yet have a confirmed attachment of its own
86
+ to safely replace, Shrine never deletes the previous file when the attachment
87
+ changes as part of *creating* a record, only when *updating* one. This
88
+ matters most when duplicating a record: `#dup` performs a shallow copy, so a
89
+ duplicated record initially points to the *same* underlying file as the
90
+ original (they'll have the same attached file data), and replacing the
91
+ attachment on the duplicate before it's ever saved won't affect the original:
92
+
93
+ ```rb
94
+ photo = Photo.create(image: file)
95
+ photo2 = photo.dup
96
+
97
+ photo2.update!(image: new_file) # replaces the attachment on `photo2`
98
+ photo.image.exists? #=> true (not affected)
99
+ ```
100
+
101
+ Once `photo2` has been saved, replacing its attachment again behaves
102
+ normally (the previously attached file is deleted):
103
+
104
+ ```rb
105
+ previous_image = photo2.image
106
+ photo2.update!(image: another_file)
107
+ previous_image.exists? #=> false
108
+ ```
109
+
110
+ Keep in mind this only protects against *replacing* the attachment on create.
111
+ As long as `photo` and `photo2` continue to reference the same underlying
112
+ file (i.e. `photo2` is saved without ever changing its attachment), destroying
113
+ either record will still delete the file the other one references, since
114
+ Shrine has no way of knowing the file is shared:
115
+
116
+ ```rb
117
+ photo2.save! # still references the same file as `photo`
118
+
119
+ photo2.destroy
120
+ photo.image.exists? #=> false
121
+ ```
122
+
123
+ If you want the duplicated record to have its own independent copy of the
124
+ file from the start, so that destroying either record is also safe, upload a
125
+ new copy explicitly after duplicating:
126
+
127
+ ```rb
128
+ photo = Photo.create(image: file)
129
+ photo2 = photo.dup
130
+
131
+ photo2.image_attacher.set(nil)
132
+ photo2.image_attacher.attach(photo.image, storage: photo.image.storage_key)
133
+ photo2.save!
134
+
135
+ photo2.destroy # no longer affects `photo`
136
+ photo.image.exists? #=> true
137
+ ```
138
+
83
139
  #### Overriding callbacks
84
140
 
85
141
  You can override any of the following attacher methods to modify callback
@@ -177,6 +233,39 @@ model errors, you can set `:validations` to `false`:
177
233
  plugin :activerecord, validations: false
178
234
  ```
179
235
 
236
+ ### JSON columns
237
+
238
+ When the data attribute is backed by a `json` or `jsonb` database column,
239
+ Active Record already serializes hashes into JSON, so Shrine skips its own
240
+ serialization to avoid double encoding.
241
+
242
+ ```rb
243
+ class Photo < ActiveRecord::Base # `image_data` is a jsonb column
244
+ include ImageUploader::Attachment(:image)
245
+ end
246
+ ```
247
+ ```rb
248
+ photo.image = file
249
+ photo.image_data #=> { "id" => "bc2e13.jpg", "storage" => "cache", "metadata" => { ... } }
250
+ ```
251
+
252
+ By default this detection only looks at real database columns. If you declare
253
+ the data attribute type via the [Attributes API] (e.g. on top of a `text`
254
+ column), enable the `:attribute_types` option so it's recognized as well:
255
+
256
+ ```rb
257
+ plugin :activerecord, attribute_types: true
258
+ ```
259
+ ```rb
260
+ class Photo < ActiveRecord::Base # `image_data` is a text column
261
+ include ImageUploader::Attachment(:image)
262
+
263
+ attribute :image_data, :json
264
+ end
265
+ ```
266
+
267
+ This option defaults to `false` for backwards compatibility.
268
+
180
269
  ## Attacher
181
270
 
182
271
  You can also use `Shrine::Attacher` directly (with or without the
@@ -217,8 +306,10 @@ See [persistence] docs for more details.
217
306
 
218
307
  [activerecord]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/activerecord.rb
219
308
  [Active Record]: https://guides.rubyonrails.org/active_record_basics.html
309
+ [Attributes API]: https://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute
220
310
  [model]: https://shrinerb.com/docs/plugins/model
221
311
  [callbacks]: https://guides.rubyonrails.org/active_record_callbacks.html
222
312
  [bug with transaction callbacks]: https://github.com/rails/rails/issues/14493
223
313
  [validation]: https://shrinerb.com/docs/plugins/validation
224
314
  [persistence]: https://shrinerb.com/docs/plugins/persistence
315
+ [Replacing]: https://shrinerb.com/docs/attacher#replacing
@@ -46,7 +46,7 @@ Then, in your initializer, you can configure all uploaders to use these jobs:
46
46
 
47
47
  ```rb
48
48
  Shrine::Attacher.promote_block do
49
- PromoteJob.perform_async(self.class.name, record.class.name, record.id, name, file_data)
49
+ PromoteJob.perform_async(self.class.name, record.class.name, record.id, name.to_s, file_data)
50
50
  end
51
51
  Shrine::Attacher.destroy_block do
52
52
  DestroyJob.perform_async(self.class.name, data)
@@ -58,7 +58,7 @@ Alternatively, you can setup backgrounding only for specific uploaders:
58
58
  ```rb
59
59
  class MyUploader < Shrine
60
60
  Attacher.promote_block do
61
- PromoteJob.perform_async(self.class.name, record.class.name, record.id, name, file_data)
61
+ PromoteJob.perform_async(self.class.name, record.class.name, record.id, name.to_s, file_data)
62
62
  end
63
63
  Attacher.destroy_block do
64
64
  DestroyJob.perform_async(self.class.name, data)
@@ -121,7 +121,7 @@ Shrine::Attacher.promote_block do |attacher|
121
121
  attacher.class.name,
122
122
  attacher.record.class.name,
123
123
  attacher.record.id,
124
- attacher.name,
124
+ attacher.name.to_s,
125
125
  attacher.file_data,
126
126
  )
127
127
  end
@@ -143,7 +143,7 @@ photo.image_attacher.promote_block do |attacher|
143
143
  attacher.class.name,
144
144
  attacher.record.class.name,
145
145
  attacher.record.id,
146
- attacher.name,
146
+ attacher.name.to_s,
147
147
  attacher.file_data,
148
148
  current_user.id, # pass arguments known at the controller level
149
149
  )
@@ -203,6 +203,32 @@ class MyUploader < Shrine
203
203
  end
204
204
  ```
205
205
 
206
+ ## Testing
207
+
208
+ Since promotion happens in a background job, it won't have run yet
209
+ immediately after you save the record, so asserting on the promoted file's
210
+ data or location will fail unless the job has actually been executed. For
211
+ example, with Active Job you'll need to run enqueued jobs inline:
212
+
213
+ ```rb
214
+ require "active_job/test_helper"
215
+
216
+ include ActiveJob::TestHelper
217
+
218
+ perform_enqueued_jobs do
219
+ photo = Photo.create(image: file) # spawns promote job
220
+ end
221
+
222
+ photo.reload # fetch attachment data updated by the background job
223
+ photo.image.storage_key #=> :store
224
+ ```
225
+
226
+ Keep in mind that while the file is cached (i.e. before the promote job has
227
+ run), the record may not have an `id` yet, so plugins like [`pretty_location`]
228
+ that build the storage location from the record's identifier won't be able
229
+ to include it until promotion happens.
230
+
206
231
  [backgrounding]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/backgrounding.rb
232
+ [`pretty_location`]: https://shrinerb.com/docs/plugins/pretty_location
207
233
  [derivatives]: https://shrinerb.com/docs/plugins/derivatives
208
234
  [atomic_helpers]: https://shrinerb.com/docs/plugins/atomic_helpers
@@ -37,8 +37,23 @@ attachment column. You can change the default error message:
37
37
  ```rb
38
38
  plugin :data_uri, error_message: "data URI was invalid"
39
39
  plugin :data_uri, error_message: ->(uri) { I18n.t("errors.data_uri_invalid") }
40
+ plugin :data_uri, error_message: ->(uri, error) { I18n.t("errors.data_uri.#{error.message}") }
40
41
  ```
41
42
 
43
+ ## Maximum size
44
+
45
+ It's a good practice to limit the maximum size of the data URI content:
46
+
47
+ ```rb
48
+ plugin :data_uri, max_size: 10*1024*1024 # 10 MB
49
+ ```
50
+
51
+ Now if content bigger than 10MB is assigned, parsing will fail the same way as
52
+ for an invalid data URI. The size is calculated from the data URI before the
53
+ content is decoded, so oversized content never gets loaded into memory. It's
54
+ exact for base64 content, while for percent-encoded content the encoded length
55
+ is used, which can overestimate.
56
+
42
57
  ## Uploader options
43
58
 
44
59
  Any options passed to `Attacher#assign_data_uri` will be forwarded to
@@ -35,7 +35,8 @@ apply to an attached file. For example, we can generate image thumbnails using
35
35
  the [ImageProcessing] gem:
36
36
 
37
37
  ```rb
38
- gem "image_processing", "~> 1.8"
38
+ gem "image_processing", "~> 2.0"
39
+ gem "mini_magick", "~> 5.0"
39
40
  ```
40
41
  ```rb
41
42
  require "image_processing/mini_magick"
@@ -308,6 +309,19 @@ uploaded_file.derivation_url(:thumbnail, prefix: "transformations/image")
308
309
  #=> ".../transformations/image/thumbnail/eyJpZCI6ImZvbyIsInN?signature=..."
309
310
  ```
310
311
 
312
+ ## Format
313
+
314
+ Some HTTP clients and CDNs use the URL path extension to determine the content
315
+ type of the response. You can append a file extension to the derivation URL
316
+ path with the `:format` option:
317
+
318
+ ```rb
319
+ uploaded_file.derivation_url(:thumbnail, format: "jpg")
320
+ #=> ".../thumbnail/eyJpZCI6ImZvbyIsInN.jpg?signature=..."
321
+ ```
322
+
323
+ The extension is included in the URL signature, so it cannot be tampered with.
324
+
311
325
  ## Expiration
312
326
 
313
327
  By default derivation URLs are valid indefinitely. If you want URLs to expire
@@ -331,6 +345,29 @@ uploaded_file.derivation_url(:thumbnail, expires_in: 90)
331
345
  #=> ".../thumbnail/eyJpZCI6ImZvbyIsInN?expires_at=1547843568&signature=..."
332
346
  ```
333
347
 
348
+ ## Custom signer
349
+
350
+ The derivation URLs are signed by default, and the signature is checked when
351
+ the URLs are requested, which prevents tampering. If you have URL expiration
352
+ turned on, this may prevent your CDN from caching the response.
353
+
354
+ In this case, you may need to do custom CDN-specific URL signing. You can
355
+ bypass Shrine's default signing by passing a custom signer:
356
+
357
+ ```rb
358
+ require "aws-sdk-cloudfront"
359
+ signer = Aws::CloudFront::UrlSigner.new(key_pair_id: "...", private_key: "...")
360
+
361
+ plugin :derivation_endpoint,
362
+ expires_in: 90,
363
+ signer: -> (url, expires_in:) do
364
+ signer.signed_url(url, expires: Time.now.to_i + expires_in)
365
+ end
366
+ ```
367
+
368
+ When `:signer` option is used, the `:secret_key` option is not required, as
369
+ that secret is only used for default signing.
370
+
334
371
  ## Response headers
335
372
 
336
373
  ### Content Type
@@ -796,6 +833,7 @@ derivation.option(:upload_location)
796
833
  | `:metadata` | List of metadata keys the source uploaded file should include in the derivation block | `[]` |
797
834
  | `:prefix` | Path prefix added to the URLs | `nil` |
798
835
  | `:secret_key` | Key used to sign derivation URLs in order to prevent tampering | required |
836
+ | `:signer` | Proc accepting URL and query params used for custom signing of URLs. | `nil` |
799
837
  | `:type` | Media type returned in the `Content-Type` response header in the derivation response | determined from derivative's extension when possible |
800
838
  | `:upload` | Whether the generated derivatives will be cached on the storage | `false` |
801
839
  | `:upload_location` | Location to which the derivatives will be uploaded on the storage | `<source id>/<name>-<args>` |
@@ -20,7 +20,8 @@ Here is an example of generating image thumbnails:
20
20
 
21
21
  ```rb
22
22
  # Gemfile
23
- gem "image_processing", "~> 1.8"
23
+ gem "image_processing", "~> 2.0"
24
+ gem "mini_magick", "~> 5.0"
24
25
  ```
25
26
  ```rb
26
27
  require "image_processing/mini_magick"
@@ -133,7 +134,7 @@ Attacher.default_url do |derivative: nil, **|
133
134
  end
134
135
  ```
135
136
  ```rb
136
- photo.image_url(:medium) #=> "https://example.com/fallbacks.com/medium.jpg"
137
+ photo.image_url(:medium) #=> "https://example.com/fallbacks/medium.jpg"
137
138
  ```
138
139
 
139
140
  Any additional URL options passed to `#<name>_url` will be forwarded to the
@@ -778,6 +779,16 @@ derivatives #=>
778
779
  Like `Shrine.uploaded_file`, the `Shrine.derivatives` method accepts data as a
779
780
  hash (stringified or symbolized) or a JSON string.
780
781
 
782
+ ### Marshalling
783
+
784
+ The `Attacher` instance uses a mutex to make `Attacher#merge_derivatives`
785
+ thread-safe, which is not marshallable. If you want to be able to marshal the
786
+ attacher instance, you can skip mutex usage:
787
+
788
+ ```rb
789
+ plugin :derivatives, mutex: false
790
+ ```
791
+
781
792
  ## Instrumentation
782
793
 
783
794
  If the `instrumentation` plugin has been loaded, the `derivatives` plugin adds
@@ -7,7 +7,7 @@ downloading uploaded files from specified storages. This can be useful when
7
7
  files from your storage isn't accessible over URL (e.g. database storages) or
8
8
  if you want to authenticate your downloads.
9
9
 
10
- ## Global Endpoint
10
+ ## Global Endpoint
11
11
 
12
12
  You can configure the plugin with the path prefix which the endpoint will be
13
13
  mounted on.
@@ -34,6 +34,7 @@ Links to the download endpoint are generated by calling
34
34
  ```rb
35
35
  uploaded_file.download_url #=> "/attachments/eyJpZCI6ImFkdzlyeTM..."
36
36
  ```
37
+
37
38
  ## Endpoint via Uploader
38
39
 
39
40
  You can also configure the plugin in the uploader directly - just make sure to mount it via your Uploader-class.
@@ -52,8 +53,8 @@ Rails.application.routes.draw do
52
53
  end
53
54
  ```
54
55
 
55
- *Hint: For shrine versions 2.x -> ensure that you don't include the plugin
56
- twice (globally and in your uploader class - see #408)*
56
+ _Hint: For shrine versions 2.x -> ensure that you don't include the plugin
57
+ twice (globally and in your uploader class - see #408)_
57
58
 
58
59
  ## Calling from a controller
59
60
 
@@ -69,6 +70,7 @@ Rails.application.routes.draw do
69
70
  get "/attachments/*rest", to: "downloads#image"
70
71
  end
71
72
  ```
73
+
72
74
  ```rb
73
75
  # app/controllers/downloads_controller.rb (Rails)
74
76
  class DownloadsController < ApplicationController
@@ -131,6 +133,16 @@ plugin :download_endpoint, download_options: -> (uploaded_file, request) {
131
133
  }
132
134
  ```
133
135
 
136
+ ## Expiring download urls
137
+
138
+ If you want to have URLs that expire after a certain time, you can use the `:expires_in` and `secret_key` options:
139
+
140
+ ```rb
141
+ plugin :download_endpoint, expires_in: 5 * 60, secret_key: "secret"
142
+ ```
143
+
144
+ this will generate URLs that are signed with a signature valid for 5 minutes.
145
+
134
146
  ## Performance considerations
135
147
 
136
148
  Streaming files through the app might impact the request throughput, depending
@@ -162,7 +174,7 @@ Shrine.download_endpoint(disposition: "attachment")
162
174
  ## Plugin options
163
175
 
164
176
  | Name | Description | Default |
165
- | :-------- | :---------- | :------ |
177
+ | :------------------ | :-------------------------------------------------------------------------------- | :------- |
166
178
  | `:disposition` | Whether browser should render the file `inline` or download it as an `attachment` | `inline` |
167
179
  | `:download_options` | Hash of storage-specific options passed to `Storage#open` | `{}` |
168
180
  | `:host` | URL host that will be added to download URLs | `nil` |
@@ -22,4 +22,19 @@ the bucket "foo". The block is yielded an instance of `MatchData`.
22
22
 
23
23
  This can be useful in combination with the `default_storage` plugin.
24
24
 
25
+ If the resolver block returns `nil` (e.g. it looks up a record that no longer
26
+ exists), `Shrine.find_storage` raises `Shrine::MissingStorage`, which can be
27
+ rescued separately from other `Shrine::Error` subclasses:
28
+
29
+ ```rb
30
+ storage /store_(\w+)/ do |match|
31
+ Library.find_by(id: match[1])&.storage
32
+ end
33
+ ```
34
+
35
+ ```rb
36
+ attacher.file.storage
37
+ # => raises Shrine::MissingStorage if the Library record was deleted
38
+ ```
39
+
25
40
  [dynamic_storage]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/dynamic_storage.rb
@@ -0,0 +1,90 @@
1
+ ---
2
+ title: Fallback Storage
3
+ ---
4
+
5
+ The [`fallback_storage`][fallback_storage] plugin allows you to specify a secondary (fallback) storage that
6
+ Shrine will read from if a file is not found in the primary storage.
7
+
8
+ This is especially useful when setting up a staging or development environment where you want to read files
9
+ from production storage without replicating or copying large amounts of data locally.
10
+
11
+ ```rb
12
+ Shrine.storages = {
13
+ cache: Shrine::Storage::S3.new(endpoint: "https://stage.example.com", prefix: "cache"),
14
+ store: Shrine::Storage::S3.new(endpoint: "https://stage.example.com"),
15
+ fallback: Shrine::Storage::S3.new(endpoint: "https://production.example.com"),
16
+ }
17
+
18
+ Shrine.plugin :fallback_storage
19
+ ```
20
+
21
+ By default, the plugin looks for a storage named `:fallback`. You can override the fallback storage
22
+ name by passing the `:store` option:
23
+
24
+ ```rb
25
+ Shrine.plugin :fallback_storage, store: :production_store
26
+ ```
27
+
28
+ ## How It Works
29
+
30
+ The plugin intercepts file existence checks, URL generation, and file reading methods on
31
+ `Shrine::UploadedFile`.
32
+
33
+ When performing read or check operations, Shrine will first attempt to query the main storage.
34
+ If the file is missing in the primary storage, it seamlessly falls back to the configured fallback storage:
35
+
36
+ * `UploadedFile#exists?` — Returns true if the file exists in the primary storage OR in the fallback storage.
37
+ * `UploadedFile#url` — Returns the URL from the primary storage if the file exists there,
38
+ otherwise generates the URL using the fallback storage.
39
+ * `UploadedFile#open` (and methods relying on it like `read`, `download`, etc.) — Opens the file from the
40
+ primary storage if present, otherwise opens it from the fallback storage.
41
+
42
+ ## Write and Delete Operations
43
+
44
+ The plugin only affects read operations. Uploads, replacements, and deletions interact strictly with
45
+ the primary storage:
46
+
47
+ * Uploads: `Shrine.upload` writes files only to the main storage.
48
+ * Deletes: `UploadedFile#delete` removes the file only from the primary storage. The fallback storage remains
49
+ untouched.
50
+
51
+ ```ruby
52
+ # File exists only in fallback storage
53
+ file = Shrine.uploaded_file({ "id" => "some-id", "storage" => "store" })
54
+ file.exists? # => true (found in fallback)
55
+
56
+ # Deleting removes it from primary storage, but fallback remains intact
57
+ file.delete
58
+ file.exists? # => true (still found in fallback)
59
+ ```
60
+
61
+ For additional security and reliability when pointing to a production environment, it is recommended
62
+ to configure the credentials for the fallback storage (e.g., S3 access keys) with read-only permissions.
63
+
64
+ ## Rails example
65
+
66
+ A common use case is a staging environment that should have access to production files without storing
67
+ its own copies. You can configure Shrine differently per environment:
68
+
69
+ ```rb
70
+ # config/initializers/shrine.rb
71
+ if Rails.env.production?
72
+ Shrine.storages = {
73
+ cache: Shrine::Storage::S3.new(endpoint: "https://production.example.com", prefix: "cache"),
74
+ store: Shrine::Storage::S3.new(endpoint: "https://production.example.com"),
75
+ }
76
+ else
77
+ Shrine.storages = {
78
+ cache: Shrine::Storage::S3.new(endpoint: "https://stage.example.com", prefix: "cache"),
79
+ store: Shrine::Storage::S3.new(endpoint: "https://stage.example.com"),
80
+ production: Shrine::Storage::S3.new(endpoint: "https://production.example.com"),
81
+ }
82
+
83
+ Shrine.plugin :fallback_storage, store: :production
84
+ end
85
+ ```
86
+
87
+ With this setup, staging uploads go to the staging endpoint, but any read for a missing file
88
+ will automatically look in the production endpoint.
89
+
90
+ [fallback_storage]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/fallback_storage.rb