shrine 3.5.0 → 3.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/doc/release_notes/3.6.0.md +23 -0
- data/doc/storage/s3.md +10 -0
- data/lib/shrine/attacher.rb +7 -0
- data/lib/shrine/plugins/derivation_endpoint.rb +12 -9
- data/lib/shrine/plugins/download_endpoint.rb +3 -1
- data/lib/shrine/plugins/presign_endpoint.rb +9 -6
- data/lib/shrine/plugins/rack_response.rb +6 -0
- data/lib/shrine/plugins/upload_endpoint.rb +3 -1
- data/lib/shrine/storage/s3.rb +10 -3
- data/lib/shrine/uploaded_file.rb +1 -0
- data/lib/shrine/version.rb +1 -1
- data/shrine.gemspec +3 -3
- metadata +19 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a5fadb4e660b7638e1d17b81ecb1761e8dd5faa675f15a3b30de0c549bafd653
|
4
|
+
data.tar.gz: a9ee826b483ab474f55ac66bc35e95a4cb8496cada12ce7595091210670f1f38
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2c0f867b7215189135563524f9e7d8cf28d8be1a5b298bc173bbe1af9fb3ced15f98dd198c8cb13bfa87abdf3eb29566083c6ebc1de0955713da6c165562c41d
|
7
|
+
data.tar.gz: 47c2f75d9a831fdc1d731086fbeeb5a6885a1845eac375a5ba2ce9b5354654f49931e6b26f2732d98edfa39cbfcf3e587be63f01490910a06a7bad64cea8219e
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
## 3.6.0 (2024-04-29)
|
2
|
+
|
3
|
+
* Add Rack 3 support (@tomasc, @janko)
|
4
|
+
|
5
|
+
* Make a copy of attacher context hash when duplicating the attacher (@reidab)
|
6
|
+
|
7
|
+
* An uploaded file can be implicitly re-opened after it has been closed (@jrochkind)
|
8
|
+
|
9
|
+
* Add new `:copy_options` for initializing the S3 storage (@hkdahal)
|
10
|
+
|
1
11
|
## 3.5.0 (2023-07-06)
|
2
12
|
|
3
13
|
* Migrate website to Docusaurus v2 (@janko)
|
@@ -0,0 +1,23 @@
|
|
1
|
+
---
|
2
|
+
title: Shrine 3.6.0
|
3
|
+
---
|
4
|
+
|
5
|
+
## New features
|
6
|
+
|
7
|
+
* The S3 storage now accepts `:copy_options` when initializing. This can be used for supporting Cloudflare R2 by removing `:tagging_directive` when copying file from temporary to permanent storage.
|
8
|
+
|
9
|
+
```rb
|
10
|
+
Shrine::Storage::S3.new(bucket: BUCKET, copy_options: {}, **s3_options)
|
11
|
+
```
|
12
|
+
|
13
|
+
## Other improvements
|
14
|
+
|
15
|
+
* Rack 3 is now supported.
|
16
|
+
|
17
|
+
* When duplicating the attacher, the `Attacher#context` hash is now copied as well, instead of being kept the same between the two attachers.
|
18
|
+
|
19
|
+
* After `UploadedFile#close` was called, `UploadedFile#opened?` will return `false` and the uploaded file can be implicitly re-opened again.
|
20
|
+
|
21
|
+
## Backwards compatibility
|
22
|
+
|
23
|
+
* Shrine API that is returning a rack response triple will now return headers as an instance of `Rack::Headers` on Rack 3, which is a subclass of `Hash`. This should keep user code that references header names in mixed case working (in addition to lowercase), but could theoretically cause issues for code explicitly requiring headers to be an instance of `Hash`.
|
data/doc/storage/s3.md
CHANGED
@@ -124,6 +124,16 @@ uploader.upload(file, upload_options: { acl: "private" })
|
|
124
124
|
the uploader level won't be forwarded for generating presigns, since presigns
|
125
125
|
are generated using the storage directly.
|
126
126
|
|
127
|
+
## Copy options
|
128
|
+
|
129
|
+
If you wish to override options that are passed when copying objects from
|
130
|
+
temporary to permanent storage, you can pass `:copy_options`:
|
131
|
+
|
132
|
+
```rb
|
133
|
+
# Removes default :tagging_directive, which isn't supported by Cloudflare R2
|
134
|
+
Shrine::Storage::S3.new(copy_options: {}, **s3_options)
|
135
|
+
```
|
136
|
+
|
127
137
|
## URL Host
|
128
138
|
|
129
139
|
If you want your S3 object URLs to be generated with a different URL host (e.g.
|
data/lib/shrine/attacher.rb
CHANGED
@@ -341,6 +341,13 @@ class Shrine
|
|
341
341
|
|
342
342
|
private
|
343
343
|
|
344
|
+
# The copy constructor that's called on #dup and #clone
|
345
|
+
# We need to duplicate the context to prevent it from being shared
|
346
|
+
def initialize_copy(other)
|
347
|
+
super
|
348
|
+
@context = @context.dup
|
349
|
+
end
|
350
|
+
|
344
351
|
# Converts a String or Hash value into an UploadedFile object and ensures
|
345
352
|
# it's uploaded to temporary storage.
|
346
353
|
#
|
@@ -365,7 +365,9 @@ class Shrine
|
|
365
365
|
handle_request(request)
|
366
366
|
end
|
367
367
|
|
368
|
-
headers[
|
368
|
+
headers = Rack::Headers[headers] if Rack.release >= "3"
|
369
|
+
headers["Content-Length"] ||= body.respond_to?(:bytesize) ? body.bytesize.to_s :
|
370
|
+
body.map(&:bytesize).inject(0, :+).to_s
|
369
371
|
|
370
372
|
[status, headers, body]
|
371
373
|
end
|
@@ -481,19 +483,18 @@ class Shrine
|
|
481
483
|
# `Content-Type` and `Content-Disposition` response headers from derivation
|
482
484
|
# options and file extension of the derivation result.
|
483
485
|
def file_response(file, env)
|
484
|
-
|
485
|
-
|
486
|
-
status = response[0]
|
486
|
+
status, headers, body = rack_file_response(file.path, env)
|
487
487
|
|
488
|
+
headers = Rack::Headers[headers] if Rack.release >= "3"
|
488
489
|
headers = {
|
489
|
-
"Content-Type" => type ||
|
490
|
-
"Content-Length" =>
|
490
|
+
"Content-Type" => type || headers["Content-Type"],
|
491
|
+
"Content-Length" => headers["Content-Length"],
|
491
492
|
"Content-Disposition" => content_disposition(file),
|
492
|
-
"Content-Range" =>
|
493
|
+
"Content-Range" => headers["Content-Range"],
|
493
494
|
"Accept-Ranges" => "bytes",
|
494
495
|
}.compact
|
495
496
|
|
496
|
-
body = Rack::BodyProxy.new(
|
497
|
+
body = Rack::BodyProxy.new(body) { File.delete(file.path) }
|
497
498
|
|
498
499
|
file.close
|
499
500
|
|
@@ -514,8 +515,10 @@ class Shrine
|
|
514
515
|
|
515
516
|
if upload_redirect
|
516
517
|
redirect_url = uploaded_file.url(**upload_redirect_url_options)
|
518
|
+
headers = { "Location" => redirect_url }
|
519
|
+
headers = Rack::Headers[headers] if Rack.release >= "3"
|
517
520
|
|
518
|
-
[302,
|
521
|
+
[302, headers, []]
|
519
522
|
else
|
520
523
|
if derivative && File.exist?(derivative.path)
|
521
524
|
file_response(derivative, env)
|
@@ -113,7 +113,9 @@ class Shrine
|
|
113
113
|
handle_request(request)
|
114
114
|
end
|
115
115
|
|
116
|
-
headers[
|
116
|
+
headers = Rack::Headers[headers] if Rack.release >= "3"
|
117
|
+
headers["Content-Length"] ||= body.respond_to?(:bytesize) ? body.bytesize.to_s :
|
118
|
+
body.map(&:bytesize).inject(0, :+).to_s
|
117
119
|
|
118
120
|
[status, headers, body]
|
119
121
|
end
|
@@ -91,7 +91,9 @@ class Shrine
|
|
91
91
|
end
|
92
92
|
end
|
93
93
|
|
94
|
-
headers[
|
94
|
+
headers = Rack::Headers[headers] if Rack.release >= "3"
|
95
|
+
headers["Content-Length"] ||= body.respond_to?(:bytesize) ? body.bytesize.to_s :
|
96
|
+
body.map(&:bytesize).inject(0, :+).to_s
|
95
97
|
|
96
98
|
[status, headers, body]
|
97
99
|
end
|
@@ -158,16 +160,17 @@ class Shrine
|
|
158
160
|
# headers, and a body enumerable. If `:rack_response` option is given,
|
159
161
|
# calls that instead.
|
160
162
|
def make_response(object, request)
|
161
|
-
if @rack_response
|
162
|
-
|
163
|
+
status, headers, body = if @rack_response
|
164
|
+
@rack_response.call(object, request)
|
163
165
|
else
|
164
|
-
|
166
|
+
[200, { "Content-Type" => CONTENT_TYPE_JSON }, [object.to_json]]
|
165
167
|
end
|
166
168
|
|
169
|
+
headers = Rack::Headers[headers] if Rack.release >= "3"
|
167
170
|
# prevent browsers from caching the response
|
168
|
-
|
171
|
+
headers["Cache-Control"] = "no-store" unless headers.key?("Cache-Control")
|
169
172
|
|
170
|
-
|
173
|
+
[status, headers, body]
|
171
174
|
end
|
172
175
|
|
173
176
|
# Used for early returning an error response.
|
@@ -32,6 +32,8 @@ class Shrine
|
|
32
32
|
headers = rack_headers(**options)
|
33
33
|
body = rack_body(**options)
|
34
34
|
|
35
|
+
headers = Rack::Headers[headers] if Rack.release >= "3"
|
36
|
+
|
35
37
|
[status, headers, body]
|
36
38
|
end
|
37
39
|
|
@@ -141,6 +143,10 @@ class Shrine
|
|
141
143
|
file.close
|
142
144
|
end
|
143
145
|
|
146
|
+
def bytesize
|
147
|
+
each.inject(0) { |sum, chunk| sum += chunk.length }
|
148
|
+
end
|
149
|
+
|
144
150
|
# Rack::Sendfile is activated when response body responds to #to_path.
|
145
151
|
def respond_to_missing?(name, include_private = false)
|
146
152
|
name == :to_path && path
|
@@ -91,7 +91,9 @@ class Shrine
|
|
91
91
|
handle_request(request)
|
92
92
|
end
|
93
93
|
|
94
|
-
headers[
|
94
|
+
headers = Rack::Headers[headers] if Rack.release >= "3"
|
95
|
+
headers["Content-Length"] ||= body.respond_to?(:bytesize) ? body.bytesize.to_s :
|
96
|
+
body.map(&:bytesize).inject(0, :+).to_s
|
95
97
|
|
96
98
|
[status, headers, body]
|
97
99
|
end
|
data/lib/shrine/storage/s3.rb
CHANGED
@@ -14,13 +14,15 @@ require "tempfile"
|
|
14
14
|
class Shrine
|
15
15
|
module Storage
|
16
16
|
class S3
|
17
|
-
attr_reader :client, :bucket, :prefix, :upload_options, :signer, :public
|
17
|
+
attr_reader :client, :bucket, :prefix, :upload_options, :copy_options, :signer, :public
|
18
18
|
|
19
19
|
MAX_MULTIPART_PARTS = 10_000
|
20
20
|
MIN_PART_SIZE = 5*1024*1024
|
21
21
|
|
22
22
|
MULTIPART_THRESHOLD = { upload: 15*1024*1024, copy: 100*1024*1024 }
|
23
23
|
|
24
|
+
COPY_OPTIONS = { tagging_directive: "REPLACE" }
|
25
|
+
|
24
26
|
# Initializes a storage for uploading to S3. All options are forwarded to
|
25
27
|
# [`Aws::S3::Client#initialize`], except the following:
|
26
28
|
#
|
@@ -41,6 +43,10 @@ class Shrine
|
|
41
43
|
# be passed to [`Aws::S3::Object#put`], [`Aws::S3::Object#copy_from`]
|
42
44
|
# and [`Aws::S3::Bucket#presigned_post`].
|
43
45
|
#
|
46
|
+
# :copy_options
|
47
|
+
# : Additional options that will be used for copying files, they will
|
48
|
+
# be passed to [`Aws::S3::Object#copy_from`].
|
49
|
+
#
|
44
50
|
# :multipart_threshold
|
45
51
|
# : If the input file is larger than the specified size, a parallelized
|
46
52
|
# multipart will be used for the upload/copy. Defaults to
|
@@ -62,13 +68,14 @@ class Shrine
|
|
62
68
|
# [`Aws::S3::Bucket#presigned_post`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#presigned_post-instance_method
|
63
69
|
# [`Aws::S3::Client#initialize`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#initialize-instance_method
|
64
70
|
# [configuring AWS SDK]: https://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/setup-config.html
|
65
|
-
def initialize(bucket:, client: nil, prefix: nil, upload_options: {}, multipart_threshold: {}, max_multipart_parts: nil, signer: nil, public: nil, **s3_options)
|
71
|
+
def initialize(bucket:, client: nil, prefix: nil, upload_options: {}, multipart_threshold: {}, max_multipart_parts: nil, signer: nil, public: nil, copy_options: COPY_OPTIONS, **s3_options)
|
66
72
|
raise ArgumentError, "the :bucket option is nil" unless bucket
|
67
73
|
|
68
74
|
@client = client || Aws::S3::Client.new(**s3_options)
|
69
75
|
@bucket = Aws::S3::Bucket.new(name: bucket, client: @client)
|
70
76
|
@prefix = prefix
|
71
77
|
@upload_options = upload_options
|
78
|
+
@copy_options = copy_options
|
72
79
|
@multipart_threshold = MULTIPART_THRESHOLD.merge(multipart_threshold)
|
73
80
|
@max_multipart_parts = max_multipart_parts || MAX_MULTIPART_PARTS
|
74
81
|
@signer = signer
|
@@ -241,7 +248,6 @@ class Shrine
|
|
241
248
|
# don't inherit source object metadata or AWS tags
|
242
249
|
options = {
|
243
250
|
metadata_directive: "REPLACE",
|
244
|
-
tagging_directive: "REPLACE"
|
245
251
|
}
|
246
252
|
|
247
253
|
if io.size && io.size >= @multipart_threshold[:copy]
|
@@ -249,6 +255,7 @@ class Shrine
|
|
249
255
|
options.merge!(multipart_copy: true, content_length: io.size)
|
250
256
|
end
|
251
257
|
|
258
|
+
options.merge!(@copy_options)
|
252
259
|
options.merge!(copy_options)
|
253
260
|
|
254
261
|
object(id).copy_from(io.storage.object(io.id), **options)
|
data/lib/shrine/uploaded_file.rb
CHANGED
data/lib/shrine/version.rb
CHANGED
data/shrine.gemspec
CHANGED
@@ -42,9 +42,9 @@ direct uploads for fully asynchronous user experience.
|
|
42
42
|
gem.add_development_dependency "mocha", "~> 1.11"
|
43
43
|
|
44
44
|
# for endpoint plugins
|
45
|
-
gem.add_development_dependency "rack", "
|
45
|
+
gem.add_development_dependency "rack", ">= 2", "< 4"
|
46
46
|
gem.add_development_dependency "http-form_data", "~> 2.2"
|
47
|
-
gem.add_development_dependency "rack-
|
47
|
+
gem.add_development_dependency "rack-test", "~> 2.1"
|
48
48
|
|
49
49
|
# for determine_mime_type plugin
|
50
50
|
gem.add_development_dependency "mimemagic", ">= 0.3.2"
|
@@ -71,6 +71,6 @@ direct uploads for fully asynchronous user experience.
|
|
71
71
|
|
72
72
|
# for ORM plugins
|
73
73
|
gem.add_development_dependency "sequel"
|
74
|
-
gem.add_development_dependency "activerecord", RUBY_VERSION >= "2.7" ? "~> 7.0" : RUBY_VERSION >= "2.5" ? "~> 6.0" : "~> 5.2"
|
74
|
+
gem.add_development_dependency "activerecord", RUBY_ENGINE == "jruby" ? "~> 7.0.0" : RUBY_VERSION >= "2.7" ? "~> 7.0" : RUBY_VERSION >= "2.5" ? "~> 6.0" : "~> 5.2"
|
75
75
|
gem.add_development_dependency "sqlite3", "~> 1.4" unless RUBY_ENGINE == "jruby"
|
76
76
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shrine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Janko Marohnić
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-04-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: down
|
@@ -84,16 +84,22 @@ dependencies:
|
|
84
84
|
name: rack
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- - "
|
87
|
+
- - ">="
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '2
|
89
|
+
version: '2'
|
90
|
+
- - "<"
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '4'
|
90
93
|
type: :development
|
91
94
|
prerelease: false
|
92
95
|
version_requirements: !ruby/object:Gem::Requirement
|
93
96
|
requirements:
|
94
|
-
- - "
|
97
|
+
- - ">="
|
95
98
|
- !ruby/object:Gem::Version
|
96
|
-
version: '2
|
99
|
+
version: '2'
|
100
|
+
- - "<"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '4'
|
97
103
|
- !ruby/object:Gem::Dependency
|
98
104
|
name: http-form_data
|
99
105
|
requirement: !ruby/object:Gem::Requirement
|
@@ -109,19 +115,19 @@ dependencies:
|
|
109
115
|
- !ruby/object:Gem::Version
|
110
116
|
version: '2.2'
|
111
117
|
- !ruby/object:Gem::Dependency
|
112
|
-
name: rack-
|
118
|
+
name: rack-test
|
113
119
|
requirement: !ruby/object:Gem::Requirement
|
114
120
|
requirements:
|
115
|
-
- - "
|
121
|
+
- - "~>"
|
116
122
|
- !ruby/object:Gem::Version
|
117
|
-
version: '
|
123
|
+
version: '2.1'
|
118
124
|
type: :development
|
119
125
|
prerelease: false
|
120
126
|
version_requirements: !ruby/object:Gem::Requirement
|
121
127
|
requirements:
|
122
|
-
- - "
|
128
|
+
- - "~>"
|
123
129
|
- !ruby/object:Gem::Version
|
124
|
-
version: '
|
130
|
+
version: '2.1'
|
125
131
|
- !ruby/object:Gem::Dependency
|
126
132
|
name: mimemagic
|
127
133
|
requirement: !ruby/object:Gem::Requirement
|
@@ -475,6 +481,7 @@ files:
|
|
475
481
|
- doc/release_notes/3.3.0.md
|
476
482
|
- doc/release_notes/3.4.0.md
|
477
483
|
- doc/release_notes/3.5.0.md
|
484
|
+
- doc/release_notes/3.6.0.md
|
478
485
|
- doc/retrieving_uploads.md
|
479
486
|
- doc/securing_uploads.md
|
480
487
|
- doc/storage/file_system.md
|
@@ -569,7 +576,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
569
576
|
- !ruby/object:Gem::Version
|
570
577
|
version: '0'
|
571
578
|
requirements: []
|
572
|
-
rubygems_version: 3.
|
579
|
+
rubygems_version: 3.5.9
|
573
580
|
signing_key:
|
574
581
|
specification_version: 4
|
575
582
|
summary: Toolkit for file attachments in Ruby applications
|