shrine 3.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55c3fc9bf7eb40700d2be0ee82900986c601b1b3449d5d5ee7b246546bfe5eb3
4
- data.tar.gz: 11ffe3b196b79946c931f5a0e9538bda74cc4abd74711124509c76a13a5eaddc
3
+ metadata.gz: 5601661d53737bf3290ec5897dc9d33cfe5edac60a7468e67de7191649671a14
4
+ data.tar.gz: bf2a26e2162a0c1805f2e8d174128e8ee5008060fdbbb9b8e358d41bfdf680a2
5
5
  SHA512:
6
- metadata.gz: 19585a7a2c486d43fb9be2abc84123adced4d18beebf50919938a9c631d16c0c2ecb15e777397b5b8a1642911171d328c55bf86769b9a31a4a3957cf3d8256d9
7
- data.tar.gz: 1dfbb667941b5f2bfa1b035230f0b1fb809c59855fc253f8365394624cffd4e3078a0aa41188eb27c8e57be8d4243e45d5dcf56a83b4d9ea1900bf02fa340dc6
6
+ metadata.gz: 7eb43ca8a78d4dedec0d24422ca4bc9f62eb6e454aa61ac7871760c4c398973a9787591295c39799f6b3250d4b3af9e80f7082c659fb6ed67435d775846ca5e2
7
+ data.tar.gz: 636a7343486f6baaa3fa841abcef8439eb0df5d7183d6ac39a756a508de544dd356bca2562a9e01ac76b871a29455d2a40601f8086e8050dcc793bd57fc568e0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ ## 3.10.0 (2026-09-20)
2
+
3
+ * Add `fallback_storage` plugin for reading files from a secondary storage when they're missing in the primary storage (@yegorov, @janko)
4
+
5
+ * `data_uri` – Add `:max_size` option for limiting the size of the data URI content, checked before the content is decoded (@jalrax, @janko)
6
+
7
+ * `data_uri` – Pass the error to the `:error_message` proc as an optional second argument (@jalrax, @janko)
8
+
9
+ ## 3.9.0 (2026-07-13)
10
+
11
+ * `Shrine.find_storage` now raises `Shrine::MissingStorage` (subclass of `Shrine::Error`) when the storage isn't registered, so it can be rescued separately (@janko)
12
+
13
+ * `url_options` – Allow regular expression storage keys for compatibility with `dynamic_storage` plugin (@janko)
14
+
15
+ * `upload_options` – Allow regular expression storage keys for compatibility with `dynamic_storage` plugin (@janko)
16
+
17
+ * `remove_invalid` – Fix model attribute not being cleared/reverted after deassigning invalid file (@janko)
18
+
19
+ * `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)
20
+
21
+ * `tempfile` – Allow `UploadedFile#tempfile` to be called without opening the uploaded file first (@janko)
22
+
23
+ * `activerecord` – Add `:attribute_types` option for skipping serialization of JSON/JSONB attributes declared via the Attributes API, not just database columns (@vojtad, @janko)
24
+
1
25
  ## 3.8.0 (2026-06-24)
2
26
 
3
27
  * `s3` – Use single-request uploads for smaller files again instead of always using multipart uploads (@janko)
data/doc/advantages.md CHANGED
@@ -412,7 +412,7 @@ on top of Rack, so that they can be used with any Ruby web framework.
412
412
  [Sequel]: http://sequel.jeremyevans.net
413
413
  [ROM]: http://rom-rb.org
414
414
  [Hanami::Model]: https://github.com/hanami/model
415
- [plugin system]: https://twin.github.io/the-plugin-system-of-sequel-and-roda/
415
+ [plugin system]: https://janko.io/the-plugin-system-of-sequel-and-roda/
416
416
  [Down]: https://github.com/janko/down
417
417
  [ContentDisposition]: https://github.com/shrinerb/content_disposition
