activestorage 7.2.2.2 → 8.0.3

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: 022126fe7ad33aff5dd19e9cb369c9a4264b20b87e33af853f07cf8946bc6b4a
4
+ data.tar.gz: e32c5d37045bb9e94d86ae04c24b290a538fe9931eca6d8debdbc8d5d049cdd4
5
5
  SHA512:
6
- metadata.gz: 1a3ff3b23455a2e472fc46dab1a947dab49032917c59a1844fee4d694445366df9ef2b99085de5d10c6107f0f1085179a303da233d6f953c9a37ddbdba70e5cc
7
- data.tar.gz: a6601fbc244adaf30ba1dac98f3062700195bfa5f728b9425976d2f14c89f6ee27223d25b00c1f7573022677e558f45a06487efba20fda259fb9006014af7a68
6
+ metadata.gz: 16d1b5343105d7b22c8b3e85b2d3200ab1fa9d4b88e0313a2154f26b0a8dcfcf3039ad86acaad6a631bccb6e91bf560f570c5e0ab9f962881edf7d52d75da5ba
7
+ data.tar.gz: f6c5e9f142ee4ddce2be8cb7ae89009d60249ab454160ed3ab4be9fdc55336d2021f9ba411957bdb1aa128d0c7de74d5b4a45e9b5cd4c26500abc75fe0903945
data/CHANGELOG.md CHANGED
@@ -1,111 +1,82 @@
1
- ## Rails 7.2.2.2 (August 13, 2025) ##
1
+ ## Rails 8.0.3 (September 22, 2025) ##
2
2
 
3
- Remove dangerous transformations
3
+ * Address deprecation of `Aws::S3::Object#upload_stream` in `ActiveStorage::Service::S3Service`.
4
4
 
5
- [CVE-2025-24293]
5
+ *Joshua Young*
6
6
 
7
- *Zack Deveau*
7
+ * Fix `config.active_storage.touch_attachment_records` to work with eager loading.
8
8
 
9
- ## Rails 7.2.2.1 (December 10, 2024) ##
9
+ *fatkodima*
10
10
 
11
- * No changes.
12
11
 
12
+ ## Rails 8.0.2.1 (August 13, 2025) ##
13
13
 
14
- ## Rails 7.2.2 (October 30, 2024) ##
14
+ * Remove dangerous transformations
15
15
 
16
- * No changes.
16
+ [CVE-2025-24293]
17
17
 
18
+ *Zack Deveau*
18
19
 
19
- ## Rails 7.2.1.2 (October 23, 2024) ##
20
+ ## Rails 8.0.2 (March 12, 2025) ##
20
21
 
21
- * No changes.
22
+ * A Blob will no longer autosave associated Attachment.
22
23
 
24
+ This fixes an issue where a record with an attachment would have
25
+ its dirty attributes reset, preventing your `after commit` callbacks
26
+ on that record to behave as expected.
23
27
 
24
- ## Rails 7.2.1.1 (October 15, 2024) ##
28
+ Note that this change doesn't require any changes on your application
29
+ and is supposed to be internal. Active Storage Attachment will continue
30
+ to be autosaved (through a different relation).
25
31
 
26
- * No changes.
32
+ *Edouard-chin*
27
33
 
28
34
 
29
- ## Rails 7.2.1 (August 22, 2024) ##
35
+ ## Rails 8.0.1 (December 13, 2024) ##
30
36
 
31
37
  * No changes.
32
38
 
33
39
 
34
- ## Rails 7.2.0 (August 09, 2024) ##
35
-
36
- * Remove deprecated `config.active_storage.silence_invalid_content_types_warning`.
37
-
38
- *Rafael Mendonça França*
39
-
40
- * Remove deprecated `config.active_storage.replace_on_assign_to_many`.
40
+ ## Rails 8.0.0.1 (December 10, 2024) ##
41
41
 
42
- *Rafael Mendonça França*
43
-
44
- * Add support for custom `key` in `ActiveStorage::Blob#compose`.
45
-
46
- *Elvin Efendiev*
47
-
48
- * Add `image/webp` to `config.active_storage.web_image_content_types` when `load_defaults "7.2"`
49
- is set.
50
-
51
- *Lewis Buckley*
52
-
53
- * Fix JSON-encoding of `ActiveStorage::Filename` instances.
54
-
55
- *Jonathan del Strother*
56
-
57
- * Fix N+1 query when fetching preview images for non-image assets.
42
+ * No changes.
58
43
 
