shrine 3.9.0 → 3.10.1

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: fc3e1cb064bc4b1fc0132c5d97c244506b65014eac06b0e8181cf44b2319bf67
4
- data.tar.gz: c289af323f41d3cd70dbe740439c9a08f595bba81150a396639cecd92b8b53af
3
+ metadata.gz: '078c4711b7bd211c577666899edb233967df970a48b96d3b6239189f9c516586'
4
+ data.tar.gz: a46ed9608642d54c84fd80cc106e56655b4bff7439ff3cf2627bd1c199b40a15
5
5
  SHA512:
6
- metadata.gz: 128cb224b8199aba9b4a24a0b911c90bc88e404fc9746b440b3edafe19b28422506edf67f4c01474701a9d9a56d5bc4a874152741239a8ff05bc1fa6449a6fb4
7
- data.tar.gz: 2582f06b0c087d56cea3e9056bd0763a8d50613dc86bc2707593a3322da0d4be018a507189ca96b63f70715309d8f469f3bc536fcf4c5b3ab859d54697e9910b
6
+ metadata.gz: 9526af08df019ad15b330e804d2136d0a2f876657b55018b3498ec5fd9c75b881ee1d7559fb4bd862512975d0a7439c4991d11dedf61b9c2bcc6d8c1c3ef38b6
7
+ data.tar.gz: af84847b2664f400250142bda5995b31d9303de422d5e73d75593ec2d9911f1cdc9e6dc27f10a3f97ab97aaefcb9ebf3f5754a35a6220501af2230dc7f7dcd6b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 3.10.1 (2026-10-10)
2
+
3
+ * `model` – Make `Attacher#write` a no-op when the attacher isn't backed by a model, fixing `remove_invalid` raising `NoMethodError` on attachers without a record (@janko)
4
+
5
+ ## 3.10.0 (2026-09-20)
6
+
7
+ * Add `fallback_storage` plugin for reading files from a secondary storage when they're missing in the primary storage (@yegorov, @janko)
8
+
9
+ * `data_uri` – Add `:max_size` option for limiting the size of the data URI content, checked before the content is decoded (@jalrax, @janko)
10
+
11
+ * `data_uri` – Pass the error to the `:error_message` proc as an optional second argument (@jalrax, @janko)
12
+
1
13
  ## 3.9.0 (2026-07-13)
2
14
 
3
15
  * `Shrine.find_storage` now raises `Shrine::MissingStorage` (subclass of `Shrine::Error`) when the storage isn't registered, so it can be rescued separately (@janko)
data/doc/advantages.md CHANGED
@@ -389,19 +389,19 @@ and Active Storage.
389
389
 
390
390
  Uppy supports direct uploads to [AWS S3][Uppy AwsS3] or to a [custom
391
391
  endpoint][Uppy XHRUpload]. It also supports **resumable** uploads, either
392
- [directly to S3][Uppy AwsS3Multipart] or via the [tus protocol][tus]. For the
392
+ [directly to S3][Uppy AwsS3 multipart] or via the [tus protocol][tus]. For the
393
393
  UI you can choose from various components, ranging from a simple [status
394
394
  bar][Uppy StatusBar] to a full-featured [dashboard][Uppy Dashboard].
395
395
 
396
396
  Shrine provides server side components for each type of upload. They are built
397
397
  on top of Rack, so that they can be used with any Ruby web framework.
398
398
 
399
- | Uppy | Shrine |
400
- | :--- | :----- |
401
- | [XHRUpload][Uppy XHRUpload] | [`upload_endpoint`][upload_endpoint] |
402
- | [AwsS3][Uppy AwsS3] | [`presign_endpoint`][presign_endpoint] |
403
- | [AwsS3Multipart][Uppy AwsS3Multipart] | [`uppy-s3_multipart`][uppy-s3_multipart] |
404
- | [Tus][Uppy Tus] | [`tus-ruby-server`][tus-ruby-server] |
399
+ | Uppy | Shrine |
400
+ | :--- | :----- |
401
+ | [XHRUpload][Uppy XHRUpload] | [`upload_endpoint`][upload_endpoint] |
402
+ | [AwsS3][Uppy AwsS3] | [`presign_endpoint`][presign_endpoint] |
403
+ | [AwsS3 (multipart)][Uppy AwsS3 multipart] | [`uppy-s3_multipart`][uppy-s3_multipart] |
404
+ | [Tus][Uppy Tus] | [`tus-ruby-server`][tus-ruby-server] |
405
405
 
406
406
  [Rack]: https://rack.github.io
407
407
  [Sinatra]: http://sinatrarb.com
@@ -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
@@ -440,10 +440,10 @@ on top of Rack, so that they can be used with any Ruby web framework.
440
440
  [Uppy XHRUpload]: https://uppy.io/docs/xhr-upload/
441
441
  [Uppy AwsS3]: https://uppy.io/docs/aws-s3/
442
442
  [Uppy Tus]: https://uppy.io/docs/tus/
