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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46e9fa07aa69797777e3072eed12a4b901b357ae840c71f4facd5b0be2d79929
4
- data.tar.gz: 0714e663dd130b9af96b195e5a3d87c128a0e6dd0b0598a7b2d4286e97223008
3
+ metadata.gz: a5fadb4e660b7638e1d17b81ecb1761e8dd5faa675f15a3b30de0c549bafd653
4
+ data.tar.gz: a9ee826b483ab474f55ac66bc35e95a4cb8496cada12ce7595091210670f1f38
5
5
  SHA512:
6
- metadata.gz: 87b70c2bd15dd044eacc1b2089b65bb0538728fd56dea5cce8e6e191b3bf07e63e2af45b877bfd317bcf38c98279c49d07f5d18db28d106cb08360478f1bcb76
7
- data.tar.gz: 57703469c6efa1e724113944815dc9fb5e2a3506cf261810c07d0e40c7849bc79d2184bb576222ca7a21c56fe3f496602e93d7b60696cee763aabdaaed71cf23
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.
@@ -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["Content-Length"] ||= body.map(&:bytesize).inject(0, :+).to_s
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
- response = rack_file_response(file.path, env)
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 || response[1]["Content-Type"],
490
- "Content-Length" => response[1]["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" => response[1]["Content-Range"],
493
+ "Content-Range" => headers["Content-Range"],
493
494
  "Accept-Ranges" => "bytes",
494
495
  }.compact
495
496
 
496
- body = Rack::BodyProxy.new(response[2]) { File.delete(file.path) }
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, { "Location" => redirect_url }, []]
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["Content-Length"] ||= body.map(&:bytesize).inject(0, :+).to_s
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["Content-Length"] ||= body.map(&:bytesize).inject(0, :+).to_s
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
- response = @rack_response.call(object, request)
163
+ status, headers, body = if @rack_response
164
+ @rack_response.call(object, request)
163
165
  else
164
- response = [200, { "Content-Type" => CONTENT_TYPE_JSON }, [object.to_json]]
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
- response[1]["Cache-Control"] = "no-store" unless response[1].key?("Cache-Control")
171
+ headers["Cache-Control"] = "no-store" unless headers.key?("Cache-Control")
169
172
 
170
- response
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["Content-Length"] ||= body.map(&:bytesize).inject(0, :+).to_s
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
@@ -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)
@@ -170,6 +170,7 @@ class Shrine
170
170
  # opened IO object.
171
171
  def close
172
172
  io.close if opened?
173
+ @io = nil
173
174
  end
174
175
 
175
176
  # Returns whether the file has already been opened.
@@ -7,7 +7,7 @@ class Shrine
7
7
 
8
8
  module VERSION
9
9
  MAJOR = 3
10
- MINOR = 5
10
+ MINOR = 6
11
11
  TINY = 0
12
12
  PRE = nil
13
13
 
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", "~> 2.0"
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-test_app"
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.5.0
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: 2023-07-07 00:00:00.000000000 Z
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.0'
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.0'
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-test_app
118
+ name: rack-test
113
119
  requirement: !ruby/object:Gem::Requirement
114
120
  requirements:
115
- - - ">="
121
+ - - "~>"
116
122
  - !ruby/object:Gem::Version
117
- version: '0'
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: '0'
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.4.12
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