activestorage 7.2.2.2 → 7.2.3.1

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: 95b4c7b82cf963f9b1d16f4646ad4538829af0a213797ac35621e7a4b6e13609
4
- data.tar.gz: dab322a2424c0c8f7fc30af73b7453d1ab36060a2f905eb31f4d21cfc7847d6a
3
+ metadata.gz: 07e918ed2ad2733425a8c83bcd4a17b0de42f1b4e4692f0ac60f37886a2b1982
4
+ data.tar.gz: 8ec0faa694ef8191678280d2f5ed44b8a3d623788e2239013a43a33c25e0f754
5
5
  SHA512:
6
- metadata.gz: 1a3ff3b23455a2e472fc46dab1a947dab49032917c59a1844fee4d694445366df9ef2b99085de5d10c6107f0f1085179a303da233d6f953c9a37ddbdba70e5cc
7
- data.tar.gz: a6601fbc244adaf30ba1dac98f3062700195bfa5f728b9425976d2f14c89f6ee27223d25b00c1f7573022677e558f45a06487efba20fda259fb9006014af7a68
6
+ metadata.gz: 71e330a9f27d46bdd58c56f7bf4ead818b549c899e5aca043f895b4377b0478b25499c05ec39392e4fcbf5c3eaa8e85f5ada5000d3f2c01ae84badeed4591326
7
+ data.tar.gz: bb8ce35c75e8a43bdc135ddac20b94928bdbc1365b3351e7570f72c566001906506ecd32a98cde9fd3e47a1b8726058d235ce413afeac31a6d874ff9687d8096
data/CHANGELOG.md CHANGED
@@ -1,11 +1,82 @@
1
+ ## Rails 7.2.3.1 (March 23, 2026) ##
2
+
3
+ * Filter user supplied metadata in DirectUploadController
4
+
5
+ [CVE-2026-33173]
6
+
7
+ *Jean Boussier*
8
+
9
+ * Configurable maxmimum streaming chunk size
10
+
11
+ Makes sure that byte ranges for blobs don't exceed 100mb by default.
12
+ Content ranges that are too big can result in denial of service.
13
+
14
+ [CVE-2026-33174]
15
+
16
+ *Gannon McGibbon*
17
+
18
+ * Limit range requests to a single range
19
+
20
+ [CVE-2026-33658]
21
+
22
+ *Jean Boussier*
23
+
24
+ * Prevent path traversal in `DiskService`.
25
+
26
+ `DiskService#path_for` now raises an `InvalidKeyError` when passed keys with dot segments (".",
27
+ ".."), or if the resolved path is outside the storage root directory.
28
+
29
+ `#path_for` also now consistently raises `InvalidKeyError` if the key is invalid in any way, for
30
+ example containing null bytes or having an incompatible encoding. Previously, the exception
31
+ raised may have been `ArgumentError` or `Encoding::CompatibilityError`.
32
+
33
+ `DiskController` now explicitly rescues `InvalidKeyError` with appropriate HTTP status codes.
34
+
35
+ [CVE-2026-33195]
36
+
37
+ *Mike Dalessio*
38
+
39
+ * Prevent glob injection in `DiskService#delete_prefixed`.
40
+
41
+ Escape glob metacharacters in the resolved path before passing to `Dir.glob`.
42
+
43
+ Note that this change breaks any existing code that is relying on `delete_prefixed` to expand
44
+ glob metacharacters. This change presumes that is unintended behavior (as other storage services
45
+ do not respect these metacharacters).
46
+
47
+ [CVE-2026-33202]
48
+
49
+ *Mike Dalessio*
50
+
51
+
52
+ ## Rails 7.2.3 (October 28, 2025) ##
53
+
54
+ * Fix `config.active_storage.touch_attachment_records` to work with eager loading.
55
+
56
+ *fatkodima*
57
+
58
+ * A Blob will no longer autosave associated Attachment.
59
+
60
+ This fixes an issue where a record with an attachment would have
61
+ its dirty attributes reset, preventing your `after commit` callbacks
62
+ on that record to behave as expected.
63
+
64
+ Note that this change doesn't require any changes on your application
65
+ and is supposed to be internal. Active Storage Attachment will continue
66
+ to be autosaved (through a different relation).
67
+
68
+ *Edouard-chin*
69
+
70
+
1
71
  ## Rails 7.2.2.2 (August 13, 2025) ##