443
- [Uppy AwsS3Multipart]: https://uppy.io/docs/aws-s3-multipart/
443
+ [Uppy AwsS3 multipart]: https://uppy.io/docs/aws-s3/#shouldusemultipartfile
444
444
  [tus]: https://tus.io
445
- [Uppy StatusBar]: https://uppy.io/examples/statusbar/
446
- [Uppy Dashboard]: https://uppy.io/examples/dashboard/
445
+ [Uppy StatusBar]: https://uppy.io/docs/status-bar/
446
+ [Uppy Dashboard]: https://uppy.io/docs/dashboard/
447
447
  [tus-ruby-server]: https://github.com/janko/tus-ruby-server
448
448
  [uppy-s3_multipart]: https://github.com/janko/uppy-s3_multipart
449
449
  [backgrounding]: https://shrinerb.com/docs/plugins/backgrounding
data/doc/design.md CHANGED
@@ -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
data/doc/direct_s3.md CHANGED
@@ -174,11 +174,21 @@ and request headers.
174
174
  }
175
175
  ```
176
176
 
177
- Uppy's [AWS S3][uppy aws-s3] plugin would then make a request to this endpoint
178
- and use these parameters to upload the file directly to S3. Once the file has
179
- been uploaded, you can generate a JSON representation of the uploaded file on
180
- the client side, and write it to the hidden attachment field (or send it
181
- directly in an AJAX request).
177
+ Uppy's [AWS S3][uppy aws-s3] plugin in Companion mode will then fetch these
178
+ parameters from `<companionEndpoint>/s3/params` and use them to upload the file
179
+ directly to S3:
180
+
181
+ ```js
182
+ uppy.use(AwsS3, {
183
+ companionEndpoint: '', // relative to the current host
184
+ shouldUseMultipart: false, // Shrine's presign endpoint only signs single-request uploads
185
+ })
186
+ ```
187
+
188
+ Once the file has been uploaded, you can generate a JSON representation of the
189
+ uploaded file on the client side (the S3 object key is available as
190
+ `response.body.key` in the `upload-success` event), and write it to the hidden
191
+ attachment field (or send it directly in an AJAX request).
182
192
 
183
193
  ```rb
184
194
  {
@@ -203,8 +213,8 @@ as the [Roda][roda demo] / [Rails][rails demo] demo app for a complete example
203
213
  of multiple direct S3 uploads.
204
214
 
205
215
  Also, if you're dealing with larger files, you may want to make the uploads
206
- resumable by using the [AWS S3 Multipart][uppy aws-s3-multipart] Uppy plugin
207
- instead, with the [uppy-s3_multipart] gem on the backend. Your back-end
216
+ resumable by using Uppy's [AWS S3][uppy aws-s3-multipart] plugin in multipart
217
+ mode instead, with the [uppy-s3_multipart] gem on the backend. Your back-end
208
218
  implementation is similar, just using `Shrine.uppy_s3_multipart` in place of
209
219
  `Shrine.presign_endpoint`. Instructions can be found in the [gem
210
220
  docs][uppy-s3_multipart shrine].
@@ -396,8 +406,8 @@ setup] guide.
396
406
  [rails demo]: https://github.com/erikdahlstrand/shrine-rails-example
397
407
  [Uppy]: https://uppy.io
398
408
  [uppy aws-s3]: https://uppy.io/docs/aws-s3/
399
- [uppy aws-s3 cors]: https://uppy.io/docs/aws-s3/#S3-Bucket-configuration
400
- [uppy aws-s3-multipart]: https://uppy.io/docs/aws-s3/
409
+ [uppy aws-s3 cors]: https://uppy.io/docs/aws-s3/#bucket-and-cors-setup
410
+ [uppy aws-s3-multipart]: https://uppy.io/docs/aws-s3/#shouldusemultipartfile
401
411
  [uppy-s3_multipart]: https://github.com/janko/uppy-s3_multipart
402
412
  [uppy-s3_multipart shrine]: https://github.com/janko/uppy-s3_multipart#shrine
403
413
  [Amazon S3 Data Consistency Model]: http://docs.aws.amazon.com/AmazonS3/latest/dev/Introduction.html#ConsistencyMode