59
- *Aaron Patterson & Justin Searls*
60
44
 
61
- * Fix all Active Storage database related models to respect
62
- `ActiveRecord::Base.table_name_prefix` configuration.
45
+ ## Rails 8.0.0 (November 07, 2024) ##
63
46
 
64
- *Chedli Bourguiba*
47
+ * No changes.
65
48
 
66
- * Fix `ActiveStorage::Representations::ProxyController` not returning the proper
67
- preview image variant for previewable files.
68
49
 
69
- *Chedli Bourguiba*
50
+ ## Rails 8.0.0.rc2 (October 30, 2024) ##
70
51
 
71
- * Fix `ActiveStorage::Representations::ProxyController` to proxy untracked
72
- variants.
52
+ * No changes.
73
53
 
74
- *Chedli Bourguiba*
75
54
 
76
- * When using the `preprocessed: true` option, avoid enqueuing transform jobs
77
- for blobs that are not representable.
55
+ ## Rails 8.0.0.rc1 (October 19, 2024) ##
78
56
 
79
- *Chedli Bourguiba*
57
+ * No changes.
80
58
 
81
- * Prevent `ActiveStorage::Blob#preview` to generate a variant if an empty variation is passed.
82
59
 
83
- Calls to `#url`, `#key` or `#download` will now use the original preview
84
- image instead of generating a variant with the exact same dimensions.
60
+ ## Rails 8.0.0.beta1 (September 26, 2024) ##
85
61
 
86
- *Chedli Bourguiba*
62
+ * Deprecate `ActiveStorage::Service::AzureStorageService`.
87
63
 
88
- * Process preview image variant when calling `ActiveStorage::Preview#processed`.
64
+ *zzak*
89
65
 
90
- For example, `attached_pdf.preview(:thumb).processed` will now immediately
91
- generate the full-sized preview image and the `:thumb` variant of it.
92
- Previously, the `:thumb` variant would not be generated until a further call
93
- to e.g. `processed.url`.
66
+ * Improve `ActiveStorage::Filename#sanitized` method to handle special characters more effectively.
67
+ Replace the characters `"*?<>` with `-` if they exist in the Filename to match the Filename convention of Win OS.
94
68
 
95
- *Chedli Bourguiba* and *Jonathan Hefner*
69
+ *Luong Viet Dung(Martin)*
96
70
 
97
- * Prevent `ActiveRecord::StrictLoadingViolationError` when strict loading is
98
- enabled and the variant of an Active Storage preview has already been
99
- processed (for example, by calling `ActiveStorage::Preview#url`).
71
+ * Improve InvariableError, UnpreviewableError and UnrepresentableError message.
100
72
 
101
- *Jonathan Hefner*
73
+ Include Blob ID and content_type in the messages.
102
74
 
103
- * Fix `preprocessed: true` option for named variants of previewable files.
75
+ *Petrik de Heus*
104
76
 
105
- *Nico Wenterodt*
77
+ * Mark proxied files as `immutable` in their Cache-Control header
106
78
 
107
- * Allow accepting `service` as a proc as well in `has_one_attached` and `has_many_attached`.
79
+ *Nate Matykiewicz*
108
80
 
109
- *Yogesh Khater*
110
81
 
111
- Please check [7-1-stable](https://github.com/rails/rails/blob/7-1-stable/activestorage/CHANGELOG.md) for previous changes.
82
+ 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 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
@@ -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)
@@ -25,13 +25,13 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
25
25
  named_disk_service(token[:service_name]).upload token[:key], request.body, checksum: token[:checksum]
26
26
  head :no_content
27
27
  else
28
- head :unprocessable_entity
28
+ head ActionDispatch::Constants::UNPROCESSABLE_CONTENT
29
29
  end
30
30
  else
31
31
  head :not_found
32
32
  end
33
33
  rescue ActiveStorage::IntegrityError
34
- head :unprocessable_entity
34
+ head ActionDispatch::Constants::UNPROCESSABLE_CONTENT
35
35
  end