2
72
 
3
- Remove dangerous transformations
73
+ * Remove dangerous transformations
4
74
 
5
75
  [CVE-2025-24293]
6
76
 
7
77
  *Zack Deveau*
8
78
 
79
+
9
80
  ## Rails 7.2.2.1 (December 10, 2024) ##
10
81
 
11
82
  * No changes.
data/README.md CHANGED
@@ -203,6 +203,6 @@ Bug reports for the Ruby on \Rails project can be filed here:
203
203
 
204
204
  * https://github.com/rails/rails/issues
205
205
 
206
- Feature requests should be discussed on the rails-core mailing list here:
206
+ Feature requests should be discussed on the rubyonrails-core forum here:
207
207
 
208
208
  * https://discuss.rubyonrails.org/c/rubyonrails-core
@@ -17,6 +17,8 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
17
17
  end
18
18
  rescue Errno::ENOENT
19
19
  head :not_found
20
+ rescue ActiveStorage::InvalidKeyError
21
+ head :not_found
20
22
  end
21
23
 
22
24
  def update
@@ -25,13 +27,15 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
25
27
  named_disk_service(token[:service_name]).upload token[:key], request.body, checksum: token[:checksum]
26
28
  head :no_content
27
29
  else
28
- head :unprocessable_entity
30
+ head ActionDispatch::Constants::UNPROCESSABLE_CONTENT
29
31
  end
30
32
  else
31
33
  head :not_found
32
34
  end
33
35
  rescue ActiveStorage::IntegrityError
34
- head :unprocessable_entity
36
+ head ActionDispatch::Constants::UNPROCESSABLE_CONTENT
37
+ rescue ActiveStorage::InvalidKeyError
38
+ head ActionDispatch::Constants::UNPROCESSABLE_CONTENT
35
39
  end
36
40
 
37
41
  private
@@ -14,7 +14,8 @@ module ActiveStorage::Streaming
14
14
  def send_blob_byte_range_data(blob, range_header, disposition: nil)
15
15
  ranges = Rack::Utils.get_byte_ranges(range_header, blob.byte_size)
16
16
 
17
- return head(:range_not_satisfiable) if ranges.blank? || ranges.all?(&:blank?)
17
+ return head(:range_not_satisfiable) unless ranges_valid?(ranges)
18
+ return head(:range_not_satisfiable) if ranges.length > ActiveStorage.streaming_max_ranges
18
19
 
19
20
  if ranges.length == 1
20
21
  range = ranges.first
@@ -51,6 +52,12 @@ module ActiveStorage::Streaming
51
52
  )
52
53
  end
53
54
 
55
+ def ranges_valid?(ranges)
56
+ return false if ranges.blank? || ranges.all?(&:blank?)
57
+
58
+ ranges.sum { |range| range.end - range.begin } < ActiveStorage.streaming_chunk_max_size
59
+ end
60
+
54
61
  # Stream the blob from storage directly to the response. The disposition can be controlled by setting +disposition+.
55
62
  # The content type and filename is set directly from the +blob+.
56
63
  def send_blob_stream(blob, disposition: nil) # :doc:
@@ -31,6 +31,72 @@ module ActiveStorage::Blob::Representable
31
31
  # Raises ActiveStorage::InvariableError if the variant processor cannot
32
32
  # transform the blob. To determine whether a blob is variable, call
33
33
  # ActiveStorage::Blob#variable?.