@@ -50,4 +50,4 @@ title: Extensions
50
50
  | [faster_s3_url](https://github.com/jrochkind/faster_s3_url) | Optimized generation of public and presigned AWS S3 GET URLs |
51
51
  | [imgproxy](https://github.com/imgproxy/imgproxy.rb) | Integration for [imgproxy](https://github.com/imgproxy/imgproxy) |
52
52
  | [rails_admin](https://github.com/sferik/rails_admin) | Integration for [RailsAdmin](https://github.com/sferik/rails_admin) |
53
- | [uppy-s3_multipart](https://github.com/janko/uppy-s3_multipart) | Integration for [Uppy AWS S3 Multipart](https://uppy.io/docs/aws-s3-multipart/) |
53
+ | [uppy-s3_multipart](https://github.com/janko/uppy-s3_multipart) | Integration for [Uppy AWS S3](https://uppy.io/docs/aws-s3/) multipart uploads |
@@ -896,13 +896,13 @@ on slow and flaky internet connections.
896
896
 
897
897
  #### Uppy S3 Multipart
898
898
 
899
- You can achieve resumable uploads directly to S3 with the [AWS S3
900
- Multipart][uppy aws-s3-multipart] Uppy plugin, accompanied with
899
+ You can achieve resumable uploads directly to S3 with Uppy's [AWS S3][uppy
900
+ aws-s3-multipart] plugin in multipart mode, accompanied with the
901
901
  `uppy_s3_multipart` Shrine plugin provided by the [uppy-s3_multipart] gem.
902
902
 
903
903
  ```rb
904
904
  # Gemfile
905
- gem "uppy-s3_multipart", "~> 0.3"
905
+ gem "uppy-s3_multipart", "~> 1.3"
906
906
  ```
907
907
  ```rb
908
908
  Shrine.plugin :uppy_s3_multipart
@@ -1108,7 +1108,7 @@ Shrine.logger.level = Logger::WARN
1108
1108
  [Uppy]: https://uppy.io
1109
1109
  [shrine-tus]: https://github.com/shrinerb/shrine-tus
1110
1110
  [tus]: https://tus.io
1111
- [uppy aws-s3-multipart]: https://uppy.io/docs/aws-s3-multipart/
1111
+ [uppy aws-s3-multipart]: https://uppy.io/docs/aws-s3/#shouldusemultipartfile
1112
1112
  [uppy aws-s3]: https://uppy.io/docs/aws-s3/
1113
1113
  [uppy tus]: https://uppy.io/docs/tus/
1114
1114
  [uppy xhr-upload]: https://uppy.io/docs/xhr-upload/
@@ -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
@@ -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
@@ -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,15 @@
1
+ ---
2
+ title: Shrine 3.10.1
3
+ ---
4
+
5
+ ## Bug fixes
6
+
7
+ * Since Shrine 3.9.0, the `remove_invalid` plugin would raise `NoMethodError` when validation failed on an attacher without a record and the `model` plugin was loaded (directly or through the `activerecord`/`sequel` plugins).
8
+
9
+ ```rb
10
+ attacher = Shrine::Attacher.new
11
+ attacher.assign(invalid_file)
12
+ # NoMethodError: undefined method '=' for nil
13
+ ```
14
+
15
+ `Attacher#write` from the `model` plugin is now a no-op when the attacher isn't backed by a model, so the invalid file is removed and validation errors are kept as before.
@@ -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
@@ -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
@@ -122,12 +122,15 @@ class Shrine
122
122
  # Writes uploaded file data into the model.
123
123
  def set(*)
124
124
  result = super
125
- write if model?
125
+ write
126
126
  result
127
127
  end
128
128
 
129
- # Writes the attachment data into the model attribute.
129
+ # Writes the attachment data into the model attribute. Does nothing
130
+ # when the attacher isn't backed by a model instance.
130
131
  def write
132
+ return unless model?
133
+
131
134
  column_values.each do |name, value|
132
135
  write_attribute(name, value)
133
136
  end
@@ -7,8 +7,8 @@ class Shrine
7
7
 
8
8
  module VERSION
9
9
  MAJOR = 3
10
- MINOR = 9
11
- TINY = 0
10
+ MINOR = 10
11
+ TINY = 1
12
12
  PRE = nil
13
13
 
14
14
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
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.9.0
4
+ version: 3.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janko Marohnić
@@ -417,6 +417,7 @@ files:
417
417
  - doc/plugins/download_endpoint.md
418
418
  - doc/plugins/dynamic_storage.md
419
419
  - doc/plugins/entity.md
420
+ - doc/plugins/fallback_storage.md
420
421
  - doc/plugins/form_assign.md
421
422
  - doc/plugins/included.md
422
423
  - doc/plugins/infer_extension.md
@@ -488,6 +489,8 @@ files:
488
489
  - doc/release_notes/3.0.0.md
489
490
  - doc/release_notes/3.0.1.md
490
491
  - doc/release_notes/3.1.0.md
492
+ - doc/release_notes/3.10.0.md
493
+ - doc/release_notes/3.10.1.md
491
494
  - doc/release_notes/3.2.0.md
492
495
  - doc/release_notes/3.2.1.md
493
496
  - doc/release_notes/3.2.2.md
@@ -530,6 +533,7 @@ files:
530
533
  - lib/shrine/plugins/download_endpoint.rb
531
534
  - lib/shrine/plugins/dynamic_storage.rb
532
535
  - lib/shrine/plugins/entity.rb
536
+ - lib/shrine/plugins/fallback_storage.rb
533
537
  - lib/shrine/plugins/form_assign.rb
534
538
  - lib/shrine/plugins/included.rb
535
539
  - lib/shrine/plugins/infer_extension.rb