418
418
  [`file`]: http://linux.die.net/man/1/file
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`](#finalization) will
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::*`](#Storage) | Manages files on a particular storage service |
13
- | [`Shrine`](#Shrine) | Wraps uploads and handles loading plugins |
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`](#shrine-uploadedfile) classes.
63
+ [`Shrine::UploadedFile`](#shrineuploadedfile) classes.
64
64
 
65
65
  ## `Shrine`
66
66
 
@@ -264,7 +264,7 @@ When a persistence plugin is loaded ([`activerecord`][activerecord],
264
264
  [Using Attacher]: https://shrinerb.com/docs/attacher
265
265
  [Notes on study of shrine implementation]: https://bibwild.wordpress.com/2018/09/12/notes-on-study-of-shrine-implementation/
266
266
  [Creating a New Plugin]: https://shrinerb.com/docs/creating-plugins
267
- [Plugin system of Sequel and Roda]: https://twin.github.io/the-plugin-system-of-sequel-and-roda/
267
+ [Plugin system of Sequel and Roda]: https://janko.io/the-plugin-system-of-sequel-and-roda/
268
268
  [entity]: https://shrinerb.com/docs/plugins/entity
269
269
  [model]: https://shrinerb.com/docs/plugins/model
270
270
  [activerecord]: https://shrinerb.com/docs/plugins/activerecord
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 when called on an
13
- open uploaded file will return a copy of its content on disk. The first time
14
- the method is called the file content will cached into a temporary file and
15
- returned. On any subsequent method calls the cached temporary file will be
16
- returned directly. The temporary file is deleted when the uploaded file is
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
@@ -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
@@ -0,0 +1,30 @@
1
+ ---
2
+ title: Shrine 3.10.0
3
+ ---
4
+
5
+ ## New features
6
+
7
+ * The new `fallback_storage` plugin makes Shrine read from a secondary (fallback) storage when a file is missing in the primary storage. This is useful for staging or development environments that should read files from production without copying them. `UploadedFile#exists?`, `#url` and `#open` (and methods relying on it) fall back to the secondary storage, while uploads and deletes only affect the primary storage.
8
+
9
+ ```rb
10
+ Shrine.storages = {
11
+ cache: Shrine::Storage::S3.new(endpoint: "https://stage.example.com", prefix: "cache"),
12
+ store: Shrine::Storage::S3.new(endpoint: "https://stage.example.com"),
13
+ fallback: Shrine::Storage::S3.new(endpoint: "https://production.example.com"),
14
+ }
15
+
16
+ Shrine.plugin :fallback_storage # looks for the `:fallback` storage by default
17
+ Shrine.plugin :fallback_storage, store: :production_store # custom storage name
18
+ ```
19
+
20
+ * The `data_uri` plugin now accepts a `:max_size` option for limiting the size of the data URI content. The size is calculated from the data URI before the content is decoded, so oversized content never gets loaded into memory. It's exact for base64 content, while for percent-encoded content the encoded length is used, which can overestimate. Content over the limit fails parsing the same way as an invalid data URI.
21
+
22
+ ```rb
23
+ plugin :data_uri, max_size: 10*1024*1024 # 10 MB
24
+ ```
25
+
26
+ * The `data_uri` plugin's `:error_message` proc can now optionally accept the error as a second argument, so the size limit can have its own message.
27
+
28
+ ```rb
29
+ plugin :data_uri, error_message: ->(uri, error) { I18n.t("errors.data_uri.#{error.message}") }
30
+ ```
@@ -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.
@@ -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(...), metadata: { "foo" => "bar" })
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(...), metadata: { "foo" => "bar" })
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 column.
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
- column = record.class.columns_hash[attribute.to_s]
116
- column && [:json, :jsonb].include?(column.type)
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
 
@@ -47,7 +47,7 @@ class Shrine
47
47
  # attacher.promote_block do |attacher|
48
48
  # Attachment::PromoteJob.perform_async(
49
49
  # attacher.record,
50
- # attacher.name
50
+ # attacher.name,
51
51
  # attacher.file_data,
52
52
  # )
53
53
  # end
@@ -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)
@@ -17,6 +17,7 @@ class Shrine
17
17
  BASE64_REGEXP = /;base64/
18
18
  CONTENT_SEPARATOR = /,/
19
19
  DEFAULT_CONTENT_TYPE = "text/plain"
20
+ BASE64_ALPHABET = "A-Za-z0-9+/"
20
21
 
21
22
  LOG_SUBSCRIBER = -> (event) do
22
23
  Shrine.logger.info "Data URI (#{event.duration}ms) – #{{
@@ -78,13 +79,37 @@ class Shrine
78
79
  data_file
79
80
  end
80
81
 
81
- # Parses the data URI string and returns parts.
82
+ # Raises `ParseError` if the content exceeds the `:max_size` option.
83
+ def verify_data_size!(uri, offset, base64)
84
+ max_size = opts[:data_uri][:max_size]
85
+ return unless max_size
86
+
87
+ size = data_size(uri, offset, base64)
88
+
89
+ raise ParseError, "data URI is too large" if size > max_size
90
+ end
91
+
92
+ # Returns the size the content will have once decoded. It's exact for
93
+ # base64 and an upper bound for percent-encoded content.
94
+ def data_size(uri, offset, base64)
95
+ return uri.bytesize - offset unless base64
96
+
97
+ header_characters = uri.byteslice(0, offset).count(BASE64_ALPHABET)
98
+ content_characters = uri.count(BASE64_ALPHABET) - header_characters
99
+
100
+ content_characters * 3 / 4 # 4 characters decode into 3 bytes
101
+ end
102
+
103
+ # Parses the data URI string, verifies content size, and returns parts.
82
104
  def parse_data_uri(uri)