34
+ #
35
+ # ==== Options
36
+ #
37
+ # Options are defined by the {image_processing gem}[https://github.com/janko/image_processing],
38
+ # and depend on which variant processor you are using:
39
+ # {Vips}[https://github.com/janko/image_processing/blob/master/doc/vips.md] or
40
+ # {MiniMagick}[https://github.com/janko/image_processing/blob/master/doc/minimagick.md].
41
+ # However, both variant processors support the following options:
42
+ #
43
+ # [+:resize_to_limit+]
44
+ # Downsizes the image to fit within the specified dimensions while retaining
45
+ # the original aspect ratio. Will only resize the image if it's larger than
46
+ # the specified dimensions.
47
+ #
48
+ # user.avatar.variant(resize_to_limit: [100, 100])
49
+ #
50
+ # [+:resize_to_fit+]
51
+ # Resizes the image to fit within the specified dimensions while retaining
52
+ # the original aspect ratio. Will downsize the image if it's larger than the
53
+ # specified dimensions or upsize if it's smaller.
54
+ #
55
+ # user.avatar.variant(resize_to_fit: [100, 100])
56
+ #
57
+ # [+:resize_to_fill+]
58
+ # Resizes the image to fill the specified dimensions while retaining the
59
+ # original aspect ratio. If necessary, will crop the image in the larger
60
+ # dimension.
61
+ #
62
+ # user.avatar.variant(resize_to_fill: [100, 100])
63
+ #
64
+ # [+:resize_and_pad+]
65
+ # Resizes the image to fit within the specified dimensions while retaining
66
+ # the original aspect ratio. If necessary, will pad the remaining area with
67
+ # transparent color if source image has alpha channel, black otherwise.
68
+ #
69
+ # user.avatar.variant(resize_and_pad: [100, 100])
70
+ #
71
+ # [+:crop+]
72
+ # Extracts an area from an image. The first two arguments are the left and
73
+ # top edges of area to extract, while the last two arguments are the width
74
+ # and height of the area to extract.
75
+ #
76
+ # user.avatar.variant(crop: [20, 50, 300, 300])
77
+ #
78
+ # [+:rotate+]
79
+ # Rotates the image by the specified angle.
80
+ #
81
+ # user.avatar.variant(rotate: 90)
82
+ #
83
+ # Some options, including those listed above, can accept additional
84
+ # processor-specific values which can be passed as a trailing hash:
85
+ #
86
+ # <!-- Vips supports configuring `crop` for many of its transformations -->
87
+ # <%= image_tag user.avatar.variant(resize_to_fill: [100, 100, { crop: :centre }]) %>
88
+ #
89
+ # If migrating an existing application between MiniMagick and Vips, you will
90
+ # need to update processor-specific options:
91
+ #
92
+ # <!-- MiniMagick -->
93
+ # <%= image_tag user.avatar.variant(resize_to_limit: [100, 100], format: :jpeg,
94
+ # sampling_factor: "4:2:0", strip: true, interlace: "JPEG", colorspace: "sRGB", quality: 80) %>
95
+ #
96
+ # <!-- Vips -->
97
+ # <%= image_tag user.avatar.variant(resize_to_limit: [100, 100], format: :jpeg,
98
+ # saver: { subsample_mode: "on", strip: true, interlace: true, quality: 80 }) %>
99
+ #
34
100
  def variant(transformations)
35
101
  if variable?
36
102
  variant_class.new(self, ActiveStorage::Variation.wrap(transformations).default_to(default_variant_transformations))
@@ -16,10 +16,18 @@
16
16
  # Blobs are intended to be immutable in as-so-far as their reference to a specific file goes. You're allowed to
17
17
  # update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file.
18
18
  # If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one.
19
+ #
20
+ # When using a custom +key+, the value is treated as trusted. Using untrusted user input
21
+ # as the key may result in unexpected behavior.
19
22
  class ActiveStorage::Blob < ActiveStorage::Record
20
23
  MINIMUM_TOKEN_LENGTH = 28
21
24
 
22
25
  has_secure_token :key, length: MINIMUM_TOKEN_LENGTH
26
+
27
+ # FIXME: these property should never have been stored in the metadata.
28
+ # The blob table should be migrated to have dedicated columns for theses.
29
+ PROTECTED_METADATA = %w(analyzed identified composed)
30
+ private_constant :PROTECTED_METADATA
23
31
  store :metadata, accessors: [ :analyzed, :identified, :composed ], coder: ActiveRecord::Coders::JSON
24
32
 
25
33
  class_attribute :services, default: {}
@@ -29,7 +37,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
29
37
  # :method:
30
38
  #
31
39
  # Returns the associated ActiveStorage::Attachment instances.
32
- has_many :attachments
40
+ has_many :attachments, autosave: false
33
41
 
34
42
  ##
35
43
  # :singleton-method:
@@ -93,6 +101,9 @@ class ActiveStorage::Blob < ActiveStorage::Record
93
101
  # be saved before the upload begins to prevent the upload clobbering another due to key collisions.
94
102
  # When providing a content type, pass <tt>identify: false</tt> to bypass
95
103
  # automatic content type inference.
