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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +96 -0
- data/README.md +11 -11
- data/doc/advantages.md +1 -1
- data/doc/attacher.md +1 -1
- data/doc/carrierwave.md +2 -2
- data/doc/changing_derivatives.md +2 -1
- data/doc/changing_location.md +19 -6
- data/doc/design.md +4 -4
- data/doc/external/articles.md +16 -16
- data/doc/external/extensions.md +1 -1
- data/doc/getting_started.md +73 -30
- data/doc/multiple_files.md +57 -22
- data/doc/plugins/activerecord.md +91 -0
- data/doc/plugins/backgrounding.md +30 -4
- data/doc/plugins/data_uri.md +15 -0
- data/doc/plugins/derivation_endpoint.md +39 -1
- data/doc/plugins/derivatives.md +13 -2
- data/doc/plugins/download_endpoint.md +16 -4
- data/doc/plugins/dynamic_storage.md +15 -0
- data/doc/plugins/fallback_storage.md +90 -0
- data/doc/plugins/keep_files.md +6 -4
- data/doc/plugins/rack_response.md +9 -0
- data/doc/plugins/refresh_metadata.md +20 -0
- data/doc/plugins/sequel.md +57 -0
- data/doc/plugins/signature.md +8 -6
- data/doc/plugins/tempfile.md +14 -6
- data/doc/plugins/upload_options.md +12 -0
- data/doc/plugins/url_options.md +12 -0
- data/doc/plugins/validation_helpers.md +1 -1
- data/doc/processing.md +5 -3
- data/doc/refile.md +3 -3
- data/doc/release_notes/2.1.0.md +1 -1
- data/doc/release_notes/3.10.0.md +30 -0
- data/doc/release_notes/3.5.0.md +63 -0
- data/doc/release_notes/3.6.0.md +23 -0
- data/doc/release_notes/3.7.0.md +75 -0
- data/doc/release_notes/3.7.1.md +31 -0
- data/doc/release_notes/3.8.0.md +37 -0
- data/doc/release_notes/3.9.0.md +42 -0
- data/doc/retrieving_uploads.md +1 -1
- data/doc/storage/s3.md +10 -0
- data/doc/testing.md +45 -17
- data/doc/upgrading_to_3.md +3 -5
- data/lib/shrine/attacher.rb +41 -29
- data/lib/shrine/attachment.rb +2 -2
- data/lib/shrine/plugins/_persistence.rb +1 -1
- data/lib/shrine/plugins/_urlsafe_serialization.rb +4 -4
- data/lib/shrine/plugins/activerecord.rb +25 -4
- data/lib/shrine/plugins/add_metadata.rb +2 -4
- data/lib/shrine/plugins/atomic_helpers.rb +7 -7
- data/lib/shrine/plugins/backgrounding.rb +10 -10
- data/lib/shrine/plugins/column.rb +7 -5
- data/lib/shrine/plugins/data_uri.rb +28 -3
- data/lib/shrine/plugins/default_url.rb +4 -4
- data/lib/shrine/plugins/delete_raw.rb +2 -2
- data/lib/shrine/plugins/derivation_endpoint.rb +65 -59
- data/lib/shrine/plugins/derivatives.rb +35 -21
- data/lib/shrine/plugins/download_endpoint.rb +72 -11
- data/lib/shrine/plugins/entity.rb +15 -9
- data/lib/shrine/plugins/fallback_storage.rb +51 -0
- data/lib/shrine/plugins/infer_extension.rb +5 -1
- data/lib/shrine/plugins/instrumentation.rb +13 -9
- data/lib/shrine/plugins/metadata_attributes.rb +1 -1
- data/lib/shrine/plugins/mirroring.rb +10 -10
- data/lib/shrine/plugins/model.rb +12 -10
- data/lib/shrine/plugins/presign_endpoint.rb +13 -10
- data/lib/shrine/plugins/pretty_location.rb +2 -2
- data/lib/shrine/plugins/processing.rb +3 -3
- data/lib/shrine/plugins/rack_file.rb +2 -2
- data/lib/shrine/plugins/rack_response.rb +12 -6
- data/lib/shrine/plugins/refresh_metadata.rb +6 -6
- data/lib/shrine/plugins/remote_url.rb +4 -4
- data/lib/shrine/plugins/remove_attachment.rb +2 -0
- data/lib/shrine/plugins/remove_invalid.rb +4 -0
- data/lib/shrine/plugins/restore_cached_data.rb +3 -3
- data/lib/shrine/plugins/sequel.rb +15 -1
- data/lib/shrine/plugins/signature.rb +2 -2
- data/lib/shrine/plugins/store_dimensions.rb +2 -2
- data/lib/shrine/plugins/tempfile.rb +0 -2
- data/lib/shrine/plugins/upload_endpoint.rb +7 -5
- data/lib/shrine/plugins/upload_options.rb +18 -4
- data/lib/shrine/plugins/url_options.rb +17 -3
- data/lib/shrine/plugins/validation.rb +8 -8
- data/lib/shrine/plugins/validation_helpers.rb +2 -2
- data/lib/shrine/plugins/versions.rb +10 -10
- data/lib/shrine/plugins.rb +6 -14
- data/lib/shrine/storage/file_system.rb +15 -19
- data/lib/shrine/storage/linter.rb +8 -8
- data/lib/shrine/storage/memory.rb +1 -3
- data/lib/shrine/storage/s3.rb +60 -40
- data/lib/shrine/uploaded_file.rb +23 -19
- data/lib/shrine/version.rb +1 -1
- data/lib/shrine.rb +24 -20
- data/shrine.gemspec +10 -8
- metadata +52 -26
data/doc/multiple_files.md
CHANGED
|
@@ -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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/doc/plugins/activerecord.md
CHANGED
|
@@ -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
|
data/doc/plugins/data_uri.md
CHANGED
|
@@ -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", "~>
|
|
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>` |
|
data/doc/plugins/derivatives.md
CHANGED
|
@@ -20,7 +20,8 @@ Here is an example of generating image thumbnails:
|
|
|
20
20
|
|
|
21
21
|
```rb
|
|
22
22
|
# Gemfile
|
|
23
|
-
gem "image_processing", "~>
|
|
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
|
|
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
|
-
|
|
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
|