shrine 3.9.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: fc3e1cb064bc4b1fc0132c5d97c244506b65014eac06b0e8181cf44b2319bf67
4
- data.tar.gz: c289af323f41d3cd70dbe740439c9a08f595bba81150a396639cecd92b8b53af
3
+ metadata.gz: 5601661d53737bf3290ec5897dc9d33cfe5edac60a7468e67de7191649671a14
4
+ data.tar.gz: bf2a26e2162a0c1805f2e8d174128e8ee5008060fdbbb9b8e358d41bfdf680a2
5
5
  SHA512:
6
- metadata.gz: 128cb224b8199aba9b4a24a0b911c90bc88e404fc9746b440b3edafe19b28422506edf67f4c01474701a9d9a56d5bc4a874152741239a8ff05bc1fa6449a6fb4
7
- data.tar.gz: 2582f06b0c087d56cea3e9056bd0763a8d50613dc86bc2707593a3322da0d4be018a507189ca96b63f70715309d8f469f3bc536fcf4c5b3ab859d54697e9910b
6
+ metadata.gz: 7eb43ca8a78d4dedec0d24422ca4bc9f62eb6e454aa61ac7871760c4c398973a9787591295c39799f6b3250d4b3af9e80f7082c659fb6ed67435d775846ca5e2
7
+ data.tar.gz: 636a7343486f6baaa3fa841abcef8439eb0df5d7183d6ac39a756a508de544dd356bca2562a9e01ac76b871a29455d2a40601f8086e8050dcc793bd57fc568e0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
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
+
1
9
  ## 3.9.0 (2026-07-13)
2
10
 
3
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)
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/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
@@ -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
+ ```
@@ -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
@@ -7,7 +7,7 @@ class Shrine
7
7
 
8
8
  module VERSION
9
9
  MAJOR = 3
10
- MINOR = 9
10
+ MINOR = 10
11
11
  TINY = 0
12
12
  PRE = nil
13
13
 
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.0
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,7 @@ 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
491
493
  - doc/release_notes/3.2.0.md
492
494
  - doc/release_notes/3.2.1.md
493
495
  - doc/release_notes/3.2.2.md
@@ -530,6 +532,7 @@ files:
530
532
  - lib/shrine/plugins/download_endpoint.rb
531
533
  - lib/shrine/plugins/dynamic_storage.rb
532
534
  - lib/shrine/plugins/entity.rb
535
+ - lib/shrine/plugins/fallback_storage.rb
533
536
  - lib/shrine/plugins/form_assign.rb
534
537
  - lib/shrine/plugins/included.rb
535
538
  - lib/shrine/plugins/infer_extension.rb