104
+ #
105
+ # The optional +key+ parameter is treated as trusted. Using untrusted user input
106
+ # as the key may result in unexpected behavior.
96
107
  def create_and_upload!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil)
97
108
  create_after_unfurling!(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify).tap do |blob|
98
109
  blob.upload_without_unfurling(io)
@@ -105,6 +116,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
105
116
  # Once the form using the direct upload is submitted, the blob can be associated with the right record using
106
117
  # the signed ID.
107
118
  def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, service_name: nil, record: nil)
119
+ metadata = filter_metadata(metadata)
108
120
  create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata, service_name: service_name
109
121
  end
110
122
 
@@ -153,21 +165,14 @@ class ActiveStorage::Blob < ActiveStorage::Record
153
165
  end
154
166
  end
155
167
 
156
- def validate_service_configuration(service_name, model_class, association_name) # :nodoc:
157
- if service_name
158
- services.fetch(service_name) do
159
- raise ArgumentError, "Cannot configure service #{service_name.inspect} for #{model_class}##{association_name}"
168
+ private
169
+ def filter_metadata(metadata)
170
+ if metadata.is_a?(Hash)
171
+ metadata.without(*PROTECTED_METADATA)
172
+ else
173
+ metadata
160
174
  end
161
- else
162
- validate_global_service_configuration
163
175
  end
164
- end
165
-
166
- def validate_global_service_configuration # :nodoc:
167
- if connected? && table_exists? && Rails.configuration.active_storage.service.nil?
168
- raise RuntimeError, "Missing Active Storage service name. Specify Active Storage service name for config.active_storage.service in config/environments/#{Rails.env}.rb"
169
- end
170
- end
171
176
  end
172
177
 
173
178
  include Analyzable
@@ -121,7 +121,7 @@ module ActiveStorage
121
121
  service_name = record.attachment_reflections[name].options[:service_name]
122
122
  if service_name.is_a?(Proc)
123
123
  service_name = service_name.call(record)
124
- ActiveStorage::Blob.validate_service_configuration(service_name, record.class, name)
124
+ Attached::Model.validate_service_configuration(service_name, record.class, name)
125
125
  end
126
126
  service_name
127
127
  end
@@ -104,7 +104,7 @@ module ActiveStorage
104
104
  # <tt>active_storage_attachments.record_type</tt> polymorphic type column of
105
105
  # the corresponding rows.
106
106
  def has_one_attached(name, dependent: :purge_later, service: nil, strict_loading: false)
107
- ActiveStorage::Blob.validate_service_configuration(service, self, name) unless service.is_a?(Proc)
107
+ Attached::Model.validate_service_configuration(service, self, name) unless service.is_a?(Proc)
108
108
 
109
109
  generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
110
110
  # frozen_string_literal: true
@@ -204,7 +204,7 @@ module ActiveStorage
204
204
  # <tt>active_storage_attachments.record_type</tt> polymorphic type column of
205
205
  # the corresponding rows.
206
206
  def has_many_attached(name, dependent: :purge_later, service: nil, strict_loading: false)
207
- ActiveStorage::Blob.validate_service_configuration(service, self, name) unless service.is_a?(Proc)
207
+ Attached::Model.validate_service_configuration(service, self, name) unless service.is_a?(Proc)
208
208
 
209
209
  generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
210
210
  # frozen_string_literal: true
@@ -255,6 +255,25 @@ module ActiveStorage
255
255
  end
256
256
  end
257
257
 
258
+ class << self
259
+ def validate_service_configuration(service_name, model_class, association_name) # :nodoc:
260
+ if service_name
261
+ ActiveStorage::Blob.services.fetch(service_name) do
262
+ raise ArgumentError, "Cannot configure service #{service_name.inspect} for #{model_class}##{association_name}"
263
+ end
264
+ else
265
+ validate_global_service_configuration(model_class)
266
+ end
267
+ end
268
+
269
+ private
270
+ def validate_global_service_configuration(model_class)
271
+ if model_class.connected? && ActiveStorage::Blob.table_exists? && Rails.configuration.active_storage.service.nil?
272
+ raise RuntimeError, "Missing Active Storage service name. Specify Active Storage service name for config.active_storage.service in config/environments/#{Rails.env}.rb"
273
+ end
274
+ end
275
+ end
276
+
258
277
  def attachment_changes # :nodoc:
259
278
  @attachment_changes ||= {}
260
279
  end
@@ -84,6 +84,10 @@ module ActiveStorage
84
84
  end
85
85
 
86
86
  initializer "active_storage.configs" do
87
+ config.before_initialize do |app|
88
+ ActiveStorage.touch_attachment_records = app.config.active_storage.touch_attachment_records != false
89
+ end
90
+
87
91
  config.after_initialize do |app|
88
92
  ActiveStorage.logger = app.config.active_storage.logger || Rails.logger
89
93
  ActiveStorage.variant_processor = app.config.active_storage.variant_processor || :mini_magick
@@ -112,13 +116,13 @@ module ActiveStorage
112
116
  ActiveStorage.variable_content_types = app.config.active_storage.variable_content_types || []
113
117
  ActiveStorage.web_image_content_types = app.config.active_storage.web_image_content_types || []
114
118
  ActiveStorage.content_types_to_serve_as_binary = app.config.active_storage.content_types_to_serve_as_binary || []
115
- ActiveStorage.touch_attachment_records = app.config.active_storage.touch_attachment_records != false
116
119
  ActiveStorage.service_urls_expire_in = app.config.active_storage.service_urls_expire_in || 5.minutes
117
120
  ActiveStorage.urls_expire_in = app.config.active_storage.urls_expire_in
118
121
  ActiveStorage.content_types_allowed_inline = app.config.active_storage.content_types_allowed_inline || []
119
122
  ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream"
120
123
  ActiveStorage.video_preview_arguments = app.config.active_storage.video_preview_arguments || "-y -vframes 1 -f image2"
121
124
  ActiveStorage.track_variants = app.config.active_storage.track_variants || false
125
+ ActiveStorage.streaming_chunk_max_size = app.config.active_storage.streaming_chunk_max_size || 100.megabytes
122
126
  end
123
127
  end
124
128
 
@@ -26,4 +26,8 @@ module ActiveStorage
26
26
 
27
27
  # Raised when a Previewer is unable to generate a preview image.
28
28
  class PreviewError < Error; end
29
+
30
+ # Raised when a storage key resolves to a path outside the service's root
31
+ # directory, indicating a potential path traversal attack.
32
+ class InvalidKeyError < Error; end
29
33
  end
@@ -50,7 +50,7 @@ module ActiveStorage
50
50
  # by ActiveSupport::Testing::FileFixtures.file_fixture, and upload
51
51
  # the file to the Service
52
52
  #
53
- # === Examples
53
+ # ==== Examples
54
54
  #
55
55
  # # tests/fixtures/active_storage/blobs.yml
56
56
  # second_thumbnail_blob: <%= ActiveStorage::FixtureSet.blob(
@@ -9,8 +9,8 @@ module ActiveStorage
9
9
  module VERSION
10
10
  MAJOR = 7
11
11
  MINOR = 2
12
- TINY = 2
13
- PRE = "2"
12
+ TINY = 3
13
+ PRE = "1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -60,7 +60,16 @@ module ActiveStorage
60
60
 
61
61
  def delete_prefixed(prefix)
62
62
  instrument :delete_prefixed, prefix: prefix do
63
- Dir.glob(path_for("#{prefix}*")).each do |path|
63
+ prefix_path = path_for(prefix)
64
+
65
+ # File.expand_path (called within path_for) strips trailing slashes.
66
+ # Restore trailing separator if the original prefix had one, so that
67
+ # the glob "prefix/*" matches files inside the directory, not siblings
68
+ # whose names start with the prefix string.
69
+ prefix_path += "/" if prefix.end_with?("/")
70
+
71
+ escaped = escape_glob_metacharacters(prefix_path)
72
+ Dir.glob("#{escaped}*").each do |path|
64
73
  FileUtils.rm_rf(path)
65
74
  end
66
75
  end
@@ -98,8 +107,39 @@ module ActiveStorage
98
107
  { "Content-Type" => content_type }
99
108
  end
100
109
 
110
+ # Every filesystem operation in DiskService resolves paths through this method (or through
111
+ # make_path_for, which delegates here). This is the primary filesystem security check: all
112
+ # path-traversal protection is enforced here. New methods that touch the filesystem MUST use
113
+ # path_for or make_path_for -- never construct paths from +root+ directly.
101
114
  def path_for(key) # :nodoc:
