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 +4 -4
- data/CHANGELOG.md +8 -0
- data/doc/advantages.md +1 -1
- data/doc/design.md +1 -1
- data/doc/plugins/data_uri.md +15 -0
- data/doc/plugins/fallback_storage.md +90 -0
- data/doc/release_notes/3.10.0.md +30 -0
- data/lib/shrine/plugins/data_uri.rb +27 -2
- data/lib/shrine/plugins/fallback_storage.rb +51 -0
- data/lib/shrine/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5601661d53737bf3290ec5897dc9d33cfe5edac60a7468e67de7191649671a14
|
|
4
|
+
data.tar.gz: bf2a26e2162a0c1805f2e8d174128e8ee5008060fdbbb9b8e358d41bfdf680a2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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://
|
|
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://
|
|
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/plugins/data_uri.md
CHANGED
|
@@ -37,8 +37,23 @@ attachment column. You can change the default error message:
|
|
|
37
37
|
```rb
|
|
38
38
|
plugin :data_uri, error_message: "data URI was invalid"
|
|
39
39
|
plugin :data_uri, error_message: ->(uri) { I18n.t("errors.data_uri_invalid") }
|
|
40
|
+
plugin :data_uri, error_message: ->(uri, error) { I18n.t("errors.data_uri.#{error.message}") }
|
|
40
41
|
```
|
|
41
42
|
|
|
43
|
+
## Maximum size
|
|
44
|
+
|
|
45
|
+
It's a good practice to limit the maximum size of the data URI content:
|
|
46
|
+
|
|
47
|
+
```rb
|
|
48
|
+
plugin :data_uri, max_size: 10*1024*1024 # 10 MB
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Now if content bigger than 10MB is assigned, parsing will fail the same way as
|
|
52
|
+
for an invalid data URI. The size is calculated from the data URI before the
|
|
53
|
+
content is decoded, so oversized content never gets loaded into memory. It's
|
|
54
|
+
exact for base64 content, while for percent-encoded content the encoded length
|
|
55
|
+
is used, which can overestimate.
|
|
56
|
+
|
|
42
57
|
## Uploader options
|
|
43
58
|
|
|
44
59
|
Any options passed to `Attacher#assign_data_uri` will be forwarded to
|
|
@@ -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
|
-
#
|
|
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
|
data/lib/shrine/version.rb
CHANGED
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.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
|