activestorage 7.2.3.1 → 8.0.0.beta1

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: 07e918ed2ad2733425a8c83bcd4a17b0de42f1b4e4692f0ac60f37886a2b1982
4
- data.tar.gz: 8ec0faa694ef8191678280d2f5ed44b8a3d623788e2239013a43a33c25e0f754
3
+ metadata.gz: 90eb90580cb0faea4b29eef5ddf0dbc99dd1087158980f968561fe45c2dcb4d4
4
+ data.tar.gz: bdd0ced8684956539dee4f32ea7eacd16744c72e318aebc0bd815adfa72ffe73
5
5
  SHA512:
6
- metadata.gz: 71e330a9f27d46bdd58c56f7bf4ead818b549c899e5aca043f895b4377b0478b25499c05ec39392e4fcbf5c3eaa8e85f5ada5000d3f2c01ae84badeed4591326
7
- data.tar.gz: bb8ce35c75e8a43bdc135ddac20b94928bdbc1365b3351e7570f72c566001906506ecd32a98cde9fd3e47a1b8726058d235ce413afeac31a6d874ff9687d8096
6
+ metadata.gz: 46465038ab95d65913a9f359af9b21fcb5b3ee9a37877b32eeabd7943de6373625d7ae51a9383099a89bd7d949ee6b5db37c5518656313121d8991b4e52e7667
7
+ data.tar.gz: 4f7590242bb707d2ed0c9b22871f2e00d28f683224341387c739b9771a85ab18e8655e1a913b2bdabb724bb694cb6b997f4b1c2771fa52a2b96ac1882593471a
data/CHANGELOG.md CHANGED
@@ -1,182 +1,23 @@
1
- ## Rails 7.2.3.1 (March 23, 2026) ##
1
+ ## Rails 8.0.0.beta1 (September 26, 2024) ##
2
2
 
3
- * Filter user supplied metadata in DirectUploadController
3
+ * Deprecate `ActiveStorage::Service::AzureStorageService`.
4
4
 
5
- [CVE-2026-33173]
5
+ *zzak*
6
6
 
7
- *Jean Boussier*
7
+ * Improve `ActiveStorage::Filename#sanitized` method to handle special characters more effectively.
8
+ Replace the characters `"*?<>` with `-` if they exist in the Filename to match the Filename convention of Win OS.
8
9
 
9
- * Configurable maxmimum streaming chunk size
10
+ *Luong Viet Dung(Martin)*
10
11
 
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.
12
+ * Improve InvariableError, UnpreviewableError and UnrepresentableError message.
13
13
 
14
- [CVE-2026-33174]
14
+ Include Blob ID and content_type in the messages.
15
15
 
16
- *Gannon McGibbon*
16
+ *Petrik de Heus*
17
17
 
18
- * Limit range requests to a single range
18
+ * Mark proxied files as `immutable` in their Cache-Control header
19
19
 
20
- [CVE-2026-33658]
20
+ *Nate Matykiewicz*
21
21
 