102
- File.join root, folder_for(key), key
115
+ if key.blank?
116
+ raise ActiveStorage::InvalidKeyError, "key is blank"
117
+ end
118
+
119
+ # Reject keys with dot segments as defense in depth. This prevents path traversal both outside
120
+ # and within the storage root. The root containment check below is a more fundamental check on
121
+ # path traversal outside of the disk service root.
122
+ begin
123
+ if key.split("/").intersect?(%w[. ..])
124
+ raise ActiveStorage::InvalidKeyError, "key has path traversal segments"
125
+ end
126
+ rescue Encoding::CompatibilityError
127
+ raise ActiveStorage::InvalidKeyError, "key has incompatible encoding"
128
+ end
129
+
130
+ begin
131
+ path = File.expand_path(File.join(root, folder_for(key), key))
132
+ rescue ArgumentError
133
+ # ArgumentError catches null bytes
134
+ raise ActiveStorage::InvalidKeyError, "key is an invalid string"
135
+ end
136
+
137
+ # The resolved path must be inside the root directory.
138
+ unless path.start_with?(File.expand_path(root) + "/")
139
+ raise ActiveStorage::InvalidKeyError, "key is outside of disk service root"
140
+ end
141
+
142
+ path
103
143
  end
104
144
 
105
145
  def compose(source_keys, destination_key, **)
@@ -156,6 +196,10 @@ module ActiveStorage
156
196
  [ key[0..1], key[2..3] ].join("/")
157
197
  end
158
198
 
199
+ def escape_glob_metacharacters(path)
200
+ path.gsub(/[\[\]*?{}\\]/) { |c| "\\#{c}" }
201
+ end
202
+
159
203
  def make_path_for(key)
160
204
  path_for(key).tap { |path| FileUtils.mkdir_p File.dirname(path) }
161
205
  end
@@ -16,6 +16,7 @@ module ActiveStorage
16
16
 
17
17
  def initialize(bucket:, upload: {}, public: false, **options)
18
18
  @client = Aws::S3::Resource.new(**options)
19
+ @transfer_manager = Aws::S3::TransferManager.new(client: @client.client) if defined?(Aws::S3::TransferManager)
19
20
  @bucket = @client.bucket(bucket)
20
21
 
21
22
  @multipart_upload_threshold = upload.delete(:multipart_threshold) || 100.megabytes
