shrine 3.7.0 → 3.8.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: 01c1404cab9882d16a6795c41467591b4083cbef76b5d42b91626a4306b33584
4
- data.tar.gz: bec484b8594573db2056eabeda97ae5c22fb170a35f984802cd200085611e556
3
+ metadata.gz: 55c3fc9bf7eb40700d2be0ee82900986c601b1b3449d5d5ee7b246546bfe5eb3
4
+ data.tar.gz: 11ffe3b196b79946c931f5a0e9538bda74cc4abd74711124509c76a13a5eaddc
5
5
  SHA512:
6
- metadata.gz: 298e5065ba40638cee54dc4b74be98e466974502f56b5bff956fb2d7cc4d73028ec8a9d79dae7c073306c4512145cfef082ec4beeeb60a0eddef5497bbcf8174
7
- data.tar.gz: 3413b070afe6315133342ab3faf7c41d97bb9767a3aa13f62bf4ba339246fb4f1b3d1ebc91036b37942b8893c20b652edf4603f2b710bd66647caed6b9cddc48
6
+ metadata.gz: 19585a7a2c486d43fb9be2abc84123adced4d18beebf50919938a9c631d16c0c2ecb15e777397b5b8a1642911171d328c55bf86769b9a31a4a3957cf3d8256d9
7
+ data.tar.gz: 1dfbb667941b5f2bfa1b035230f0b1fb809c59855fc253f8365394624cffd4e3078a0aa41188eb27c8e57be8d4243e45d5dcf56a83b4d9ea1900bf02fa340dc6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
+ ## 3.8.0 (2026-06-24)
2
+
3
+ * `s3` – Use single-request uploads for smaller files again instead of always using multipart uploads (@janko)
4
+
5
+ * `derivatives` – Forward `Attacher#promote` options to `Attacher#upload_derivatives` (@milkcocoa)
6
+
7
+ * `file_system` – Prevent path traversal outside of the storage directory (@janko)
8
+
9
+ ## 3.7.1 (2026-06-03)
10
+
11
+ * Update method signatures of some plugins to work around a Bootsnap bug causing a `wrong number of arguments` error (@janko)
12
+
13
+ * `derivation_endpoint` – Add `:format` argument to `UploadedFile#derivation_url` for path extension (@janko)
14
+
15
+ * `rack_response` – Add `:etag` argument for setting a custom `ETag` (@camilohollanda)
16
+
1
17
  ## 3.7.0 (2026-05-27)
2
18
 
19
+ * `rack_response` - Add `:etag` option for setting a custom `ETag` header (@janko)
20
+
3
21
  * `download_endpoint` - Add support for expiring URLs (@davidwessman)
4
22
 
5
23
  * `s3` - Use `TransferManager` where available instead of deprecated `upload_steam` (@danieldevlewis)
@@ -309,6 +309,19 @@ uploaded_file.derivation_url(:thumbnail, prefix: "transformations/image")
309
309
  #=> ".../transformations/image/thumbnail/eyJpZCI6ImZvbyIsInN?signature=..."
310
310
  ```
311
311
 
312
+ ## Format
313
+
314
+ Some HTTP clients and CDNs use the URL path extension to determine the content
315
+ type of the response. You can append a file extension to the derivation URL
316
+ path with the `:format` option:
317
+
318
+ ```rb
319
+ uploaded_file.derivation_url(:thumbnail, format: "jpg")
320
+ #=> ".../thumbnail/eyJpZCI6ImZvbyIsInN.jpg?signature=..."
321
+ ```
322
+
323
+ The extension is included in the URL signature, so it cannot be tampered with.
324
+
312
325
  ## Expiration
313
326
 
314
327
  By default derivation URLs are valid indefinitely. If you want URLs to expire
@@ -95,6 +95,15 @@ headers["Content-Range"] #=> "bytes 100-200/1000"
95
95
  body # partial content
96
96
  ```
97
97
 
98
+ ## ETag
99
+
100
+ A custom ETag value can be provided via the `:etag` option:
101
+
102
+ ```rb
103
+ response = uploaded_file.to_rack_response(etag: "my-custom-etag")
104
+ response[1]["ETag"] #=> "my-custom-etag"
105
+ ```
106
+
98
107
  ## Download options
99
108
 
100
109
  The `#to_rack_response` method will automatically open the `UploadedFile` if it