22
- *Jean Boussier*
23
22
 
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
-
71
- ## Rails 7.2.2.2 (August 13, 2025) ##
72
-
73
- * Remove dangerous transformations
74
-
75
- [CVE-2025-24293]
76
-
77
- *Zack Deveau*
78
-
79
-
80
- ## Rails 7.2.2.1 (December 10, 2024) ##
81
-
82
- * No changes.
83
-
84
-
85
- ## Rails 7.2.2 (October 30, 2024) ##
86
-
87
- * No changes.
88
-
89
-
90
- ## Rails 7.2.1.2 (October 23, 2024) ##
91
-
92
- * No changes.
93
-
94
-
95
- ## Rails 7.2.1.1 (October 15, 2024) ##
96
-
97
- * No changes.
98
-
99
-
100
- ## Rails 7.2.1 (August 22, 2024) ##
101
-
102
- * No changes.
103
-
104
-
105
- ## Rails 7.2.0 (August 09, 2024) ##
106
-
107
- * Remove deprecated `config.active_storage.silence_invalid_content_types_warning`.
108
-
109
- *Rafael Mendonça França*
110
-
111
- * Remove deprecated `config.active_storage.replace_on_assign_to_many`.
112
-
113
- *Rafael Mendonça França*
114
-
115
- * Add support for custom `key` in `ActiveStorage::Blob#compose`.
116
-
117
- *Elvin Efendiev*
118
-
119
- * Add `image/webp` to `config.active_storage.web_image_content_types` when `load_defaults "7.2"`
120
- is set.
121
-
122
- *Lewis Buckley*
123
-
124
- * Fix JSON-encoding of `ActiveStorage::Filename` instances.
125
-
126
- *Jonathan del Strother*
127
-
128
- * Fix N+1 query when fetching preview images for non-image assets.
129
-
130
- *Aaron Patterson & Justin Searls*
131
-
132
- * Fix all Active Storage database related models to respect
133
- `ActiveRecord::Base.table_name_prefix` configuration.
134
-
135
- *Chedli Bourguiba*
136
-
137
- * Fix `ActiveStorage::Representations::ProxyController` not returning the proper
138
- preview image variant for previewable files.
139
-
140
- *Chedli Bourguiba*
141
-
142
- * Fix `ActiveStorage::Representations::ProxyController` to proxy untracked
143
- variants.
144
-
145
- *Chedli Bourguiba*
146
-
147
- * When using the `preprocessed: true` option, avoid enqueuing transform jobs
148
- for blobs that are not representable.
149
-
150
- *Chedli Bourguiba*
151
-
152
- * Prevent `ActiveStorage::Blob#preview` to generate a variant if an empty variation is passed.
153
-
154
- Calls to `#url`, `#key` or `#download` will now use the original preview
155
- image instead of generating a variant with the exact same dimensions.
156
-
157
- *Chedli Bourguiba*
158
-
159
- * Process preview image variant when calling `ActiveStorage::Preview#processed`.
160
-
161
- For example, `attached_pdf.preview(:thumb).processed` will now immediately
162
- generate the full-sized preview image and the `:thumb` variant of it.
163
- Previously, the `:thumb` variant would not be generated until a further call
164
- to e.g. `processed.url`.
165
-
166
- *Chedli Bourguiba* and *Jonathan Hefner*
167
-
168
- * Prevent `ActiveRecord::StrictLoadingViolationError` when strict loading is
169
- enabled and the variant of an Active Storage preview has already been
170
- processed (for example, by calling `ActiveStorage::Preview#url`).
171
-
172
- *Jonathan Hefner*
173
-
174
- * Fix `preprocessed: true` option for named variants of previewable files.
175
-
176
- *Nico Wenterodt*
177
-
178
- * Allow accepting `service` as a proc as well in `has_one_attached` and `has_many_attached`.
179
-
180
- *Yogesh Khater*
181
-
182
- Please check [7-1-stable](https://github.com/rails/rails/blob/7-1-stable/activestorage/CHANGELOG.md) for previous changes.
23
+ Please check [7-2-stable](https://github.com/rails/rails/blob/7-2-stable/activestorage/CHANGELOG.md) for previous changes.
data/README.md CHANGED
@@ -73,7 +73,7 @@ end
73
73
  ```erb
74
74
  <%= form_with model: @message, local: true do |form| %>
75
75
  <%= form.text_field :title, placeholder: "Title" %><br>
76
- <%= form.text_area :content %><br><br>
76
+ <%= form.textarea :content %><br><br>
77
77
 
78
78
  <%= form.file_field :images, multiple: true %><br>
79
79
  <%= form.submit %>
@@ -88,7 +88,7 @@ class MessagesController < ApplicationController
88
88
  end
89
89
 
90
90
  def create
91
- message = Message.create! params.require(:message).permit(:title, :content, images: [])
91
+ message = Message.create! params.expect(message: [ :title, :content, images: [] ])
92
92
  redirect_to message
93
93
  end
94
94
 
@@ -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 rubyonrails-core forum here:
206
+ Feature requests should be discussed on the rails-core mailing list here:
207
207
 
208
208
  * https://discuss.rubyonrails.org/c/rubyonrails-core
@@ -845,4 +845,4 @@ function autostart() {
845
845
 
846
846
  setTimeout(autostart, 1);
847
847
 
848
- export { DirectUpload, DirectUploadController, DirectUploadsController, start };
848
+ export { DirectUpload, DirectUploadController, DirectUploadsController, dispatchEvent, start };
@@ -822,6 +822,7 @@
822
822
  exports.DirectUpload = DirectUpload;
823
823
  exports.DirectUploadController = DirectUploadController;
824
824
  exports.DirectUploadsController = DirectUploadsController;
825
+ exports.dispatchEvent = dispatchEvent;
825
826
  exports.start = start;
826
827
  Object.defineProperty(exports, "__esModule", {
827
828
  value: true
@@ -11,7 +11,7 @@ class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
11
11
 
12
12
  private
13
13
  def blob_args
14
- params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, metadata: {}).to_h.symbolize_keys
14
+ params.expect(blob: [:filename, :byte_size, :checksum, :content_type, metadata: {}]).to_h.symbolize_keys
15
15
  end
16
16
 
17
17
  def direct_upload_json(blob)
@@ -17,8 +17,6 @@ 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
22
20
  end
23
21
 
24
22
  def update
@@ -27,15 +25,13 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
27
25
  named_disk_service(token[:service_name]).upload token[:key], request.body, checksum: token[:checksum]
28
26
  head :no_content
29
27
  else
30
- head ActionDispatch::Constants::UNPROCESSABLE_CONTENT
28
+ head :unprocessable_entity
31
29
  end
32
30
  else
33
31
  head :not_found
34
32
  end
35
33
  rescue ActiveStorage::IntegrityError
36
- head ActionDispatch::Constants::UNPROCESSABLE_CONTENT
37
- rescue ActiveStorage::InvalidKeyError
38
- head ActionDispatch::Constants::UNPROCESSABLE_CONTENT
34
+ head :unprocessable_entity
39
35
  end
40
36
 
41
37
  private
@@ -14,8 +14,7 @@ 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) unless ranges_valid?(ranges)
18
- return head(:range_not_satisfiable) if ranges.length > ActiveStorage.streaming_max_ranges
17
+ return head(:range_not_satisfiable) if ranges.blank? || ranges.all?(&:blank?)
19
18
 
20
19
  if ranges.length == 1
21
20
  range = ranges.first
@@ -52,12 +51,6 @@ module ActiveStorage::Streaming
52
51
  )
53
52
  end
54
53
 
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
-
61
54
  # Stream the blob from storage directly to the response. The disposition can be controlled by setting +disposition+.
62
55
  # The content type and filename is set directly from the +blob+.
63
56
  def send_blob_stream(blob, disposition: nil) # :doc:
@@ -68,6 +61,15 @@ module ActiveStorage::Streaming
68
61
  blob.download do |chunk|
69
62
  stream.write chunk
70
63
  end
64
+ rescue ActiveStorage::FileNotFoundError
65
+ expires_now
66
+ head :not_found
67
+ rescue
68
+ # Status and caching headers are already set, but not commited.
69
+ # Change the status to 500 manually.
70
+ expires_now
71
+ head :internal_server_error
72
+ raise
71
73
  end
72
74
  end
73
75
  end
@@ -2,7 +2,8 @@ import { start } from "./ujs"
2
2
  import { DirectUpload } from "./direct_upload"
3
3
  import { DirectUploadController } from "./direct_upload_controller"
4
4
  import { DirectUploadsController } from "./direct_uploads_controller"
5
- export { start, DirectUpload, DirectUploadController, DirectUploadsController }
5
+ import { dispatchEvent } from "./helpers"
6
+ export { start, DirectUpload, DirectUploadController, DirectUploadsController, dispatchEvent }
6
7
 
7
8
  function autostart() {
8
9
  if (window.ActiveStorage) {
@@ -31,77 +31,11 @@ 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
- #
100
34
  def variant(transformations)
101
35
  if variable?
102
36
  variant_class.new(self, ActiveStorage::Variation.wrap(transformations).default_to(default_variant_transformations))
103
37
  else
104
- raise ActiveStorage::InvariableError
38
+ raise ActiveStorage::InvariableError, "Can't transform blob with ID=#{id} and content_type=#{content_type}"
105
39
  end
106
40
  end
107
41
 
@@ -130,7 +64,7 @@ module ActiveStorage::Blob::Representable
130
64
  if previewable?
131
65
  ActiveStorage::Preview.new(self, transformations)
132
66
  else
133
- raise ActiveStorage::UnpreviewableError
67
+ raise ActiveStorage::UnpreviewableError, "No previewer found for blob with ID=#{id} and content_type=#{content_type}"
134
68
  end
135
69
  end
136
70
 
@@ -155,7 +89,7 @@ module ActiveStorage::Blob::Representable
155
89
  when variable?
156
90
  variant transformations
157
91
  else
158
- raise ActiveStorage::UnrepresentableError
92
+ raise ActiveStorage::UnrepresentableError, "No previewer found and can't transform blob with ID=#{id} and content_type=#{content_type}"
159
93
  end
160
94
  end
161
95
 
@@ -16,18 +16,10 @@
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.
22
19
  class ActiveStorage::Blob < ActiveStorage::Record
23
20
  MINIMUM_TOKEN_LENGTH = 28
24
21
 
25
22
  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
31
23
  store :metadata, accessors: [ :analyzed, :identified, :composed ], coder: ActiveRecord::Coders::JSON
32
24
 
33
25
  class_attribute :services, default: {}
@@ -37,7 +29,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
37
29
  # :method:
38
30
  #
39
31
  # Returns the associated ActiveStorage::Attachment instances.
40
- has_many :attachments, autosave: false
32
+ has_many :attachments
41
33
 
42
34
  ##
43
35
  # :singleton-method:
@@ -79,9 +71,8 @@ class ActiveStorage::Blob < ActiveStorage::Record
79
71
  end
80
72
 
81
73
  # Works like +find_signed+, but will raise an +ActiveSupport::MessageVerifier::InvalidSignature+
82
- # exception if the +signed_id+ has either expired, has a purpose mismatch, is for another record,
83
- # or has been tampered with. It will also raise an +ActiveRecord::RecordNotFound+ exception if
84
- # the valid signed id can't find a record.
74
+ # exception if the +signed_id+ has either expired, has a purpose mismatch, or has been tampered with.
75
+ # It will also raise an +ActiveRecord::RecordNotFound+ exception if the valid signed id can't find a record.
85
76
  def find_signed!(id, record: nil, purpose: :blob_id)
86
77
  super(id, purpose: purpose)
87
78
  end
@@ -101,9 +92,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
101
92
  # be saved before the upload begins to prevent the upload clobbering another due to key collisions.
102
93
  # When providing a content type, pass <tt>identify: false</tt> to bypass
103
94
  # 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.
107
95
  def create_and_upload!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil)
108
96
  create_after_unfurling!(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify).tap do |blob|
109
97
  blob.upload_without_unfurling(io)
@@ -116,7 +104,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
116
104
  # Once the form using the direct upload is submitted, the blob can be associated with the right record using
117
105
  # the signed ID.
118
106
  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)
120
107
  create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata, service_name: service_name
121
108
  end
122
109
 
@@ -165,14 +152,21 @@ class ActiveStorage::Blob < ActiveStorage::Record
165
152
  end
166
153
  end
167
154
 
168
- private
169
- def filter_metadata(metadata)
170
- if metadata.is_a?(Hash)
171
- metadata.without(*PROTECTED_METADATA)
172
- else
173
- metadata
155
+ def validate_service_configuration(service_name, model_class, association_name) # :nodoc:
156
+ if service_name
157
+ services.fetch(service_name) do
158
+ raise ArgumentError, "Cannot configure service #{service_name.inspect} for #{model_class}##{association_name}"
174
159
  end
160
+ else
161
+ validate_global_service_configuration
175
162
  end
163
+ end
164
+
165
+ def validate_global_service_configuration # :nodoc:
166
+ if connected? && table_exists? && Rails.configuration.active_storage.service.nil?
167
+ raise RuntimeError, "Missing Active Storage service name. Specify Active Storage service name for config.active_storage.service in config/environments/#{Rails.env}.rb"
168
+ end
169
+ end
176
170
  end
177
171
 
178
172
  include Analyzable
@@ -57,7 +57,7 @@ class ActiveStorage::Filename
57
57
  #
58
58
  # Characters considered unsafe for storage (e.g. \, $, and the RTL override character) are replaced with a dash.
59
59
  def sanitized
60
- @filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-")
60
+ @filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/<>?*\"\t\r\n\\", "-")
61
61
  end
62
62
 
63
63
  # Returns the sanitized version of the filename.
@@ -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
- Attached::Model.validate_service_configuration(service_name, record.class, name)
124
+ ActiveStorage::Blob.validate_service_configuration(service_name, record.class, name)
125
125
  end
126
126
  service_name
127
127
  end
@@ -61,18 +61,16 @@ module ActiveStorage
61
61
  # There is no column defined on the model side, Active Storage takes
62
62
  # care of the mapping between your records and the attachment.
63
63
  #
64
- # To avoid N+1 queries, you can include the attached blobs in your query like so:
65
- #
66
- # User.with_attached_avatar
67
- #
68
64
  # Under the covers, this relationship is implemented as a +has_one+ association to a
69
65
  # ActiveStorage::Attachment record and a +has_one-through+ association to a
70
66
  # ActiveStorage::Blob record. These associations are available as +avatar_attachment+
71
67
  # and +avatar_blob+. But you shouldn't need to work with these associations directly in
72
68
  # most circumstances.
73
69
  #
74
- # The system has been designed to having you go through the ActiveStorage::Attached::One
75
- # proxy that provides the dynamic proxy to the associations and factory methods, like +attach+.
70
+ # Instead, +has_one_attached+ generates an ActiveStorage::Attached::One proxy to
71
+ # provide access to the associations and factory methods, like +attach+:
72
+ #
73
+ # user.avatar.attach(uploaded_file)
76
74
  #
77
75
  # The +:dependent+ option defaults to +:purge_later+. This means the attachment will be
78
76
  # purged (i.e. destroyed) in the background whenever the record is destroyed.
@@ -92,6 +90,10 @@ module ActiveStorage
92
90
  # has_one_attached :avatar, service: ->(user) { user.in_europe_region? ? :s3_europe : :s3_usa }
93
91
  # end
94
92
  #
93
+ # To avoid N+1 queries, you can include the attached blobs in your query like so:
94
+ #
95
+ # User.with_attached_avatar
96
+ #
95
97
  # If you need to enable +strict_loading+ to prevent lazy loading of attachment,
96
98
  # pass the +:strict_loading+ option. You can do:
97
99
  #
@@ -104,7 +106,7 @@ module ActiveStorage
104
106
  # <tt>active_storage_attachments.record_type</tt> polymorphic type column of
105
107
  # the corresponding rows.
106
108
  def has_one_attached(name, dependent: :purge_later, service: nil, strict_loading: false)
107
- Attached::Model.validate_service_configuration(service, self, name) unless service.is_a?(Proc)
109
+ ActiveStorage::Blob.validate_service_configuration(service, self, name) unless service.is_a?(Proc)
108
110
 
109
111
  generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
110
112
  # frozen_string_literal: true
@@ -161,18 +163,16 @@ module ActiveStorage
161
163
  # There are no columns defined on the model side, Active Storage takes
162
164
  # care of the mapping between your records and the attachments.
163
165
  #
164
- # To avoid N+1 queries, you can include the attached blobs in your query like so:
165
- #
166
- # Gallery.where(user: Current.user).with_attached_photos
167
- #
168
166
  # Under the covers, this relationship is implemented as a +has_many+ association to a
169
167
  # ActiveStorage::Attachment record and a +has_many-through+ association to a
170
168
  # ActiveStorage::Blob record. These associations are available as +photos_attachments+
171
169
  # and +photos_blobs+. But you shouldn't need to work with these associations directly in
172
170
  # most circumstances.
173
171
  #
174
- # The system has been designed to having you go through the ActiveStorage::Attached::Many
175
- # proxy that provides the dynamic proxy to the associations and factory methods, like +#attach+.
172
+ # Instead, +has_many_attached+ generates an ActiveStorage::Attached::Many proxy to
173
+ # provide access to the associations and factory methods, like +attach+:
174
+ #
175
+ # user.photos.attach(uploaded_file)
176
176
  #
177
177
  # The +:dependent+ option defaults to +:purge_later+. This means the attachments will be
178
178
  # purged (i.e. destroyed) in the background whenever the record is destroyed.
@@ -192,6 +192,10 @@ module ActiveStorage
192
192
  # has_many_attached :photos, service: ->(gallery) { gallery.personal? ? :personal_s3 : :s3 }
193
193
  # end
194
194
  #
195
+ # To avoid N+1 queries, you can include the attached blobs in your query like so:
196
+ #
197
+ # Gallery.where(user: Current.user).with_attached_photos
198
+ #
195
199
  # If you need to enable +strict_loading+ to prevent lazy loading of attachments,
196
200
  # pass the +:strict_loading+ option. You can do:
197
201
  #
@@ -204,7 +208,7 @@ module ActiveStorage
204
208
  # <tt>active_storage_attachments.record_type</tt> polymorphic type column of
205
209
  # the corresponding rows.
206
210
  def has_many_attached(name, dependent: :purge_later, service: nil, strict_loading: false)
207
- Attached::Model.validate_service_configuration(service, self, name) unless service.is_a?(Proc)
211
+ ActiveStorage::Blob.validate_service_configuration(service, self, name) unless service.is_a?(Proc)
208
212
 
209
213
  generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
210
214
  # frozen_string_literal: true
@@ -255,25 +259,6 @@ module ActiveStorage
255
259
  end
256
260
  end
257
261
 
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
-
277
262
  def attachment_changes # :nodoc:
278
263
  @attachment_changes ||= {}
279
264
  end
@@ -84,10 +84,6 @@ 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
-
91
87
  config.after_initialize do |app|
92
88
  ActiveStorage.logger = app.config.active_storage.logger || Rails.logger
93
89
  ActiveStorage.variant_processor = app.config.active_storage.variant_processor || :mini_magick
@@ -116,13 +112,13 @@ module ActiveStorage
116
112
  ActiveStorage.variable_content_types = app.config.active_storage.variable_content_types || []
117
113
  ActiveStorage.web_image_content_types = app.config.active_storage.web_image_content_types || []
118
114
  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
119
116
  ActiveStorage.service_urls_expire_in = app.config.active_storage.service_urls_expire_in || 5.minutes
120
117
  ActiveStorage.urls_expire_in = app.config.active_storage.urls_expire_in
121
118
  ActiveStorage.content_types_allowed_inline = app.config.active_storage.content_types_allowed_inline || []
122
119
  ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream"
123
120
  ActiveStorage.video_preview_arguments = app.config.active_storage.video_preview_arguments || "-y -vframes 1 -f image2"
124
121
  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
126
122
  end
127
123
  end
128
124
 
@@ -26,8 +26,4 @@ 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
33
29
  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(
@@ -7,10 +7,10 @@ module ActiveStorage
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 7
11
- MINOR = 2
12
- TINY = 3
13
- PRE = "1"
10
+ MAJOR = 8
11
+ MINOR = 0
12
+ TINY = 0
13
+ PRE = "beta1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -15,6 +15,13 @@ module ActiveStorage
15
15
  attr_reader :client, :container, :signer
16
16
 
17
17
  def initialize(storage_account_name:, storage_access_key:, container:, public: false, **options)
18
+ ActiveStorage.deprecator.warn <<~MSG.squish
19
+ `ActiveStorage::Service::AzureStorageService` is deprecated and will be
20
+ removed in Rails 8.1.
21
+ Please try the `azure-blob` gem instead.
22
+ This gem is not maintained by the Rails team, so please test your applications before deploying to production.
23
+ MSG
24
+
18
25
  @client = Azure::Storage::Blob::BlobService.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key, **options)
19
26
  @signer = Azure::Storage::Common::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key)
20
27
  @container = container
@@ -60,16 +60,7 @@ module ActiveStorage
60
60
 
61
61
  def delete_prefixed(prefix)
62
62
  instrument :delete_prefixed, prefix: prefix do
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|
63
+ Dir.glob(path_for("#{prefix}*")).each do |path|
73
64
  FileUtils.rm_rf(path)
74
65
  end
75
66
  end
@@ -107,39 +98,8 @@ module ActiveStorage
107
98
  { "Content-Type" => content_type }
108
99
  end
109
100
 
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.
114
101
  def path_for(key) # :nodoc:
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
102
+ File.join root, folder_for(key), key
143
103
  end
144
104
 
145
105
  def compose(source_keys, destination_key, **)
@@ -196,10 +156,6 @@ module ActiveStorage
196
156
  [ key[0..1], key[2..3] ].join("/")
197
157
  end
198
158
 
199
- def escape_glob_metacharacters(path)
200
- path.gsub(/[\[\]*?{}\\]/) { |c| "\\#{c}" }
201
- end
202
-
203
159
  def make_path_for(key)
204
160
  path_for(key).tap { |path| FileUtils.mkdir_p File.dirname(path) }
205
161
  end
@@ -30,6 +30,13 @@ module ActiveStorage
30
30
 
31
31
  def initialize(primary:, mirrors:)
32
32
  @primary, @mirrors = primary, mirrors
33
+ @executor = Concurrent::ThreadPoolExecutor.new(
34
+ min_threads: 1,
35
+ max_threads: mirrors.size,
36
+ max_queue: 0,
37
+ fallback_policy: :caller_runs,
38
+ idle_time: 60
39
+ )
33
40
  end
34
41
 
35
42
  # Upload the +io+ to the +key+ specified to all services. The upload to the primary service is done synchronously
@@ -75,10 +82,12 @@ module ActiveStorage
75
82
  end
76
83
 
77
84
  def perform_across_services(method, *args)
78
- # FIXME: Convert to be threaded
79
- each_service.collect do |service|
80
- service.public_send method, *args
85
+ tasks = each_service.collect do |service|
86
+ Concurrent::Promise.execute(executor: @executor) do
87
+ service.public_send method, *args
88
+ end
81
89
  end
90
+ tasks.each(&:value!)
82
91
  end
83
92
  end
84
93
  end
@@ -16,7 +16,6 @@ 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)
20
19
  @bucket = @client.bucket(bucket)
21
20
 
22
21
  @multipart_upload_threshold = upload.delete(:multipart_threshold) || 100.megabytes
@@ -101,8 +100,7 @@ module ActiveStorage
101
100
  def compose(source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
102
101
  content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
103
102
 
104
- upload_stream(
105
- key: destination_key,
103
+ object_for(destination_key).upload_stream(
106
104
  content_type: content_type,
107
105
  content_disposition: content_disposition,
108
106
  part_size: MINIMUM_UPLOAD_PART_SIZE,
@@ -118,14 +116,6 @@ module ActiveStorage
118
116
  end
119
117
 
120
118
  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
-
129
119
  def private_url(key, expires_in:, filename:, disposition:, content_type:, **client_opts)
130
120
  object_for(key).presigned_url :get, expires_in: expires_in.to_i,
131
121
  response_content_disposition: content_disposition_with(type: disposition, filename: filename),
@@ -136,6 +126,7 @@ module ActiveStorage
136
126
  object_for(key).public_url(**client_opts)
137
127
  end
138
128
 
129
+
139
130
  MAXIMUM_UPLOAD_PARTS_COUNT = 10000
140
131
  MINIMUM_UPLOAD_PART_SIZE = 5.megabytes
141
132
 
@@ -148,18 +139,12 @@ module ActiveStorage
148
139
  def upload_with_multipart(key, io, content_type: nil, content_disposition: nil, custom_metadata: {})
149
140
  part_size = [ io.size.fdiv(MAXIMUM_UPLOAD_PARTS_COUNT).ceil, MINIMUM_UPLOAD_PART_SIZE ].max
150
141
 
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|
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|
159
143
  IO.copy_stream(io, out)
160
144
  end
161
145
  end
162
146
 
147
+
163
148
  def object_for(key)
164
149
  bucket.object(key)
165
150
  end
@@ -27,7 +27,6 @@ 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"
31
30
 
32
31
  require "active_storage/version"
33
32
  require "active_storage/deprecator"
@@ -73,6 +72,7 @@ module ActiveStorage
73
72
  "annotate",
74
73
  "antialias",
75
74
  "append",
75
+ "apply",
76
76
  "attenuate",
77
77
  "authenticate",
78
78
  "auto_gamma",
@@ -213,6 +213,7 @@ module ActiveStorage
213
213
  "linewidth",
214
214
  "liquid_rescale",
215
215
  "list",
216
+ "loader",
216
217
  "log",
217
218
  "loop",
218
219
  "lowlight_color",
@@ -275,6 +276,7 @@ module ActiveStorage
275
276
  "rotate",
276
277
  "sample",
277
278
  "sampling_factor",
279
+ "saver",
278
280
  "scale",
279
281
  "scene",
280
282
  "screen",
@@ -351,7 +353,6 @@ module ActiveStorage
351
353
  ]
352
354
  mattr_accessor :unsupported_image_processing_arguments
353
355
 
354
- mattr_accessor :streaming_chunk_max_size, default: 100.megabytes
355
356
  mattr_accessor :service_urls_expire_in, default: 5.minutes
356
357
  mattr_accessor :touch_attachment_records, default: true
357
358
  mattr_accessor :urls_expire_in
@@ -362,18 +363,6 @@ module ActiveStorage
362
363
 
363
364
  mattr_accessor :track_variants, default: false
364
365
 
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
-
377
366
  mattr_accessor :video_preview_arguments, default: "-y -vframes 1 -f image2"
378
367
 
379
368
  module Transformers
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activestorage
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.2.3.1
4
+ version: 8.0.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2024-09-26 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: activesupport
@@ -15,56 +16,56 @@ dependencies:
15
16
  requirements:
16
17
  - - '='
17
18
  - !ruby/object:Gem::Version
18
- version: 7.2.3.1
19
+ version: 8.0.0.beta1
19
20
  type: :runtime
20
21
  prerelease: false
21
22
  version_requirements: !ruby/object:Gem::Requirement
22
23
  requirements:
23
24
  - - '='
24
25
  - !ruby/object:Gem::Version
25
- version: 7.2.3.1
26
+ version: 8.0.0.beta1
26
27
  - !ruby/object:Gem::Dependency
27
28
  name: actionpack
28
29
  requirement: !ruby/object:Gem::Requirement
29
30
  requirements:
30
31
  - - '='
31
32
  - !ruby/object:Gem::Version
32
- version: 7.2.3.1
33
+ version: 8.0.0.beta1
33
34
  type: :runtime
34
35
  prerelease: false
35
36
  version_requirements: !ruby/object:Gem::Requirement
36
37
  requirements:
37
38
  - - '='
38
39
  - !ruby/object:Gem::Version
39
- version: 7.2.3.1
40
+ version: 8.0.0.beta1
40
41
  - !ruby/object:Gem::Dependency
41
42
  name: activejob
42
43
  requirement: !ruby/object:Gem::Requirement
43
44
  requirements:
44
45
  - - '='
45
46
  - !ruby/object:Gem::Version
46
- version: 7.2.3.1
47
+ version: 8.0.0.beta1
47
48
  type: :runtime
48
49
  prerelease: false
49
50
  version_requirements: !ruby/object:Gem::Requirement
50
51
  requirements:
51
52
  - - '='
52
53
  - !ruby/object:Gem::Version
53
- version: 7.2.3.1
54
+ version: 8.0.0.beta1
54
55
  - !ruby/object:Gem::Dependency
55
56
  name: activerecord
56
57
  requirement: !ruby/object:Gem::Requirement
57
58
  requirements:
58
59
  - - '='
59
60
  - !ruby/object:Gem::Version
60
- version: 7.2.3.1
61
+ version: 8.0.0.beta1
61
62
  type: :runtime
62
63
  prerelease: false
63
64
  version_requirements: !ruby/object:Gem::Requirement
64
65
  requirements:
65
66
  - - '='
66
67
  - !ruby/object:Gem::Version
67
- version: 7.2.3.1
68
+ version: 8.0.0.beta1
68
69
  - !ruby/object:Gem::Dependency
69
70
  name: marcel
70
71
  requirement: !ruby/object:Gem::Requirement
@@ -189,11 +190,12 @@ licenses:
189
190
  - MIT
190
191
  metadata:
191
192
  bug_tracker_uri: https://github.com/rails/rails/issues
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/
193
+ changelog_uri: https://github.com/rails/rails/blob/v8.0.0.beta1/activestorage/CHANGELOG.md
194
+ documentation_uri: https://api.rubyonrails.org/v8.0.0.beta1/
194
195
  mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
195
- source_code_uri: https://github.com/rails/rails/tree/v7.2.3.1/activestorage
196
+ source_code_uri: https://github.com/rails/rails/tree/v8.0.0.beta1/activestorage
196
197
  rubygems_mfa_required: 'true'
198
+ post_install_message:
197
199
  rdoc_options: []
198
200
  require_paths:
199
201
  - lib
@@ -201,14 +203,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
201
203
  requirements:
202
204
  - - ">="
203
205
  - !ruby/object:Gem::Version
204
- version: 3.1.0
206
+ version: 3.2.0
205
207
  required_rubygems_version: !ruby/object:Gem::Requirement
206
208
  requirements:
207
209
  - - ">="
208
210
  - !ruby/object:Gem::Version
209
211
  version: '0'
210
212
  requirements: []
211
- rubygems_version: 4.0.6
213
+ rubygems_version: 3.5.16
214
+ signing_key:
212
215
  specification_version: 4
213
216
  summary: Local and cloud file storage framework.
214
217
  test_files: []