shrine 3.7.1 → 3.9.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 +29 -1
- data/doc/attacher.md +1 -1
- data/doc/design.md +3 -3
- data/doc/plugins/activerecord.md +91 -0
- data/doc/plugins/backgrounding.md +26 -0
- data/doc/plugins/dynamic_storage.md +15 -0
- data/doc/plugins/sequel.md +57 -0
- 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/release_notes/3.7.1.md +18 -0
- data/doc/release_notes/3.8.0.md +37 -0
- data/doc/release_notes/3.9.0.md +42 -0
- data/lib/shrine/attacher.rb +13 -8
- data/lib/shrine/plugins/activerecord.rb +25 -4
- data/lib/shrine/plugins/backgrounding.rb +1 -1
- data/lib/shrine/plugins/column.rb +1 -1
- data/lib/shrine/plugins/derivatives.rb +7 -5
- data/lib/shrine/plugins/remove_invalid.rb +4 -0
- data/lib/shrine/plugins/sequel.rb +15 -1
- data/lib/shrine/plugins/tempfile.rb +0 -2
- data/lib/shrine/plugins/upload_options.rb +17 -3
- data/lib/shrine/plugins/url_options.rb +17 -3
- data/lib/shrine/storage/file_system.rb +11 -2
- data/lib/shrine/storage/s3.rb +5 -10
- data/lib/shrine/version.rb +2 -2
- data/lib/shrine.rb +5 -1
- data/shrine.gemspec +4 -2
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fc3e1cb064bc4b1fc0132c5d97c244506b65014eac06b0e8181cf44b2319bf67
|
|
4
|
+
data.tar.gz: c289af323f41d3cd70dbe740439c9a08f595bba81150a396639cecd92b8b53af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 128cb224b8199aba9b4a24a0b911c90bc88e404fc9746b440b3edafe19b28422506edf67f4c01474701a9d9a56d5bc4a874152741239a8ff05bc1fa6449a6fb4
|
|
7
|
+
data.tar.gz: 2582f06b0c087d56cea3e9056bd0763a8d50613dc86bc2707593a3322da0d4be018a507189ca96b63f70715309d8f469f3bc536fcf4c5b3ab859d54697e9910b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
|
+
## 3.9.0 (2026-07-13)
|
|
2
|
+
|
|
3
|
+
* `Shrine.find_storage` now raises `Shrine::MissingStorage` (subclass of `Shrine::Error`) when the storage isn't registered, so it can be rescued separately (@janko)
|
|
4
|
+
|
|
5
|
+
* `url_options` – Allow regular expression storage keys for compatibility with `dynamic_storage` plugin (@janko)
|
|
6
|
+
|
|
7
|
+
* `upload_options` – Allow regular expression storage keys for compatibility with `dynamic_storage` plugin (@janko)
|
|
8
|
+
|
|
9
|
+
* `remove_invalid` – Fix model attribute not being cleared/reverted after deassigning invalid file (@janko)
|
|
10
|
+
|
|
11
|
+
* `activerecord`/`sequel` – Skip deleting the previous attachment when the record being saved was just created, to avoid deleting the original attachment of a duplicated record (@janko)
|
|
12
|
+
|
|
13
|
+
* `tempfile` – Allow `UploadedFile#tempfile` to be called without opening the uploaded file first (@janko)
|
|
14
|
+
|
|
15
|
+
* `activerecord` – Add `:attribute_types` option for skipping serialization of JSON/JSONB attributes declared via the Attributes API, not just database columns (@vojtad, @janko)
|
|
16
|
+
|
|
17
|
+
## 3.8.0 (2026-06-24)
|
|
18
|
+
|
|
19
|
+
* `s3` – Use single-request uploads for smaller files again instead of always using multipart uploads (@janko)
|
|
20
|
+
|
|
21
|
+
* `derivatives` – Forward `Attacher#promote` options to `Attacher#upload_derivatives` (@milkcocoa)
|
|
22
|
+
|
|
23
|
+
* `file_system` – Prevent path traversal outside of the storage directory (@janko)
|
|
24
|
+
|
|
1
25
|
## 3.7.1 (2026-06-03)
|
|
2
26
|
|
|
3
|
-
* Update method signatures of some plugins to work around a Bootsnap bug causing a `wrong number of arguments` error
|
|
27
|
+
* Update method signatures of some plugins to work around a Bootsnap bug causing a `wrong number of arguments` error (@janko)
|
|
28
|
+
|
|
29
|
+
* `derivation_endpoint` – Add `:format` argument to `UploadedFile#derivation_url` for path extension (@janko)
|
|
30
|
+
|
|
31
|
+
* `rack_response` – Add `:etag` argument for setting a custom `ETag` (@camilohollanda)
|
|
4
32
|
|
|
5
33
|
## 3.7.0 (2026-05-27)
|
|
6
34
|
|
data/doc/attacher.md
CHANGED
|
@@ -189,7 +189,7 @@ attacher.upload(file, location: "path/to/file") # setting upload locat
|
|
|
189
189
|
|
|
190
190
|
### Changes
|
|
191
191
|
|
|
192
|
-
When a new file is attached, calling [`Attacher#finalize`](#
|
|
192
|
+
When a new file is attached, calling [`Attacher#finalize`](#finalizing) will
|
|
193
193
|
perform additional actions such as promotion and deleting any previous file.
|
|
194
194
|
It will also trigger [validation].
|
|
195
195
|
|
data/doc/design.md
CHANGED
|
@@ -9,8 +9,8 @@ There are five main types of classes that you deal with in Shrine:
|
|
|
9
9
|
|
|
10
10
|
| Class | Description |
|
|
11
11
|
| :---- | :---------- |
|
|
12
|
-
| [`Shrine::Storage::*`](#
|
|
13
|
-
| [`Shrine`](#
|
|
12
|
+
| [`Shrine::Storage::*`](#storage) | Manages files on a particular storage service |
|
|
13
|
+
| [`Shrine`](#shrine) | Wraps uploads and handles loading plugins |
|
|
14
14
|
| [`Shrine::UploadedFile`](#shrineuploadedfile) | Represents a file uploaded to a storage |
|
|
15
15
|
| [`Shrine::Attacher`](#shrineattacher) | Handles file attachment logic |
|
|
16
16
|
| [`Shrine::Attachment`](#shrineattachment) | Provides convenience model attachment interface |
|
|
@@ -60,7 +60,7 @@ end
|
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
Storages are typically not used directly, but through [`Shrine`](#shrine) and
|
|
63
|
-
[`Shrine::UploadedFile`](#
|
|
63
|
+
[`Shrine::UploadedFile`](#shrineuploadedfile) classes.
|
|
64
64
|
|
|
65
65
|
## `Shrine`
|
|
66
66
|
|
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
|
|
@@ -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
|
|
@@ -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
|
data/doc/plugins/sequel.md
CHANGED
|
@@ -100,6 +100,63 @@ set `:hooks` to `false`:
|
|
|
100
100
|
plugin :sequel, hooks: false
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
+
#### Duplicating records
|
|
104
|
+
|
|
105
|
+
Since a record being created can't yet have a confirmed attachment of its own
|
|
106
|
+
to safely replace, Shrine never deletes the previous file when the attachment
|
|
107
|
+
changes as part of *creating* a record, only when *updating* one.
|
|
108
|
+
|
|
109
|
+
Note that Sequel's `#dup`/`#clone`, unlike Active Record's, don't reset the
|
|
110
|
+
primary key or persistence state — a duplicated record still refers to the
|
|
111
|
+
*same* row, so saving it just updates that row rather than inserting a new
|
|
112
|
+
one. There's only ever one row here, so there's nothing to protect:
|
|
113
|
+
|
|
114
|
+
```rb
|
|
115
|
+
photo = Photo.create(image: file)
|
|
116
|
+
photo2 = photo.dup # `photo2` refers to the same row as `photo`
|
|
117
|
+
|
|
118
|
+
photo2.update(image: new_file)
|
|
119
|
+
photo.image.exists? #=> false (the row now has `new_file`, so this is expected)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The common way to duplicate a record in Sequel is instead to construct a new
|
|
123
|
+
one from the original's values, which *does* produce a genuinely new,
|
|
124
|
+
unpersisted row, and so *is* protected:
|
|
125
|
+
|
|
126
|
+
```rb
|
|
127
|
+
photo2 = Photo.new(photo.values.except(:id))
|
|
128
|
+
|
|
129
|
+
photo2.update(image: new_file)
|
|
130
|
+
photo.image.exists? #=> true (not affected)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Keep in mind this only protects against *replacing* the attachment on create.
|
|
134
|
+
As long as `photo` and `photo2` continue to reference the same underlying
|
|
135
|
+
file (i.e. `photo2` is saved without ever changing its attachment),
|
|
136
|
+
destroying either record will still delete the file the other one
|
|
137
|
+
references, since Shrine has no way of knowing the file is shared:
|
|
138
|
+
|
|
139
|
+
```rb
|
|
140
|
+
photo2.save # still references the same file as `photo`
|
|
141
|
+
|
|
142
|
+
photo2.destroy
|
|
143
|
+
photo.image.exists? #=> false
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
If you want `photo2` to have its own independent copy of the file from the
|
|
147
|
+
start, so that destroying either record is also safe, upload a new copy
|
|
148
|
+
explicitly:
|
|
149
|
+
|
|
150
|
+
```rb
|
|
151
|
+
photo2 = Photo.new(photo.values.reject { |k, _| k == :id })
|
|
152
|
+
photo2.image_attacher.set(nil)
|
|
153
|
+
photo2.image_attacher.attach(photo.image, storage: photo.image.storage_key)
|
|
154
|
+
photo2.save
|
|
155
|
+
|
|
156
|
+
photo2.destroy # no longer affects `photo`
|
|
157
|
+
photo.image.exists? #=> true
|
|
158
|
+
```
|
|
159
|
+
|
|
103
160
|
### Validations
|
|
104
161
|
|
|
105
162
|
If you're using the [`validation`][validation] plugin, the attachment module
|
data/doc/plugins/tempfile.md
CHANGED
|
@@ -9,14 +9,22 @@ uploaded file on disk.
|
|
|
9
9
|
Shrine.plugin :tempfile
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
The plugin provides the `UploadedFile#tempfile` method, which
|
|
13
|
-
|
|
14
|
-
the
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
closed
|
|
12
|
+
The plugin provides the `UploadedFile#tempfile` method, which returns a copy
|
|
13
|
+
of the uploaded file's content on disk. The first time the method is called
|
|
14
|
+
the file content will be downloaded into a temporary file and returned. On
|
|
15
|
+
any subsequent method calls the cached temporary file will be returned
|
|
16
|
+
directly. If the uploaded file is currently open, its tempfile is deleted
|
|
17
|
+
when the uploaded file is closed; otherwise it's deleted whenever it becomes
|
|
18
|
+
unreachable and is garbage collected (so it's still recommended to close the
|
|
19
|
+
uploaded file when you're done with it, to have the tempfile cleaned up
|
|
20
|
+
deterministically).
|
|
18
21
|
|
|
19
22
|
```rb
|
|
23
|
+
uploaded_file.tempfile #=> #<Tempfile:...> (file is downloaded and cached)
|
|
24
|
+
uploaded_file.tempfile #=> #<Tempfile:...> (cache is returned)
|
|
25
|
+
|
|
26
|
+
# OR
|
|
27
|
+
|
|
20
28
|
uploaded_file.open do
|
|
21
29
|
# ...
|
|
22
30
|
uploaded_file.tempfile #=> #<Tempfile:...> (file is cached)
|
|
@@ -29,4 +29,16 @@ the uploader.
|
|
|
29
29
|
uploader.upload(file, upload_options: { acl: "public-read" })
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
If storage keys are generated dynamically (e.g. via the
|
|
33
|
+
[`dynamic_storage`][dynamic_storage] plugin), it's not possible to list every
|
|
34
|
+
storage key upfront. In that case you can use a `Regexp` instead, which will
|
|
35
|
+
be matched against the storage key:
|
|
36
|
+
|
|
37
|
+
```rb
|
|
38
|
+
plugin :upload_options, /_store\z/ => { acl: "private" }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
An exact storage key match always takes precedence over a `Regexp` match.
|
|
42
|
+
|
|
32
43
|
[upload_options]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/upload_options.rb
|
|
44
|
+
[dynamic_storage]: https://shrinerb.com/docs/plugins/dynamic_storage
|
data/doc/plugins/url_options.md
CHANGED
|
@@ -24,4 +24,16 @@ In both cases the default options are merged with options passed to
|
|
|
24
24
|
`UploadedFile#url`, and the latter will always have precedence over default
|
|
25
25
|
options.
|
|
26
26
|
|
|
27
|
+
If storage keys are generated dynamically (e.g. via the
|
|
28
|
+
[`dynamic_storage`][dynamic_storage] plugin), it's not possible to list every
|
|
29
|
+
storage key upfront. In that case you can use a `Regexp` instead, which will
|
|
30
|
+
be matched against the storage key:
|
|
31
|
+
|
|
32
|
+
```rb
|
|
33
|
+
plugin :url_options, /_store\z/ => { expires_in: 24*60*60 }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
An exact storage key match always takes precedence over a `Regexp` match.
|
|
37
|
+
|
|
27
38
|
[url_options]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/url_options.rb
|
|
39
|
+
[dynamic_storage]: https://shrinerb.com/docs/plugins/dynamic_storage
|
data/doc/release_notes/3.7.1.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
title: Shrine 3.7.1
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## New features
|
|
6
|
+
|
|
7
|
+
* The `derivation_endpoint` plugin now supports adding a file extension to the derivation URL path via the `:format` option on `UploadedFile#derivation_url`. Some HTTP clients and CDNs use the URL path extension to determine the content type of the response.
|
|
8
|
+
|
|
9
|
+
```rb
|
|
10
|
+
uploaded_file.derivation_url(:thumbnail, format: "jpg")
|
|
11
|
+
#=> ".../thumbnail/eyJpZCI6ImZvbyIsInN.jpg?signature=..."
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The extension is included in the URL signature, so it cannot be tampered with.
|
|
15
|
+
|
|
16
|
+
* The `rack_response` plugin now accepts an `:etag` option for setting a custom `ETag` header, overriding the default one Shrine generates.
|
|
17
|
+
|
|
18
|
+
```rb
|
|
19
|
+
response = uploaded_file.to_rack_response(etag: "my-custom-etag")
|
|
20
|
+
response[1]["ETag"] #=> "my-custom-etag"
|
|
21
|
+
```
|
|
22
|
+
|
|
5
23
|
## Bug fixes
|
|
6
24
|
|
|
7
25
|
* When initializing Shrine with the `activerecord` plugin, under Bootsnap 1.24.5 and Ruby 3.4.5 this would raise an error:
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Shrine 3.8.0
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Security
|
|
6
|
+
|
|
7
|
+
* The `file_system` storage now prevents path traversal outside of the storage
|
|
8
|
+
directory. Previously, an id containing `../` sequences (e.g. coming from
|
|
9
|
+
attacker-controlled data) could resolve to a location outside of the
|
|
10
|
+
configured storage directory. Now `Shrine::Error` is raised whenever an id
|
|
11
|
+
would resolve outside of the storage directory:
|
|
12
|
+
|
|
13
|
+
```rb
|
|
14
|
+
storage = Shrine::Storage::FileSystem.new("uploads")
|
|
15
|
+
storage.open("../../etc/passwd") #~> Shrine::Error
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## New features
|
|
19
|
+
|
|
20
|
+
* The `derivatives` plugin now forwards options passed to `Attacher#promote`
|
|
21
|
+
into `Attacher#upload_derivatives`. This means options such as upload options
|
|
22
|
+
reach the derivatives upload as well, not just the main file.
|
|
23
|
+
|
|
24
|
+
```rb
|
|
25
|
+
attacher.promote(upload_options: { acl: "public-read" })
|
|
26
|
+
# the same options are now forwarded when uploading derivatives
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Other improvements
|
|
30
|
+
|
|
31
|
+
* The `s3` storage again uses single-request uploads for smaller files, instead
|
|
32
|
+
of always using multipart uploads. In Shrine 3.7.0, when a `TransferManager`
|
|
33
|
+
was available, it was used for uploads of any size, but `TransferManager`
|
|
34
|
+
uploads via multipart unconditionally, which is inefficient for smaller files.
|
|
35
|
+
Now files at or below the multipart threshold (`:upload`, 15MB by default) are
|
|
36
|
+
uploaded in a single request, and only larger files use multipart upload via
|
|
37
|
+
the `TransferManager` (falling back to the older API when it's not available).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Shrine 3.9.0
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## New features
|
|
6
|
+
|
|
7
|
+
* `Shrine.find_storage` now raises `Shrine::MissingStorage` (a subclass of `Shrine::Error`) when the storage isn't registered, instead of a generic `Shrine::Error`. This is useful with the `dynamic_storage` plugin, where the resolver might fail to find its dependencies (e.g. a deleted DB record), and lets you rescue that case separately from other errors.
|
|
8
|
+
|
|
9
|
+
```rb
|
|
10
|
+
begin
|
|
11
|
+
Shrine.find_storage(:store)
|
|
12
|
+
rescue Shrine::MissingStorage
|
|
13
|
+
# handle missing storage specifically
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
* The `url_options` and `upload_options` plugins now accept a `Regexp` in place of a storage key. This is useful when storage keys are generated dynamically (e.g. via the `dynamic_storage` plugin), since it's not possible to list every storage key upfront. An exact storage key match always takes precedence over a `Regexp` match.
|
|
18
|
+
|
|
19
|
+
```rb
|
|
20
|
+
plugin :url_options, /_store\z/ => { expires_in: 24*60*60 }
|
|
21
|
+
plugin :upload_options, /_store\z/ => { acl: "private" }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
* The `activerecord` plugin now accepts an `:attribute_types` option. When enabled, it uses `type_for_attribute` (instead of just `columns_hash`) to detect whether the data attribute is JSON/JSONB, so Shrine also skips its own serialization for attributes declared via the Active Record Attributes API (not just real database columns). It defaults to `false` for backwards compatibility with apps that might be relying on the previous double-serialization behavior.
|
|
25
|
+
|
|
26
|
+
```rb
|
|
27
|
+
class Photo < ActiveRecord::Base # `image_data` is a text column
|
|
28
|
+
include ImageUploader::Attachment(:image)
|
|
29
|
+
|
|
30
|
+
attribute :image_data, :json
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
plugin :activerecord, attribute_types: true
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Bug fixes
|
|
37
|
+
|
|
38
|
+
* The `activerecord` and `sequel` plugins no longer delete the previous attachment when the record being saved was just created. Previously, duplicating a persisted record (e.g. via `#dup`) and immediately assigning a new attachment before saving the duplicate could end up deleting the *original* record's attachment, since both records shared the same underlying file.
|
|
39
|
+
|
|
40
|
+
* The `remove_invalid` plugin now correctly clears/reverts the model attribute after an invalid file is deassigned. Previously the record's raw attribute could be left out of sync with the attacher after validation failed.
|
|
41
|
+
|
|
42
|
+
* `UploadedFile#tempfile` (from the `tempfile` plugin) can now be called without opening the uploaded file first. Previously it raised `Shrine::Error` unless the file was already open; now it downloads and caches the file on demand. If the uploaded file is open, its tempfile is still cleaned up when the file is closed; otherwise it's cleaned up whenever it becomes unreachable and is garbage collected, so it's still recommended to close the uploaded file when you're done with it for deterministic cleanup.
|
data/lib/shrine/attacher.rb
CHANGED
|
@@ -20,7 +20,7 @@ class Shrine
|
|
|
20
20
|
|
|
21
21
|
# Initializes the attacher from a data hash generated from `Attacher#data`.
|
|
22
22
|
#
|
|
23
|
-
# attacher = Attacher.from_data({ "id" => "...", "storage" => "...", "metadata" => {
|
|
23
|
+
# attacher = Attacher.from_data({ "id" => "...", "storage" => "...", "metadata" => {} })
|
|
24
24
|
# attacher.file #=> #<Shrine::UploadedFile>
|
|
25
25
|
def from_data(data, **)
|
|
26
26
|
attacher = new(**)
|
|
@@ -60,8 +60,8 @@ class Shrine
|
|
|
60
60
|
# useful when the uploaded file comes from form fields). Forwards any
|
|
61
61
|
# additional options to #attach_cached.
|
|
62
62
|
#
|
|
63
|
-
# attacher.assign(File.open(
|
|
64
|
-
# attacher.assign(File.open(
|
|
63
|
+
# attacher.assign(File.open(path))
|
|
64
|
+
# attacher.assign(File.open(path), metadata: { "foo" => "bar" })
|
|
65
65
|
# attacher.assign('{"id":"...","storage":"cache","metadata":{...}}')
|
|
66
66
|
# attacher.assign({ "id" => "...", "storage" => "cache", "metadata" => {} })
|
|
67
67
|
#
|
|
@@ -82,10 +82,10 @@ class Shrine
|
|
|
82
82
|
# #attach.
|
|
83
83
|
#
|
|
84
84
|
# # upload file to temporary storage and set the uploaded file.
|
|
85
|
-
# attacher.attach_cached(File.open(
|
|
85
|
+
# attacher.attach_cached(File.open(path))
|
|
86
86
|
#
|
|
87
87
|
# # foward additional options to the uploader
|
|
88
|
-
# attacher.attach_cached(File.open(
|
|
88
|
+
# attacher.attach_cached(File.open(path), metadata: { "foo" => "bar" })
|
|
89
89
|
#
|
|
90
90
|
# # sets an existing cached file from JSON data
|
|
91
91
|
# attacher.attach_cached('{"id":"...","storage":"cache","metadata":{...}}')
|
|
@@ -189,7 +189,7 @@ class Shrine
|
|
|
189
189
|
# attacher.destroy_previous
|
|
190
190
|
# previous_file.exists? #=> false
|
|
191
191
|
def destroy_previous
|
|
192
|
-
@previous.destroy_attached if changed?
|
|
192
|
+
@previous.destroy_attached if changed? && destroy_previous?
|
|
193
193
|
end
|
|
194
194
|
|
|
195
195
|
# Destroys the attached file if it exists and is uploaded to permanent
|
|
@@ -299,7 +299,7 @@ class Shrine
|
|
|
299
299
|
# Loads the uploaded file from data generated by `Attacher#data`.
|
|
300
300
|
#
|
|
301
301
|
# attacher.file #=> nil
|
|
302
|
-
# attacher.load_data({ "id" => "...", "storage" => "...", "metadata" => {
|
|
302
|
+
# attacher.load_data({ "id" => "...", "storage" => "...", "metadata" => {} })
|
|
303
303
|
# attacher.file #=> #<Shrine::UploadedFile>
|
|
304
304
|
def load_data(data)
|
|
305
305
|
@file = data && uploaded_file(data)
|
|
@@ -356,7 +356,7 @@ class Shrine
|
|
|
356
356
|
# #=> #<Shrine::UploadedFile>
|
|
357
357
|
#
|
|
358
358
|
# # from Hash data
|
|
359
|
-
# attacher.cached({ "id" => "...", "storage" => "cache", "metadata" => {
|
|
359
|
+
# attacher.cached({ "id" => "...", "storage" => "cache", "metadata" => {} })
|
|
360
360
|
# #=> #<Shrine::UploadedFile>
|
|
361
361
|
def cached(value, **)
|
|
362
362
|
uploaded_file = uploaded_file(value)
|
|
@@ -380,6 +380,11 @@ class Shrine
|
|
|
380
380
|
attached? && !cached?
|
|
381
381
|
end
|
|
382
382
|
|
|
383
|
+
# Whether the previous attached file should be deleted.
|
|
384
|
+
def destroy_previous?
|
|
385
|
+
true
|
|
386
|
+
end
|
|
387
|
+
|
|
383
388
|
# Whether assigning the given file is considered a change.
|
|
384
389
|
def change?(file)
|
|
385
390
|
@file != file
|
|
@@ -12,7 +12,7 @@ class Shrine
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def self.configure(uploader, **opts)
|
|
15
|
-
uploader.opts[:activerecord] ||= { callbacks: true, validations: true }
|
|
15
|
+
uploader.opts[:activerecord] ||= { callbacks: true, validations: true, attribute_types: false }
|
|
16
16
|
uploader.opts[:activerecord].merge!(opts)
|
|
17
17
|
end
|
|
18
18
|
|
|
@@ -108,12 +108,22 @@ class Shrine
|
|
|
108
108
|
record.transaction { yield record.clone.reload(lock: true) }
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
-
# Returns true if the data attribute represents a JSON or JSONB
|
|
111
|
+
# Returns true if the data attribute represents a JSON or JSONB type.
|
|
112
112
|
# Used by the _persistence plugin to determine whether serialization
|
|
113
113
|
# should be skipped.
|
|
114
|
+
#
|
|
115
|
+
# By default only real database columns are checked. When the
|
|
116
|
+
# `:attribute_types` plugin option is enabled, the check uses
|
|
117
|
+
# `type_for_attribute`, so attributes declared via the Attributes API
|
|
118
|
+
# are recognized as well.
|
|
114
119
|
def activerecord_hash_attribute?
|
|
115
|
-
|
|
116
|
-
|
|
120
|
+
if shrine_class.opts[:activerecord][:attribute_types]
|
|
121
|
+
attribute_type = record.class.type_for_attribute(attribute.to_s)
|
|
122
|
+
attribute_type && [:json, :jsonb].include?(attribute_type.type)
|
|
123
|
+
else
|
|
124
|
+
column = record.class.columns_hash[attribute.to_s]
|
|
125
|
+
column && [:json, :jsonb].include?(column.type)
|
|
126
|
+
end
|
|
117
127
|
end
|
|
118
128
|
|
|
119
129
|
# Returns whether the record is an ActiveRecord model. Used by the
|
|
@@ -121,6 +131,17 @@ class Shrine
|
|
|
121
131
|
def activerecord?
|
|
122
132
|
record.is_a?(::ActiveRecord::Base)
|
|
123
133
|
end
|
|
134
|
+
|
|
135
|
+
# Prevents the previous file from being destroyed when this save
|
|
136
|
+
# created the record (as opposed to updating an existing one), since
|
|
137
|
+
# a record being created can't yet have its own confirmed attachment
|
|
138
|
+
# to safely replace (e.g. when it was duplicated via `#dup` from
|
|
139
|
+
# another, still-persisted record).
|
|
140
|
+
def destroy_previous?
|
|
141
|
+
return super unless activerecord?
|
|
142
|
+
|
|
143
|
+
super && !record.previously_new_record?
|
|
144
|
+
end
|
|
124
145
|
end
|
|
125
146
|
end
|
|
126
147
|
|
|
@@ -54,7 +54,7 @@ class Shrine
|
|
|
54
54
|
# Converts the column data hash into a string (generates JSON by
|
|
55
55
|
# default).
|
|
56
56
|
#
|
|
57
|
-
# Attacher.serialize_column({ "id" => "...", "storage" => "...", "metadata" => {
|
|
57
|
+
# Attacher.serialize_column({ "id" => "...", "storage" => "...", "metadata" => {} })
|
|
58
58
|
# #=> '{"id":"...","storage":"...","metadata":{...}}'
|
|
59
59
|
#
|
|
60
60
|
# Attacher.serialize_column(nil)
|
|
@@ -161,7 +161,7 @@ class Shrine
|
|
|
161
161
|
# attacher.stored?(attacher.derivatives[:thumb]) #=> true
|
|
162
162
|
def promote(**options)
|
|
163
163
|
super
|
|
164
|
-
promote_derivatives
|
|
164
|
+
promote_derivatives(**options)
|
|
165
165
|
create_derivatives if create_derivatives_on_promote?
|
|
166
166
|
end
|
|
167
167
|
|
|
@@ -431,12 +431,12 @@ class Shrine
|
|
|
431
431
|
# attacher.load_data({
|
|
432
432
|
# "id" => "...",
|
|
433
433
|
# "storage" => "store",
|
|
434
|
-
# "metadata" => {
|
|
434
|
+
# "metadata" => {},
|
|
435
435
|
# "derivatives" => {
|
|
436
436
|
# "thumb" => {
|
|
437
437
|
# "id" => "...",
|
|
438
438
|
# "storage" => "store",
|
|
439
|
-
# "metadata" => {
|
|
439
|
+
# "metadata" => {},
|
|
440
440
|
# }
|
|
441
441
|
# }
|
|
442
442
|
# })
|
|
@@ -471,7 +471,7 @@ class Shrine
|
|
|
471
471
|
|
|
472
472
|
# Sets a hash of derivatives.
|
|
473
473
|
#
|
|
474
|
-
# attacher.derivatives = { thumb: Shrine.uploaded_file(
|
|
474
|
+
# attacher.derivatives = { thumb: Shrine.uploaded_file(data) }
|
|
475
475
|
# attacher.derivatives #=> { thumb: #<Shrine::UploadedFile ...> }
|
|
476
476
|
def derivatives=(derivatives)
|
|
477
477
|
unless derivatives.is_a?(Hash)
|
|
@@ -483,7 +483,9 @@ class Shrine
|
|
|
483
483
|
|
|
484
484
|
# Iterates through nested derivatives and maps results.
|
|
485
485
|
#
|
|
486
|
-
# attacher.map_derivative(derivatives)
|
|
486
|
+
# attacher.map_derivative(derivatives) do |path, file|
|
|
487
|
+
# # ...
|
|
488
|
+
# end
|
|
487
489
|
def map_derivative(derivatives, **options, &block)
|
|
488
490
|
shrine_class.map_derivative(derivatives, **options, &block)
|
|
489
491
|
end
|
|
@@ -87,8 +87,11 @@ class Shrine
|
|
|
87
87
|
end
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
-
# Calls Attacher#save. Called before model save.
|
|
90
|
+
# Calls Attacher#save. Called before model save. Remembers whether
|
|
91
|
+
# the record is new, since by the time `#sequel_after_save` runs,
|
|
92
|
+
# Sequel has already flipped `#new?` to false.
|
|
91
93
|
def sequel_before_save
|
|
94
|
+
@previously_new_record = record.new?
|
|
92
95
|
save
|
|
93
96
|
end
|
|
94
97
|
|
|
@@ -132,6 +135,17 @@ class Shrine
|
|
|
132
135
|
def sequel?
|
|
133
136
|
record.is_a?(::Sequel::Model)
|
|
134
137
|
end
|
|
138
|
+
|
|
139
|
+
# Prevents the previous file from being destroyed when this save
|
|
140
|
+
# created the record (as opposed to updating an existing one), since
|
|
141
|
+
# a record being created can't yet have its own confirmed attachment
|
|
142
|
+
# to safely replace (e.g. when it was duplicated from another,
|
|
143
|
+
# still-persisted record).
|
|
144
|
+
def destroy_previous?
|
|
145
|
+
return super unless sequel?
|
|
146
|
+
|
|
147
|
+
super && !@previously_new_record
|
|
148
|
+
end
|
|
135
149
|
end
|
|
136
150
|
end
|
|
137
151
|
|
|
@@ -4,9 +4,9 @@ class Shrine
|
|
|
4
4
|
module Plugins
|
|
5
5
|
# Documentation can be found on https://shrinerb.com/docs/plugins/upload_options
|
|
6
6
|
module UploadOptions
|
|
7
|
-
def self.configure(uploader,
|
|
7
|
+
def self.configure(uploader, options = {})
|
|
8
8
|
uploader.opts[:upload_options] ||= {}
|
|
9
|
-
uploader.opts[:upload_options].merge!(
|
|
9
|
+
uploader.opts[:upload_options].merge!(options)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
module InstanceMethods
|
|
@@ -19,11 +19,25 @@ class Shrine
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def get_upload_options(io, options)
|
|
22
|
-
upload_options =
|
|
22
|
+
upload_options = find_upload_options || {}
|
|
23
23
|
upload_options = upload_options.call(io, options) if upload_options.respond_to?(:call)
|
|
24
24
|
upload_options = upload_options.merge(options[:upload_options]) if options[:upload_options]
|
|
25
25
|
upload_options
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
# Matches the storage key exactly first, then falls back to any
|
|
29
|
+
# registered regex that matches the storage key. The regex form is
|
|
30
|
+
# useful when storage keys are generated dynamically (e.g. via the
|
|
31
|
+
# `dynamic_storage` plugin), since it's not possible to list every
|
|
32
|
+
# storage key upfront.
|
|
33
|
+
def find_upload_options
|
|
34
|
+
upload_options = opts[:upload_options]
|
|
35
|
+
|
|
36
|
+
return upload_options[storage_key] if upload_options.key?(storage_key)
|
|
37
|
+
|
|
38
|
+
_, options = upload_options.find { |key, _| key.is_a?(Regexp) && key.match?(storage_key.to_s) }
|
|
39
|
+
options
|
|
40
|
+
end
|
|
27
41
|
end
|
|
28
42
|
end
|
|
29
43
|
|
|
@@ -4,9 +4,9 @@ class Shrine
|
|
|
4
4
|
module Plugins
|
|
5
5
|
# Documentation can be found on https://shrinerb.com/docs/plugins/url_options
|
|
6
6
|
module UrlOptions
|
|
7
|
-
def self.configure(uploader,
|
|
7
|
+
def self.configure(uploader, options = {})
|
|
8
8
|
uploader.opts[:url_options] ||= {}
|
|
9
|
-
uploader.opts[:url_options].merge!(
|
|
9
|
+
uploader.opts[:url_options].merge!(options)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
module FileMethods
|
|
@@ -19,10 +19,24 @@ class Shrine
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
21
|
def url_options(options)
|
|
22
|
-
default_options =
|
|
22
|
+
default_options = find_url_options
|
|
23
23
|
default_options = default_options.call(self, options) if default_options.respond_to?(:call)
|
|
24
24
|
default_options || {}
|
|
25
25
|
end
|
|
26
|
+
|
|
27
|
+
# Matches the storage key exactly first, then falls back to any
|
|
28
|
+
# registered regex that matches the storage key. The regex form is
|
|
29
|
+
# useful when storage keys are generated dynamically (e.g. via the
|
|
30
|
+
# `dynamic_storage` plugin), since it's not possible to list every
|
|
31
|
+
# storage key upfront.
|
|
32
|
+
def find_url_options
|
|
33
|
+
url_options = shrine_class.opts[:url_options]
|
|
34
|
+
|
|
35
|
+
return url_options[storage_key] if url_options.key?(storage_key)
|
|
36
|
+
|
|
37
|
+
_, options = url_options.find { |key, _| key.is_a?(Regexp) && key.match?(storage_key.to_s) }
|
|
38
|
+
options
|
|
39
|
+
end
|
|
26
40
|
end
|
|
27
41
|
end
|
|
28
42
|
|
|
@@ -113,9 +113,18 @@ class Shrine
|
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
-
# Returns the full path to the file.
|
|
116
|
+
# Returns the full path to the file. Raises Shrine::Error if the id would
|
|
117
|
+
# resolve to a location outside of the storage #directory (e.g. an id
|
|
118
|
+
# containing `../` path traversal sequences in attacker-controlled data).
|
|
117
119
|
def path(id)
|
|
118
|
-
directory.join(id.gsub("/", File::SEPARATOR))
|
|
120
|
+
path = directory.join(id.gsub("/", File::SEPARATOR))
|
|
121
|
+
expanded = path.expand_path
|
|
122
|
+
|
|
123
|
+
unless expanded == directory || expanded.to_s.start_with?("#{directory}#{File::SEPARATOR}")
|
|
124
|
+
raise Shrine::Error, "path #{id.inspect} resolves outside of the storage directory"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
path
|
|
119
128
|
end
|
|
120
129
|
|
|
121
130
|
protected
|
data/lib/shrine/storage/s3.rb
CHANGED
|
@@ -230,20 +230,15 @@ class Shrine
|
|
|
230
230
|
|
|
231
231
|
private
|
|
232
232
|
|
|
233
|
-
#
|
|
234
|
-
# Uses @transfer_manager, if defined, for any size upload.
|
|
235
|
-
# Falls back to the original code using the older, now depricated
|
|
236
|
-
# AWS APIs for users of older version of the AWS Gem.
|
|
237
|
-
# for multipart uploads of large files.
|
|
233
|
+
# Uploads the file to S3. Uses multipart upload for large files.
|
|
238
234
|
def put(io, id, **)
|
|
239
|
-
if @
|
|
235
|
+
if io.respond_to?(:size) && io.size && io.size <= @multipart_threshold[:upload]
|
|
236
|
+
object(id).put(body: io, **)
|
|
237
|
+
elsif @transfer_manager # multipart upload - transfer manager
|
|
240
238
|
@transfer_manager.upload_stream(bucket: bucket.name, key: object_key(id), part_size: part_size(io), **) do |write_stream|
|
|
241
239
|
IO.copy_stream(io, write_stream)
|
|
242
240
|
end
|
|
243
|
-
|
|
244
|
-
object(id).put(body: io, **)
|
|
245
|
-
else
|
|
246
|
-
# multipart upload old API
|
|
241
|
+
else # multipart upload - before transfer manager
|
|
247
242
|
object(id).upload_stream(part_size: part_size(io), **) do |write_stream|
|
|
248
243
|
IO.copy_stream(io, write_stream)
|
|
249
244
|
end
|
data/lib/shrine/version.rb
CHANGED
data/lib/shrine.rb
CHANGED
|
@@ -28,6 +28,10 @@ class Shrine
|
|
|
28
28
|
class FileNotFound < Error
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Raised by Shrine.find_storage when the storage isn't registered.
|
|
32
|
+
class MissingStorage < Error
|
|
33
|
+
end
|
|
34
|
+
|
|
31
35
|
@opts = {}
|
|
32
36
|
@storages = {}
|
|
33
37
|
@logger = Logger.new(STDOUT)
|
|
@@ -86,7 +90,7 @@ class Shrine
|
|
|
86
90
|
# Retrieves the storage under the given identifier (can be a Symbol or
|
|
87
91
|
# a String), raising Shrine::Error if the storage is missing.
|
|
88
92
|
def find_storage(name)
|
|
89
|
-
storages[name.to_sym] || storages[name.to_s] or fail
|
|
93
|
+
storages[name.to_sym] || storages[name.to_s] or fail MissingStorage, "storage #{name.inspect} isn't registered on #{self}"
|
|
90
94
|
end
|
|
91
95
|
|
|
92
96
|
# Generates an instance of Shrine::Attachment to be included in the
|
data/shrine.gemspec
CHANGED
|
@@ -36,6 +36,8 @@ direct uploads for fully asynchronous user experience.
|
|
|
36
36
|
gem.add_dependency "down", "~> 5.1"
|
|
37
37
|
gem.add_dependency "content_disposition", "~> 1.0"
|
|
38
38
|
|
|
39
|
+
gem.add_development_dependency "rdoc", "~> 8.0" unless RUBY_ENGINE == "jruby"
|
|
40
|
+
|
|
39
41
|
# general testing helpers
|
|
40
42
|
gem.add_development_dependency "rake", ">= 11.1"
|
|
41
43
|
gem.add_development_dependency "minitest", "~> 6.0"
|
|
@@ -67,10 +69,10 @@ direct uploads for fully asynchronous user experience.
|
|
|
67
69
|
|
|
68
70
|
# for instrumentation plugin
|
|
69
71
|
gem.add_development_dependency "dry-monitor"
|
|
70
|
-
gem.add_development_dependency "activesupport", RUBY_ENGINE == "jruby" ? "~>
|
|
72
|
+
gem.add_development_dependency "activesupport", RUBY_ENGINE == "jruby" ? "~> 8.0.0" : "~> 8.1"
|
|
71
73
|
|
|
72
74
|
# for ORM plugins
|
|
73
75
|
gem.add_development_dependency "sequel"
|
|
74
|
-
gem.add_development_dependency "activerecord", RUBY_ENGINE == "jruby" ? "~>
|
|
76
|
+
gem.add_development_dependency "activerecord", RUBY_ENGINE == "jruby" ? "~> 8.0.0" : "~> 8.1"
|
|
75
77
|
gem.add_development_dependency "sqlite3", "~> 2.1" unless RUBY_ENGINE == "jruby"
|
|
76
78
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: shrine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Janko Marohnić
|
|
@@ -37,6 +37,20 @@ dependencies:
|
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '1.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rdoc
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '8.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '8.0'
|
|
40
54
|
- !ruby/object:Gem::Dependency
|
|
41
55
|
name: rake
|
|
42
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -483,6 +497,8 @@ files:
|
|
|
483
497
|
- doc/release_notes/3.6.0.md
|
|
484
498
|
- doc/release_notes/3.7.0.md
|
|
485
499
|
- doc/release_notes/3.7.1.md
|
|
500
|
+
- doc/release_notes/3.8.0.md
|
|
501
|
+
- doc/release_notes/3.9.0.md
|
|
486
502
|
- doc/retrieving_uploads.md
|
|
487
503
|
- doc/securing_uploads.md
|
|
488
504
|
- doc/storage/file_system.md
|
|
@@ -576,7 +592,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
576
592
|
- !ruby/object:Gem::Version
|
|
577
593
|
version: '0'
|
|
578
594
|
requirements: []
|
|
579
|
-
rubygems_version:
|
|
595
|
+
rubygems_version: 4.0.13
|
|
580
596
|
specification_version: 4
|
|
581
597
|
summary: Toolkit for file attachments in Ruby applications
|
|
582
598
|
test_files: []
|