83
105
  scanner = StringScanner.new(uri)
84
106
  scanner.scan(DATA_REGEXP) or raise ParseError, "data URI has invalid format"
85
107
  media_type = scanner.scan(MEDIA_TYPE_REGEXP)
86
108
  base64 = scanner.scan(BASE64_REGEXP)
87
109
  scanner.scan(CONTENT_SEPARATOR) or raise ParseError, "data URI has invalid format"
110
+
111
+ verify_data_size!(scanner.string, scanner.pos, base64)
112
+
88
113
  content = scanner.post_match
89
114
 
90
115
  { content_type: media_type, base64: !!base64, data: content }
@@ -129,7 +154,7 @@ class Shrine
129
154
  # Generates an error message for failed data URI parse.
130
155
  def data_uri_error_messsage(uri, error)
131
156
  message = shrine_class.opts[:data_uri][:error_message]
132
- message = message.call(uri) if message.respond_to?(:call)
157
+ message = message.call(*[uri, error].take(message.arity.abs)) if message.respond_to?(:call)
133
158
  message || error.message
134
159
  end
135
160
  end
@@ -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) { |path, file| ... }
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
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Shrine
4
+ module Plugins
5
+ # Documentation can be found on https://shrinerb.com/docs/plugins/fallback_storage
6
+ module FallbackStorage
7
+ def self.configure(uploader, **opts)
8
+ uploader.opts[:fallback_storage] ||= { store: :fallback }
9
+ uploader.opts[:fallback_storage].merge!(opts)
10
+
11
+ uploader.find_storage(uploader.opts.dig(:fallback_storage, :store))
12
+ end
13
+
14
+ module FileMethods
15
+ def self.included(base)
16
+ base.class_eval do
17
+ alias_method :storage_exists?, :exists?
18
+
19
+ def exists?
20
+ super || fallback_storage.exists?(id)
21
+ end
22
+ end
23
+ end
24
+
25
+ def url(**)
26
+ if storage_exists?
27
+ super
28
+ else
29
+ fallback_storage.url(id, **)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def _open(**)
36
+ if storage_exists?
37
+ super
38
+ else
39
+ fallback_storage.open(id, **)
40
+ end
41
+ end
42
+
43
+ def fallback_storage
44
+ shrine_class.find_storage(shrine_class.opts.dig(:fallback_storage, :store))
45
+ end
46
+ end
47
+ end
48
+
49
+ register_plugin(:fallback_storage, FallbackStorage)
50
+ end
51
+ end
@@ -26,6 +26,10 @@ class Shrine
26
26
  else
27
27
  load_data nil
28
28
  end
29
+
30
+ # `load_data` bypasses `Attacher#set`, so when the model plugin is
31
+ # loaded the record attribute needs to be synced manually.
32
+ write if respond_to?(:write)
29
33
  end
30
34
  end
31
35
  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
 
@@ -19,8 +19,6 @@ class Shrine
19
19
 
20
20
  module FileMethods
21
21
  def tempfile
22
- raise Error, "uploaded file must be opened" unless @io
23
-
24
22
  @tempfile ||= download
25
23
  @tempfile.rewind
26
24
  @tempfile
@@ -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, **opts)
7
+ def self.configure(uploader, options = {})
8
8
  uploader.opts[:upload_options] ||= {}
9
- uploader.opts[:upload_options].merge!(opts)
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 = opts[:upload_options][storage_key] || {}
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, **opts)
7
+ def self.configure(uploader, options = {})
8
8
  uploader.opts[:url_options] ||= {}
9
- uploader.opts[:url_options].merge!(opts)
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 = shrine_class.opts[:url_options][storage_key]
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
 
@@ -7,7 +7,7 @@ class Shrine
7
7
 
8
8
  module VERSION
9
9
  MAJOR = 3
10
- MINOR = 8
10
+ MINOR = 10
11
11
  TINY = 0
12
12
  PRE = nil
13
13
 
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 Error, "storage #{name.inspect} isn't registered on #{self}"
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"
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.8.0
4
+ version: 3.10.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
@@ -155,6 +169,20 @@ dependencies:
155
169
  - - ">="