@@ -100,7 +101,8 @@ module ActiveStorage
100
101
  def compose(source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
101
102
  content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
102
103
 
103
- object_for(destination_key).upload_stream(
104
+ upload_stream(
105
+ key: destination_key,
104
106
  content_type: content_type,
105
107
  content_disposition: content_disposition,
106
108
  part_size: MINIMUM_UPLOAD_PART_SIZE,
@@ -116,6 +118,14 @@ module ActiveStorage
116
118
  end
117
119
 
118
120
  private
121
+ def upload_stream(key:, **options, &block)
122
+ if @transfer_manager
123
+ @transfer_manager.upload_stream(key: key, bucket: bucket.name, **options, &block)
124
+ else
125
+ object_for(key).upload_stream(**options, &block)
126
+ end
127
+ end
128
+
119
129
  def private_url(key, expires_in:, filename:, disposition:, content_type:, **client_opts)
120
130
  object_for(key).presigned_url :get, expires_in: expires_in.to_i,
121
131
  response_content_disposition: content_disposition_with(type: disposition, filename: filename),
@@ -126,7 +136,6 @@ module ActiveStorage
126
136
  object_for(key).public_url(**client_opts)
127
137
  end
128
138
 
129
-
130
139
  MAXIMUM_UPLOAD_PARTS_COUNT = 10000
131
140
  MINIMUM_UPLOAD_PART_SIZE = 5.megabytes
132
141
 
@@ -139,12 +148,18 @@ module ActiveStorage
139
148
  def upload_with_multipart(key, io, content_type: nil, content_disposition: nil, custom_metadata: {})
140
149
  part_size = [ io.size.fdiv(MAXIMUM_UPLOAD_PARTS_COUNT).ceil, MINIMUM_UPLOAD_PART_SIZE ].max
141
150
 
142
- object_for(key).upload_stream(content_type: content_type, content_disposition: content_disposition, part_size: part_size, metadata: custom_metadata, **upload_options) do |out|
151
+ upload_stream(
152
+ key: key,
153
+ content_type: content_type,
154
+ content_disposition: content_disposition,
155
+ part_size: part_size,
156
+ metadata: custom_metadata,
157
+ **upload_options
158
+ ) do |out|
143
159
  IO.copy_stream(io, out)
144
160
  end
145
161
  end
146
162
 
147
-
148
163
  def object_for(key)
149
164
  bucket.object(key)
150
165
  end
@@ -27,6 +27,7 @@ require "active_record"
27
27
  require "active_support"
28
28
  require "active_support/rails"
29
29
  require "active_support/core_ext/numeric/time"
30
+ require "active_support/core_ext/numeric/bytes"
30
31
 
31
32
  require "active_storage/version"
32
33
  require "active_storage/deprecator"
@@ -350,6 +351,7 @@ module ActiveStorage
350
351
  ]
351
352
  mattr_accessor :unsupported_image_processing_arguments
352
353
 
354
+ mattr_accessor :streaming_chunk_max_size, default: 100.megabytes
353
355
  mattr_accessor :service_urls_expire_in, default: 5.minutes
354
356
  mattr_accessor :touch_attachment_records, default: true
355
357
  mattr_accessor :urls_expire_in
@@ -360,6 +362,18 @@ module ActiveStorage
360
362
 
361
363
  mattr_accessor :track_variants, default: false
362
364
 
365
+ singleton_class.attr_accessor :checksum_implementation
366
+ @checksum_implementation = OpenSSL::Digest::MD5
367
+ begin
368
+ @checksum_implementation.hexdigest("test")
369
+ rescue # OpenSSL may have MD5 disabled
370
+ require "digest/md5"
371
+ @checksum_implementation = Digest::MD5
372
+ end
373
+
374
+ singleton_class.attr_accessor :streaming_max_ranges
375
+ @streaming_max_ranges = 1
376
+
363
377
  mattr_accessor :video_preview_arguments, default: "-y -vframes 1 -f image2"
364
378
 
365
379
  module Transformers
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activestorage
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.2.2.2
4
+ version: 7.2.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
@@ -15,56 +15,56 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 7.2.2.2
18
+ version: 7.2.3.1
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 7.2.2.2
25
+ version: 7.2.3.1
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: actionpack
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 7.2.2.2
32
+ version: 7.2.3.1
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: 7.2.2.2
39
+ version: 7.2.3.1
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: activejob
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - '='
45
45
  - !ruby/object:Gem::Version
46
- version: 7.2.2.2
46
+ version: 7.2.3.1
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - '='
52
52
  - !ruby/object:Gem::Version
53
- version: 7.2.2.2
53
+ version: 7.2.3.1
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: activerecord
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - '='
59
59
  - !ruby/object:Gem::Version
60
- version: 7.2.2.2
60
+ version: 7.2.3.1
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - '='
66
66
  - !ruby/object:Gem::Version
67
- version: 7.2.2.2
67
+ version: 7.2.3.1
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: marcel
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -189,10 +189,10 @@ licenses:
189
189
  - MIT
190
190
  metadata:
191
191
  bug_tracker_uri: https://github.com/rails/rails/issues
192
- changelog_uri: https://github.com/rails/rails/blob/v7.2.2.2/activestorage/CHANGELOG.md
193
- documentation_uri: https://api.rubyonrails.org/v7.2.2.2/
192
+ changelog_uri: https://github.com/rails/rails/blob/v7.2.3.1/activestorage/CHANGELOG.md
193
+ documentation_uri: https://api.rubyonrails.org/v7.2.3.1/
194
194
  mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
195
- source_code_uri: https://github.com/rails/rails/tree/v7.2.2.2/activestorage
195
+ source_code_uri: https://github.com/rails/rails/tree/v7.2.3.1/activestorage
196
196
  rubygems_mfa_required: 'true'
197
197
  rdoc_options: []
198
198
  require_paths:
@@ -208,7 +208,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
208
208
  - !ruby/object:Gem::Version
209
209
  version: '0'
210
210
  requirements: []
211
- rubygems_version: 3.6.9
211
+ rubygems_version: 4.0.6
212
212
  specification_version: 4
213
213
  summary: Local and cloud file storage framework.
214
214
  test_files: []