36
36
 
37
37
  private
@@ -61,6 +61,15 @@ module ActiveStorage::Streaming
61
61
  blob.download do |chunk|
62
62
  stream.write chunk
63
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 committed.
69
+ # Change the status to 500 manually.
70
+ expires_now
71
+ head :internal_server_error
72
+ raise
64
73
  end
65
74
  end
66
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) {
@@ -25,17 +25,83 @@ module ActiveStorage::Blob::Representable
25
25
  #
26
26
  # <%= image_tag Current.user.avatar.variant(resize_to_limit: [100, 100]) %>
27
27
  #
28
- # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
29
- # can then produce on-demand.
28
+ # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::Representations::ProxyController
29
+ # or ActiveStorage::Representations::RedirectController can then produce on-demand.
30
30
  #
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))
37
103
  else
38
- raise ActiveStorage::InvariableError
104
+ raise ActiveStorage::InvariableError, "Can't transform blob with ID=#{id} and content_type=#{content_type}"
39
105
  end
40
106
  end
41
107
 
@@ -64,7 +130,7 @@ module ActiveStorage::Blob::Representable
64
130
  if previewable?
65
131
  ActiveStorage::Preview.new(self, transformations)
66
132
  else
67
- raise ActiveStorage::UnpreviewableError
133
+ raise ActiveStorage::UnpreviewableError, "No previewer found for blob with ID=#{id} and content_type=#{content_type}"
68
134
  end
69
135
  end
70
136
 
@@ -89,7 +155,7 @@ module ActiveStorage::Blob::Representable
89
155
  when variable?
90
156
  variant transformations
91
157
  else
92
- raise ActiveStorage::UnrepresentableError
158
+ raise ActiveStorage::UnrepresentableError, "No previewer found and can't transform blob with ID=#{id} and content_type=#{content_type}"
93
159
  end
94
160
  end
95
161
 
@@ -29,7 +29,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
29
29
  # :method:
30
30
  #
31
31
  # Returns the associated ActiveStorage::Attachment instances.
32
- has_many :attachments
32
+ has_many :attachments, autosave: false
33
33
 
34
34
  ##
35
35
  # :singleton-method:
@@ -71,9 +71,8 @@ class ActiveStorage::Blob < ActiveStorage::Record
71
71
  end
72
72
 
73
73
  # Works like +find_signed+, but will raise an +ActiveSupport::MessageVerifier::InvalidSignature+
74
- # exception if the +signed_id+ has either expired, has a purpose mismatch, is for another record,
75
- # or has been tampered with. It will also raise an +ActiveRecord::RecordNotFound+ exception if
76
- # 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.
77
76
  def find_signed!(id, record: nil, purpose: :blob_id)
78
77
  super(id, purpose: purpose)
79
78
  end
@@ -152,22 +151,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
152
151
  combined_blob.save!
153
152
  end
154
153
  end
155
-
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}"
160
- end
161
- else
162
- validate_global_service_configuration
163
- 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
154
  end
172
155
 
173
156
  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.
@@ -22,15 +22,15 @@
22
22
  # Note that to create a variant it's necessary to download the entire blob file from the service. Because of this process,
23
23
  # you also want to be considerate about when the variant is actually processed. You shouldn't be processing variants inline
24
24
  # in a template, for example. Delay the processing to an on-demand controller, like the one provided in
25
- # ActiveStorage::RepresentationsController.
25
+ # ActiveStorage::Representations::ProxyController and ActiveStorage::Representations::RedirectController.
26
26
  #
27
27
  # To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided
28
28
  # by Active Storage like so:
29
29
  #
30
30
  # <%= image_tag Current.user.avatar.variant(resize_to_limit: [100, 100]) %>
31
31
  #
32
- # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
33
- # can then produce on-demand.
32
+ # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::Representations::ProxyController
33
+ # or ActiveStorage::Representations::RedirectController can then produce on-demand.
34
34
  #
35
35
  # When you do want to actually produce the variant needed, call +processed+. This will check that the variant
36
36
  # has already been processed and uploaded to the service, and, if so, just return that. Otherwise it will perform
@@ -74,11 +74,11 @@ class ActiveStorage::Variant
74
74
  "variants/#{blob.key}/#{OpenSSL::Digest::SHA256.hexdigest(variation.key)}"