156
170
  - !ruby/object:Gem::Version
157
171
  version: '0'
172
+ - !ruby/object:Gem::Dependency
173
+ name: ruby-filemagic
174
+ requirement: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - "~>"
177
+ - !ruby/object:Gem::Version
178
+ version: '0.7'
179
+ type: :development
180
+ prerelease: false
181
+ version_requirements: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - "~>"
184
+ - !ruby/object:Gem::Version
185
+ version: '0.7'
158
186
  - !ruby/object:Gem::Dependency
159
187
  name: mime-types
160
188
  requirement: !ruby/object:Gem::Requirement
@@ -287,14 +315,14 @@ dependencies:
287
315
  requirements:
288
316
  - - "~>"
289
317
  - !ruby/object:Gem::Version
290
- version: 8.0.0
318
+ version: '8.1'
291
319
  type: :development
292
320
  prerelease: false
293
321
  version_requirements: !ruby/object:Gem::Requirement
294
322
  requirements:
295
323
  - - "~>"
296
324
  - !ruby/object:Gem::Version
297
- version: 8.0.0
325
+ version: '8.1'
298
326
  - !ruby/object:Gem::Dependency
299
327
  name: sequel
300
328
  requirement: !ruby/object:Gem::Requirement
@@ -315,14 +343,28 @@ dependencies:
315
343
  requirements:
316
344
  - - "~>"
317
345
  - !ruby/object:Gem::Version
318
- version: 8.0.0
346
+ version: '8.1'
319
347
  type: :development
320
348
  prerelease: false
321
349
  version_requirements: !ruby/object:Gem::Requirement
322
350
  requirements:
323
351
  - - "~>"
324
352
  - !ruby/object:Gem::Version
325
- version: 8.0.0
353
+ version: '8.1'
354
+ - !ruby/object:Gem::Dependency
355
+ name: sqlite3
356
+ requirement: !ruby/object:Gem::Requirement
357
+ requirements:
358
+ - - "~>"
359
+ - !ruby/object:Gem::Version
360
+ version: '2.1'
361
+ type: :development
362
+ prerelease: false
363
+ version_requirements: !ruby/object:Gem::Requirement
364
+ requirements:
365
+ - - "~>"
366
+ - !ruby/object:Gem::Version
367
+ version: '2.1'
326
368
  description: |
327
369
  Shrine is a toolkit for file attachments in Ruby applications. It supports
328
370
  uploading, downloading, processing and deleting IO objects, backed by various
@@ -375,6 +417,7 @@ files:
375
417
  - doc/plugins/download_endpoint.md
376
418
  - doc/plugins/dynamic_storage.md
377
419
  - doc/plugins/entity.md
420
+ - doc/plugins/fallback_storage.md
378
421
  - doc/plugins/form_assign.md
379
422
  - doc/plugins/included.md
380
423
  - doc/plugins/infer_extension.md
@@ -446,6 +489,7 @@ files:
446
489
  - doc/release_notes/3.0.0.md
447
490
  - doc/release_notes/3.0.1.md
448
491
  - doc/release_notes/3.1.0.md
492
+ - doc/release_notes/3.10.0.md
449
493
  - doc/release_notes/3.2.0.md
450
494
  - doc/release_notes/3.2.1.md
451
495
  - doc/release_notes/3.2.2.md
@@ -456,6 +500,7 @@ files:
456
500
  - doc/release_notes/3.7.0.md
457
501
  - doc/release_notes/3.7.1.md
458
502
  - doc/release_notes/3.8.0.md
503
+ - doc/release_notes/3.9.0.md
459
504
  - doc/retrieving_uploads.md
460
505
  - doc/securing_uploads.md
461
506
  - doc/storage/file_system.md
@@ -487,6 +532,7 @@ files:
487
532
  - lib/shrine/plugins/download_endpoint.rb
488
533
  - lib/shrine/plugins/dynamic_storage.rb
489
534
  - lib/shrine/plugins/entity.rb
535
+ - lib/shrine/plugins/fallback_storage.rb
490
536
  - lib/shrine/plugins/form_assign.rb
491
537
  - lib/shrine/plugins/included.rb
492
538
  - lib/shrine/plugins/infer_extension.rb
@@ -549,7 +595,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
549
595
  - !ruby/object:Gem::Version
550
596
  version: '0'
551
597
  requirements: []
552
- rubygems_version: 4.0.3
598
+ rubygems_version: 4.0.13
553
599
  specification_version: 4
554
600
  summary: Toolkit for file attachments in Ruby applications
555
601
  test_files: []