@@ -0,0 +1,31 @@
1
+ ---
2
+ title: Shrine 3.7.1
3
+ ---
4
+
5
+ ## New features
6
+
7
+ * The `derivation_endpoint` plugin now supports adding a file extension to the derivation URL path via the `:format` option on `UploadedFile#derivation_url`. Some HTTP clients and CDNs use the URL path extension to determine the content type of the response.
8
+
9
+ ```rb
10
+ uploaded_file.derivation_url(:thumbnail, format: "jpg")
11
+ #=> ".../thumbnail/eyJpZCI6ImZvbyIsInN.jpg?signature=..."
12
+ ```
13
+
14
+ The extension is included in the URL signature, so it cannot be tampered with.
15
+
16
+ * The `rack_response` plugin now accepts an `:etag` option for setting a custom `ETag` header, overriding the default one Shrine generates.
17
+
18
+ ```rb
19
+ response = uploaded_file.to_rack_response(etag: "my-custom-etag")
20
+ response[1]["ETag"] #=> "my-custom-etag"
21
+ ```
22
+
23
+ ## Bug fixes
24
+
25
+ * When initializing Shrine with the `activerecord` plugin, under Bootsnap 1.24.5 and Ruby 3.4.5 this would raise an error:
26
+
27
+ ```
28
+ ArgumentError: wrong number of arguments (given 2, expected 1; required keyword: plugin)
29
+ ```
30
+
31
+ The seems to be triggered by a special combination of method signatures, where keyword arguments get converted into positional arguments under Bootsnap. To work around this issue, method signatures of some plugin methods have been updated.
@@ -0,0 +1,37 @@
1
+ ---
2
+ title: Shrine 3.8.0
3
+ ---
4
+
5
+ ## Security
6
+
7
+ * The `file_system` storage now prevents path traversal outside of the storage
8
+ directory. Previously, an id containing `../` sequences (e.g. coming from
9
+ attacker-controlled data) could resolve to a location outside of the
10
+ configured storage directory. Now `Shrine::Error` is raised whenever an id
11
+ would resolve outside of the storage directory:
12
+
13
+ ```rb
14
+ storage = Shrine::Storage::FileSystem.new("uploads")
15
+ storage.open("../../etc/passwd") #~> Shrine::Error
16
+ ```
17
+
18
+ ## New features
19
+
20
+ * The `derivatives` plugin now forwards options passed to `Attacher#promote`
21
+ into `Attacher#upload_derivatives`. This means options such as upload options
22
+ reach the derivatives upload as well, not just the main file.
23
+
24
+ ```rb
25
+ attacher.promote(upload_options: { acl: "public-read" })
26
+ # the same options are now forwarded when uploading derivatives
27
+ ```
28
+
29
+ ## Other improvements
30
+
31
+ * The `s3` storage again uses single-request uploads for smaller files, instead
32
+ of always using multipart uploads. In Shrine 3.7.0, when a `TransferManager`
33
+ was available, it was used for uploads of any size, but `TransferManager`
34
+ uploads via multipart unconditionally, which is inefficient for smaller files.
35
+ Now files at or below the multipart threshold (`:upload`, 15MB by default) are
36
+ uploaded in a single request, and only larger files use multipart upload via
37
+ the `TransferManager` (falling back to the older API when it's not available).
@@ -7,7 +7,7 @@ class Shrine
7
7
  #
8
8
  # plugin :_persistence, plugin: MyPlugin
9
9
  module Persistence
10
- def self.load_dependencies(uploader, *)
10
+ def self.load_dependencies(uploader, **)
11
11
  uploader.plugin :atomic_helpers
12
12
  uploader.plugin :entity
13
13
  end
@@ -24,7 +24,7 @@ class Shrine
24
24
  }.inspect}"
25
25
  end
26
26
 
27
- def self.load_dependencies(uploader, *)
27
+ def self.load_dependencies(uploader, **)
28
28
  uploader.plugin :validation
29
29
  end
30
30
 
@@ -303,9 +303,9 @@ class Shrine
303
303
  class Derivation::Url < Derivation::Command
304
304
  delegate :name, :args, :source, :secret_key, :signer
305
305
 
306
- def call(host: nil, prefix: nil, metadata: [], **)
306
+ def call(host: nil, prefix: nil, metadata: [], format: nil, **)
307
307
  base_url = [host, *prefix].join("/")
308
- path = path_identifier(metadata:)
308
+ path = path_identifier(metadata:, format:)
309
309
 
310
310
  if signer
311
311
  url = [base_url, path].join("/")
@@ -318,12 +318,13 @@ class Shrine
318
318
 
319
319
  private
320
320
 