75
75
  end
76
76
 
77
- # Returns the URL of the blob variant on the service. See {ActiveStorage::Blob#url} for details.
77
+ # Returns the URL of the blob variant on the service. See ActiveStorage::Blob#url for details.
78
78
  #
79
79
  # Use <tt>url_for(variant)</tt> (or the implied form, like <tt>link_to variant</tt> or <tt>redirect_to variant</tt>) to get the stable URL
80
- # for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method
81
- # for its redirection.
80
+ # for a variant that points to the ActiveStorage::Representations::ProxyController or ActiveStorage::Representations::RedirectController,
81
+ # which in turn will use this +service_call+ method for its redirection.
82
82
  def url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline)
83
83
  service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
84
84
  end
@@ -32,7 +32,7 @@ module ActiveStorage
32
32
  {}
33
33
  end
34
34
  rescue ::Vips::Error => error
35
- logger.error "Skipping image analysis due to an Vips error: #{error.message}"
35
+ logger.error "Skipping image analysis due to a Vips error: #{error.message}"
36
36
  {}
37
37
  end
38
38
  end
@@ -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
@@ -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
- # Under the covers, this relationship is implemented as a +has_one+ association to a
69
- # ActiveStorage::Attachment record and a +has_one-through+ association to a
64
+ # Under the covers, this relationship is implemented as a +has_one+ association to an
65
+ # ActiveStorage::Attachment record and a +has_one-through+ association to an
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
- ActiveStorage::Blob.validate_service_configuration(service, self, name) unless service.is_a?(Proc)
109
+ Attached::Model.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
- # Under the covers, this relationship is implemented as a +has_many+ association to a
169
- # ActiveStorage::Attachment record and a +has_many-through+ association to a
166
+ # Under the covers, this relationship is implemented as a +has_many+ association to an
167
+ # ActiveStorage::Attachment record and a +has_many-through+ association to an
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
- ActiveStorage::Blob.validate_service_configuration(service, self, name) unless service.is_a?(Proc)
211
+ Attached::Model.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,6 +259,25 @@ module ActiveStorage
255
259
  end
256
260
  end
257
261
 
262
+ class << self
263
+ def validate_service_configuration(service_name, model_class, association_name) # :nodoc:
264
+ if service_name
265
+ ActiveStorage::Blob.services.fetch(service_name) do
266
+ raise ArgumentError, "Cannot configure service #{service_name.inspect} for #{model_class}##{association_name}"
267
+ end
268
+ else
269
+ validate_global_service_configuration(model_class)
270
+ end
271
+ end
272
+
273
+ private
274
+ def validate_global_service_configuration(model_class)
275
+ if model_class.connected? && ActiveStorage::Blob.table_exists? && Rails.configuration.active_storage.service.nil?
276
+ raise RuntimeError, "Missing Active Storage service name. Specify Active Storage service name for config.active_storage.service in config/environments/#{Rails.env}.rb"
277
+ end
278
+ end
279
+ end
280
+
258
281
  def attachment_changes # :nodoc:
259
282
  @attachment_changes ||= {}
260
283
  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,7 +116,6 @@ 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 || []
@@ -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 = 2
13
- PRE = "2"
10
+ MAJOR = 8
11
+ MINOR = 0
12
+ TINY = 3
13
+ PRE = nil
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
@@ -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,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
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: 8.0.3
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: 8.0.3
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: 8.0.3
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: 8.0.3
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: 8.0.3
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: 8.0.3
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: 8.0.3
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: 8.0.3
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: 8.0.3
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/v8.0.3/activestorage/CHANGELOG.md
193
+ documentation_uri: https://api.rubyonrails.org/v8.0.3/
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/v8.0.3/activestorage
196
196
  rubygems_mfa_required: 'true'
197
197
  rdoc_options: []
198
198
  require_paths:
@@ -201,7 +201,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
201
201
  requirements:
202
202
  - - ">="
203
203
  - !ruby/object:Gem::Version
204
- version: 3.1.0
204
+ version: 3.2.0
205
205
  required_rubygems_version: !ruby/object:Gem::Requirement
206
206
  requirements:
207
207
  - - ">="