321
- def path_identifier(metadata: [])
322
- [
321
+ def path_identifier(metadata: [], format: nil)
322
+ path = [
323
323
  name,
324
324
  *args,
325
325
  source.urlsafe_dump(metadata:)
326
326
  ].map{|component| Rack::Utils.escape_path(component.to_s)}.join('/')
327
+ format ? "#{path}.#{format}" : path
327
328
  end
328
329
 
329
330
  def query(expires_in: nil,
@@ -386,6 +387,7 @@ class Shrine
386
387
  check_expiry!(request)
387
388
 
388
389
  name, *args, serialized_file = request.path_info.split("/")[1..-1]
390
+ serialized_file = serialized_file.sub(/\.\w+$/, "")
389
391
 
390
392
  name = name.to_sym
391
393
  uploaded_file = shrine_class::UploadedFile.urlsafe_load(serialized_file)
@@ -161,7 +161,7 @@ class Shrine
161
161
  # attacher.stored?(attacher.derivatives[:thumb]) #=> true
162
162
  def promote(**options)
163
163
  super
164
- promote_derivatives
164
+ promote_derivatives(**options)
165
165
  create_derivatives if create_derivatives_on_promote?
166
166
  end
167
167
 
@@ -4,7 +4,7 @@ class Shrine
4
4
  module Plugins
5
5
  # Documentation can be found on https://shrinerb.com/docs/plugins/metadata_attributes
6
6
  module MetadataAttributes
7
- def self.load_dependencies(uploader, *)
7
+ def self.load_dependencies(uploader, **)
8
8
  uploader.plugin :entity
9
9
  end
10
10
 
@@ -49,14 +49,14 @@ class Shrine
49
49
  # "Content-Disposition" headers, whose values are extracted from
50
50
  # metadata. Also returns the correct "Content-Range" header on ranged
51
51
  # requests.
52
- def rack_headers(filename: nil, type: nil, disposition: "inline", range: false)
52
+ def rack_headers(filename: nil, type: nil, disposition: "inline", range: false, etag: nil)
53
53
  {
54
54
  "Content-Length" => content_length(range),
55
55
  "Content-Type" => content_type(type),
56
56
  "Content-Disposition" => content_disposition(disposition, filename),
57
57
  "Content-Range" => content_range(range),
58
58
  "Accept-Ranges" => accept_ranges(range),
59
- "ETag" => etag,
59
+ "ETag" => etag || self.etag,
60
60
  }.compact
61
61
  end
62
62
 
@@ -18,7 +18,7 @@ class Shrine
18
18
 
19
19
  DOWNLOADER = -> (url, **options) { Down.download(url, **options) }
20
20
 
21
- def self.load_dependencies(uploader, *)
21
+ def self.load_dependencies(uploader, **)
22
22
  uploader.plugin :validation
23
23
  end
24
24
 
@@ -32,7 +32,7 @@ class Shrine
32
32
  "%.1f %s" % [bytes.to_f / 1024 ** exp, FILESIZE_UNITS[exp]]
33
33
  end
34
34
 
35
- def self.load_dependencies(uploader, *)
35
+ def self.load_dependencies(uploader, **)
36
36
  uploader.plugin :validation
37
37
  end
38
38
 
@@ -113,9 +113,18 @@ class Shrine
113
113
  end
114
114
  end
115
115
 
116
- # Returns the full path to the file.
116
+ # Returns the full path to the file. Raises Shrine::Error if the id would
117
+ # resolve to a location outside of the storage #directory (e.g. an id
118
+ # containing `../` path traversal sequences in attacker-controlled data).
117
119
  def path(id)
118
- directory.join(id.gsub("/", File::SEPARATOR))
120
+ path = directory.join(id.gsub("/", File::SEPARATOR))
121
+ expanded = path.expand_path
122
+
123
+ unless expanded == directory || expanded.to_s.start_with?("#{directory}#{File::SEPARATOR}")
124
+ raise Shrine::Error, "path #{id.inspect} resolves outside of the storage directory"
125
+ end
126
+
127
+ path
119
128
  end
120
129
 
121
130
  protected
@@ -230,20 +230,15 @@ class Shrine
230
230
 
231
231
  private
232
232
 
233
- # Upload the file to S3.
234
- # Uses @transfer_manager, if defined, for any size upload.
235
- # Falls back to the original code using the older, now depricated
236
- # AWS APIs for users of older version of the AWS Gem.
237
- # for multipart uploads of large files.
233
+ # Uploads the file to S3. Uses multipart upload for large files.
238
234
  def put(io, id, **)
239
- if @transfer_manager
235
+ if io.respond_to?(:size) && io.size && io.size <= @multipart_threshold[:upload]
236
+ object(id).put(body: io, **)
237
+ elsif @transfer_manager # multipart upload - transfer manager
240
238
  @transfer_manager.upload_stream(bucket: bucket.name, key: object_key(id), part_size: part_size(io), **) do |write_stream|
241
239
  IO.copy_stream(io, write_stream)
242
240
  end
243
- elsif io.respond_to?(:size) && io.size && io.size <= @multipart_threshold[:upload]
244
- object(id).put(body: io, **)
245
- else
246
- # multipart upload old API
241
+ else # multipart upload - before transfer manager
247
242
  object(id).upload_stream(part_size: part_size(io), **) do |write_stream|
248
243
  IO.copy_stream(io, write_stream)
249
244
  end
@@ -7,7 +7,7 @@ class Shrine
7
7
 
8
8
  module VERSION
9
9
  MAJOR = 3
10
- MINOR = 7
10
+ MINOR = 8
11
11
  TINY = 0
12
12
  PRE = nil
13
13
 
data/shrine.gemspec CHANGED
@@ -67,10 +67,10 @@ direct uploads for fully asynchronous user experience.
67
67
 
68
68
  # for instrumentation plugin
69
69
  gem.add_development_dependency "dry-monitor"
70
- gem.add_development_dependency "activesupport", RUBY_ENGINE == "jruby" ? "~> 7.0.0" : "~> 8.1"
70
+ gem.add_development_dependency "activesupport", RUBY_ENGINE == "jruby" ? "~> 8.0.0" : "~> 8.1"
71
71
 
72
72
  # for ORM plugins
73
73
  gem.add_development_dependency "sequel"
74
- gem.add_development_dependency "activerecord", RUBY_ENGINE == "jruby" ? "~> 7.0.0" : "~> 8.1"
74
+ gem.add_development_dependency "activerecord", RUBY_ENGINE == "jruby" ? "~> 8.0.0" : "~> 8.1"
75
75
  gem.add_development_dependency "sqlite3", "~> 2.1" unless RUBY_ENGINE == "jruby"
76
76
  end
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.7.0
4
+ version: 3.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janko Marohnić
@@ -155,20 +155,6 @@ dependencies:
155
155
  - - ">="
156
156
  - !ruby/object:Gem::Version
157
157
  version: '0'
158
- - !ruby/object:Gem::Dependency
159
- name: ruby-filemagic
160
- requirement: !ruby/object:Gem::Requirement
161
- requirements:
162
- - - "~>"
163
- - !ruby/object:Gem::Version
164
- version: '0.7'
165
- type: :development
166
- prerelease: false
167
- version_requirements: !ruby/object:Gem::Requirement
168
- requirements:
169
- - - "~>"
170
- - !ruby/object:Gem::Version
171
- version: '0.7'
172
158
  - !ruby/object:Gem::Dependency
173
159
  name: mime-types
174
160
  requirement: !ruby/object:Gem::Requirement
@@ -301,14 +287,14 @@ dependencies:
301
287
  requirements:
302
288
  - - "~>"
303
289
  - !ruby/object:Gem::Version
304
- version: '8.1'
290
+ version: 8.0.0
305
291
  type: :development
306
292
  prerelease: false
307
293
  version_requirements: !ruby/object:Gem::Requirement
308
294
  requirements:
309
295
  - - "~>"
310
296
  - !ruby/object:Gem::Version
311
- version: '8.1'
297
+ version: 8.0.0
312
298
  - !ruby/object:Gem::Dependency
313
299
  name: sequel
314
300
  requirement: !ruby/object:Gem::Requirement
@@ -329,28 +315,14 @@ dependencies:
329
315
  requirements:
330
316
  - - "~>"
331
317
  - !ruby/object:Gem::Version
332
- version: '8.1'
333
- type: :development
334
- prerelease: false
335
- version_requirements: !ruby/object:Gem::Requirement
336
- requirements:
337
- - - "~>"
338
- - !ruby/object:Gem::Version
339
- version: '8.1'
340
- - !ruby/object:Gem::Dependency
341
- name: sqlite3
342
- requirement: !ruby/object:Gem::Requirement
343
- requirements:
344
- - - "~>"
345
- - !ruby/object:Gem::Version
346
- version: '2.1'
318
+ version: 8.0.0
347
319
  type: :development
348
320
  prerelease: false
349
321
  version_requirements: !ruby/object:Gem::Requirement
350
322
  requirements:
351
323
  - - "~>"
352
324
  - !ruby/object:Gem::Version
353
- version: '2.1'
325
+ version: 8.0.0
354
326
  description: |
355
327
  Shrine is a toolkit for file attachments in Ruby applications. It supports
356
328
  uploading, downloading, processing and deleting IO objects, backed by various
@@ -482,6 +454,8 @@ files:
482
454
  - doc/release_notes/3.5.0.md
483
455
  - doc/release_notes/3.6.0.md
484
456
  - doc/release_notes/3.7.0.md
457
+ - doc/release_notes/3.7.1.md
458
+ - doc/release_notes/3.8.0.md
485
459
  - doc/retrieving_uploads.md
486
460
  - doc/securing_uploads.md
487
461
  - doc/storage/file_system.md