activestorage 8.0.5 → 8.1.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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -115
  3. data/README.md +5 -2
  4. data/app/assets/javascripts/activestorage.esm.js +37 -1
  5. data/app/assets/javascripts/activestorage.js +37 -1
  6. data/app/controllers/active_storage/disk_controller.rb +0 -4
  7. data/app/controllers/concerns/active_storage/streaming.rb +1 -8
  8. data/app/javascript/activestorage/direct_upload_controller.js +48 -1
  9. data/app/models/active_storage/blob/representable.rb +2 -2
  10. data/app/models/active_storage/blob.rb +5 -26
  11. data/app/models/active_storage/filename.rb +1 -0
  12. data/app/models/active_storage/variant.rb +11 -11
  13. data/app/models/active_storage/variation.rb +1 -1
  14. data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +7 -11
  15. data/lib/active_storage/analyzer/image_analyzer/vips.rb +10 -11
  16. data/lib/active_storage/analyzer/image_analyzer.rb +5 -0
  17. data/lib/active_storage/attached.rb +0 -1
  18. data/lib/active_storage/downloader.rb +1 -1
  19. data/lib/active_storage/engine.rb +44 -10
  20. data/lib/active_storage/errors.rb +0 -4
  21. data/lib/active_storage/gem_version.rb +3 -3
  22. data/lib/active_storage/service/configurator.rb +6 -0
  23. data/lib/active_storage/service/disk_service.rb +3 -47
  24. data/lib/active_storage/service/gcs_service.rb +10 -2
  25. data/lib/active_storage/service/mirror_service.rb +1 -1
  26. data/lib/active_storage/service/registry.rb +6 -0
  27. data/lib/active_storage/service.rb +4 -1
  28. data/lib/active_storage/transformers/image_magick.rb +72 -0
  29. data/lib/active_storage/transformers/image_processing_transformer.rb +5 -67
  30. data/lib/active_storage/transformers/vips.rb +11 -0
  31. data/lib/active_storage.rb +4 -5
  32. metadata +15 -14
  33. data/lib/active_storage/service/azure_storage_service.rb +0 -201
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb8d04a9237523518b1a6b00aab432decc944e0432e7296d1f283126c73eb374
4
- data.tar.gz: 74fabd6db3bfefae7e51f9be208fafb37b76e5df3bdc238bafe60fa8357bd405
3
+ metadata.gz: 8959531cd3a50439e1d30882ac0398b7d361da8a6859049b01c31e1a35e58862
4
+ data.tar.gz: c4027f992ab9f941cecbe88a7a415d52f5a3800b86e69fe8789567f320a9b9ea
5
5
  SHA512:
6
- metadata.gz: e8555d671d585f19dcecb996f38d4aa4f182e3e98082856916e13cefa8f6f9c0288f11dbf83f7f80f641dbf82e5e93a39559bf7e86b98638b3737029c17660c4
7
- data.tar.gz: bd3b5a92c37258cecf1bca978f4a59a090c6bb20a30cf290479a173add2efa6de64ffe120d4649c1d4d9aa3b0e500c2ae7b24b44ee4b6413e65efafe7eac91e4
6
+ metadata.gz: 2750ae72f60c4e388b74813b74fa2580a5ebe48887eca53905b1d1173a08bac4ff931f22fadc3fb2ffec0d96417f7661fe22aba44cdaa36e7480c7df4b0f52d0
7
+ data.tar.gz: 3201606c2dce4e6e41649c8aca6432dbd2c0635c966366809bc6552ad07cc074596d3d973054bce87f79f3eca9ffb68d91e116d9b8f81ac5dad7d59b8040cd5b
data/CHANGELOG.md CHANGED
@@ -1,86 +1,51 @@
1
- ## Rails 8.0.5 (March 24, 2026) ##
1
+ ## Rails 8.1.0.beta1 (September 04, 2025) ##
2
2
 
3
- * Fix `ActiveStorage::Blob` content type predicate methods to handle `nil`.
3
+ * Remove deprecated `:azure` storage service.
4
4
 
5
- *Daichi KUDO*
5
+ *Rafael Mendonça França*
6
6
 
7
+ * Remove unnecessary calls to the GCP metadata server.
7
8
 
8
- ## Rails 8.0.4.1 (March 23, 2026) ##
9
+ Calling Google::Auth.get_application_default triggers an explicit call to
10
+ the metadata server - given it was being called for significant number of
11
+ file operations, it can lead to considerable tail latencies and even metadata
12
+ server overloads. Instead, it's preferable (and significantly more efficient)
13
+ that applications use:
9
14
 
10
- * Filter user supplied metadata in DirectUploadController
15
+ ```ruby
16
+ Google::Apis::RequestOptions.default.authorization = Google::Auth.get_application_default(...)
17
+ ```
11
18
 
12
- [CVE-2026-33173]
19
+ In the cases applications do not set that, the GCP libraries automatically determine credentials.
13
20
 
14
- *Jean Boussier*
21
+ This also enables using credentials other than those of the associated GCP
22
+ service account like when using impersonation.
15
23
 
16
- * Configurable maxmimum streaming chunk size
24
+ *Alex Coomans*
17
25
 
18
- Makes sure that byte ranges for blobs don't exceed 100mb by default.
19
- Content ranges that are too big can result in denial of service.
26
+ * Direct upload progress accounts for server processing time.
20
27
 
21
- [CVE-2026-33174]
28
+ *Jeremy Daer*
22
29
 
23
- *Gannon McGibbon*
30
+ * Delegate `ActiveStorage::Filename#to_str` to `#to_s`
24
31
 
25
- * Limit range requests to a single range
32
+ Supports checking String equality:
26
33
 
27
- [CVE-2026-33658]
34
+ ```ruby
35
+ filename = ActiveStorage::Filename.new("file.txt")
36
+ filename == "file.txt" # => true
37
+ filename in "file.txt" # => true
38
+ "file.txt" == filename # => true
39
+ ```
28
40
 
29
- *Jean Boussier*
41
+ *Sean Doyle*
30
42
 
31
- * Prevent path traversal in `DiskService`.
43
+ * Add support for alternative MD5 implementation through `config.active_storage.checksum_implementation`.
32
44
 
33
- `DiskService#path_for` now raises an `InvalidKeyError` when passed keys with dot segments (".",
34
- ".."), or if the resolved path is outside the storage root directory.
45
+ Also automatically degrade to using the slower `Digest::MD5` implementation if `OpenSSL::Digest::MD5`
46
+ is found to be disabled because of OpenSSL FIPS mode.
35
47
 
36
- `#path_for` also now consistently raises `InvalidKeyError` if the key is invalid in any way, for
37
- example containing null bytes or having an incompatible encoding. Previously, the exception
38
- raised may have been `ArgumentError` or `Encoding::CompatibilityError`.
39
-
40
- `DiskController` now explicitly rescues `InvalidKeyError` with appropriate HTTP status codes.
41
-
42
- [CVE-2026-33195]
43
-
44
- *Mike Dalessio*
45
-
46
- * Prevent glob injection in `DiskService#delete_prefixed`.
47
-
48
- Escape glob metacharacters in the resolved path before passing to `Dir.glob`.
49
-
50
- Note that this change breaks any existing code that is relying on `delete_prefixed` to expand
51
- glob metacharacters. This change presumes that is unintended behavior (as other storage services
52
- do not respect these metacharacters).
53
-
54
- [CVE-2026-33202]
55
-
56
- *Mike Dalessio*
57
-
58
-
59
- ## Rails 8.0.4 (October 28, 2025) ##
60
-
61
- * No changes.
62
-
63
-
64
- ## Rails 8.0.3 (September 22, 2025) ##
65
-
66
- * Address deprecation of `Aws::S3::Object#upload_stream` in `ActiveStorage::Service::S3Service`.
67
-
68
- *Joshua Young*
69
-
70
- * Fix `config.active_storage.touch_attachment_records` to work with eager loading.
71
-
72
- *fatkodima*
73
-
74
-
75
- ## Rails 8.0.2.1 (August 13, 2025) ##
76
-
77
- * Remove dangerous transformations
78
-
79
- [CVE-2025-24293]
80
-
81
- *Zack Deveau*
82
-
83
- ## Rails 8.0.2 (March 12, 2025) ##
48
+ *Matt Pasquini*, *Jean Boussier*
84
49
 
85
50
  * A Blob will no longer autosave associated Attachment.
86
51
 
@@ -94,52 +59,4 @@
94
59
 
95
60
  *Edouard-chin*
96
61
 
97
-
98
- ## Rails 8.0.1 (December 13, 2024) ##
99
-
100
- * No changes.
101
-
102
-
103
- ## Rails 8.0.0.1 (December 10, 2024) ##
104
-
105
- * No changes.
106
-
107
-
108
- ## Rails 8.0.0 (November 07, 2024) ##
109
-
110
- * No changes.
111
-
112
-
113
- ## Rails 8.0.0.rc2 (October 30, 2024) ##
114
-
115
- * No changes.
116
-
117
-
118
- ## Rails 8.0.0.rc1 (October 19, 2024) ##
119
-
120
- * No changes.
121
-
122
-
123
- ## Rails 8.0.0.beta1 (September 26, 2024) ##
124
-
125
- * Deprecate `ActiveStorage::Service::AzureStorageService`.
126
-
127
- *zzak*
128
-
129
- * Improve `ActiveStorage::Filename#sanitized` method to handle special characters more effectively.
130
- Replace the characters `"*?<>` with `-` if they exist in the Filename to match the Filename convention of Win OS.
131
-
132
- *Luong Viet Dung(Martin)*
133
-
134
- * Improve InvariableError, UnpreviewableError and UnrepresentableError message.
135
-
136
- Include Blob ID and content_type in the messages.
137
-
138
- *Petrik de Heus*
139
-
140
- * Mark proxied files as `immutable` in their Cache-Control header
141
-
142
- *Nate Matykiewicz*
143
-
144
-
145
- Please check [7-2-stable](https://github.com/rails/rails/blob/7-2-stable/activestorage/CHANGELOG.md) for previous changes.
62
+ Please check [8-0-stable](https://github.com/rails/rails/blob/8-0-stable/activestorage/CHANGELOG.md) for previous changes.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Active Storage
2
2
 
3
- Active Storage makes it simple to upload and reference files in cloud services like [Amazon S3](https://aws.amazon.com/s3/), [Google Cloud Storage](https://cloud.google.com/storage/docs/), or [Microsoft Azure Storage](https://azure.microsoft.com/en-us/services/storage/), and attach those files to Active Records. Supports having one main service and mirrors in other services for redundancy. It also provides a disk service for testing or local deployments, but the focus is on cloud storage.
3
+ Active Storage makes it simple to upload and reference files in cloud services like [Amazon S3](https://aws.amazon.com/s3/), or [Google Cloud Storage](https://cloud.google.com/storage/docs/), and attach those files to Active Records. Supports having one main service and mirrors in other services for redundancy. It also provides a disk service for testing or local deployments, but the focus is on cloud storage.
4
4
 
5
5
  Files can be uploaded from the server to the cloud or directly from the client to the cloud.
6
6
 
@@ -173,7 +173,10 @@ Active Storage, with its included JavaScript library, supports uploading directl
173
173
  ```erb
174
174
  <%= form.file_field :attachments, multiple: true, direct_upload: true %>
175
175
  ```
176
- 3. That's it! Uploads begin upon form submission.
176
+
177
+ 3. Configure CORS on third-party storage services to allow direct upload requests.
178
+
179
+ 4. That's it! Uploads begin upon form submission.
177
180
 
178
181
  ### Direct upload JavaScript events
179
182
 
@@ -672,7 +672,7 @@ class DirectUploadController {
672
672
  }));
673
673
  }
674
674
  uploadRequestDidProgress(event) {
675
- const progress = event.loaded / event.total * 100;
675
+ const progress = event.loaded / event.total * 90;
676
676
  if (progress) {
677
677
  this.dispatch("progress", {
678
678
  progress: progress
@@ -707,6 +707,42 @@ class DirectUploadController {
707
707
  xhr: xhr
708
708
  });
709
709
  xhr.upload.addEventListener("progress", (event => this.uploadRequestDidProgress(event)));
710
+ xhr.upload.addEventListener("loadend", (() => {
711
+ this.simulateResponseProgress(xhr);
712
+ }));
713
+ }
714
+ simulateResponseProgress(xhr) {
715
+ let progress = 90;
716
+ const startTime = Date.now();
717
+ const updateProgress = () => {
718
+ const elapsed = Date.now() - startTime;
719
+ const estimatedResponseTime = this.estimateResponseTime();
720
+ const responseProgress = Math.min(elapsed / estimatedResponseTime, 1);
721
+ progress = 90 + responseProgress * 9;
722
+ this.dispatch("progress", {
723
+ progress: progress
724
+ });
725
+ if (xhr.readyState !== XMLHttpRequest.DONE && progress < 99) {
726
+ requestAnimationFrame(updateProgress);
727
+ }
728
+ };
729
+ xhr.addEventListener("loadend", (() => {
730
+ this.dispatch("progress", {
731
+ progress: 100
732
+ });
733
+ }));
734
+ requestAnimationFrame(updateProgress);
735
+ }
736
+ estimateResponseTime() {
737
+ const fileSize = this.file.size;
738
+ const MB = 1024 * 1024;
739
+ if (fileSize < MB) {
740
+ return 1e3;
741
+ } else if (fileSize < 10 * MB) {
742
+ return 2e3;
743
+ } else {
744
+ return 3e3 + fileSize / MB * 50;
745
+ }
710
746
  }
711
747
  }
712
748
 
@@ -662,7 +662,7 @@
662
662
  }));
663
663
  }
664
664
  uploadRequestDidProgress(event) {
665
- const progress = event.loaded / event.total * 100;
665
+ const progress = event.loaded / event.total * 90;
666
666
  if (progress) {
667
667
  this.dispatch("progress", {
668
668
  progress: progress
@@ -697,6 +697,42 @@
697
697
  xhr: xhr
698
698
  });
699
699
  xhr.upload.addEventListener("progress", (event => this.uploadRequestDidProgress(event)));
700
+ xhr.upload.addEventListener("loadend", (() => {
701
+ this.simulateResponseProgress(xhr);
702
+ }));
703
+ }
704
+ simulateResponseProgress(xhr) {
705
+ let progress = 90;
706
+ const startTime = Date.now();
707
+ const updateProgress = () => {
708
+ const elapsed = Date.now() - startTime;
709
+ const estimatedResponseTime = this.estimateResponseTime();
710
+ const responseProgress = Math.min(elapsed / estimatedResponseTime, 1);
711
+ progress = 90 + responseProgress * 9;
712
+ this.dispatch("progress", {
713
+ progress: progress
714
+ });
715
+ if (xhr.readyState !== XMLHttpRequest.DONE && progress < 99) {
716
+ requestAnimationFrame(updateProgress);
717
+ }
718
+ };
719
+ xhr.addEventListener("loadend", (() => {
720
+ this.dispatch("progress", {
721
+ progress: 100
722
+ });
723
+ }));
724
+ requestAnimationFrame(updateProgress);
725
+ }
726
+ estimateResponseTime() {
727
+ const fileSize = this.file.size;
728
+ const MB = 1024 * 1024;
729
+ if (fileSize < MB) {
730
+ return 1e3;
731
+ } else if (fileSize < 10 * MB) {
732
+ return 2e3;
733
+ } else {
734
+ return 3e3 + fileSize / MB * 50;
735
+ }
700
736
  }
701
737
  }
702
738
  const inputSelector = "input[type=file][data-direct-upload-url]:not([disabled])";
@@ -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
@@ -34,8 +32,6 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
34
32
  end
35
33
  rescue ActiveStorage::IntegrityError
36
34
  head ActionDispatch::Constants::UNPROCESSABLE_CONTENT
37
- rescue ActiveStorage::InvalidKeyError
38
- head ActionDispatch::Constants::UNPROCESSABLE_CONTENT
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:
@@ -31,7 +31,8 @@ export class DirectUploadController {
31
31
  }
32
32
 
33
33
  uploadRequestDidProgress(event) {
34
- const progress = event.loaded / event.total * 100
34
+ // Scale upload progress to 0-90% range
35
+ const progress = (event.loaded / event.total) * 90
35
36
  if (progress) {
36
37
  this.dispatch("progress", { progress })
37
38
  }
@@ -63,5 +64,51 @@ export class DirectUploadController {
63
64
  directUploadWillStoreFileWithXHR(xhr) {
64
65
  this.dispatch("before-storage-request", { xhr })
65
66
  xhr.upload.addEventListener("progress", event => this.uploadRequestDidProgress(event))
67
+
68
+ // Start simulating progress after upload completes
69
+ xhr.upload.addEventListener("loadend", () => {
70
+ this.simulateResponseProgress(xhr)
71
+ })
72
+ }
73
+
74
+ simulateResponseProgress(xhr) {
75
+ let progress = 90
76
+ const startTime = Date.now()
77
+
78
+ const updateProgress = () => {
79
+ // Simulate progress from 90% to 99% over estimated time
80
+ const elapsed = Date.now() - startTime
81
+ const estimatedResponseTime = this.estimateResponseTime()
82
+ const responseProgress = Math.min(elapsed / estimatedResponseTime, 1)
83
+ progress = 90 + (responseProgress * 9) // 90% to 99%
84
+
85
+ this.dispatch("progress", { progress })
86
+
87
+ // Continue until response arrives or we hit 99%
88
+ if (xhr.readyState !== XMLHttpRequest.DONE && progress < 99) {
89
+ requestAnimationFrame(updateProgress)
90
+ }
91
+ }
92
+
93
+ // Stop simulation when response arrives
94
+ xhr.addEventListener("loadend", () => {
95
+ this.dispatch("progress", { progress: 100 })
96
+ })
97
+
98
+ requestAnimationFrame(updateProgress)
99
+ }
100
+
101
+ estimateResponseTime() {
102
+ // Base estimate: 1 second for small files, scaling up for larger files
103
+ const fileSize = this.file.size
104
+ const MB = 1024 * 1024
105
+
106
+ if (fileSize < MB) {
107
+ return 1000 // 1 second for files under 1MB
108
+ } else if (fileSize < 10 * MB) {
109
+ return 2000 // 2 seconds for files 1-10MB
110
+ } else {
111
+ return 3000 + (fileSize / MB * 50) // 3+ seconds for larger files
112
+ }
66
113
  }
67
114
  }
@@ -25,8 +25,8 @@ 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::Representations::ProxyController
29
- # or ActiveStorage::Representations::RedirectController can then produce on-demand.
28
+ # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
29
+ # 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
@@ -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: {}
@@ -100,9 +92,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
100
92
  # be saved before the upload begins to prevent the upload clobbering another due to key collisions.
101
93
  # When providing a content type, pass <tt>identify: false</tt> to bypass
102
94
  # automatic content type inference.
103
- #
104
- # The optional +key+ parameter is treated as trusted. Using untrusted user input
105
- # as the key may result in unexpected behavior.
106
95
  def create_and_upload!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil)
107
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|
108
97
  blob.upload_without_unfurling(io)
@@ -115,7 +104,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
115
104
  # Once the form using the direct upload is submitted, the blob can be associated with the right record using
116
105
  # the signed ID.
117
106
  def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, service_name: nil, record: nil)
118
- metadata = filter_metadata(metadata)
119
107
  create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata, service_name: service_name
120
108
  end
121
109
 
@@ -163,15 +151,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
163
151
  combined_blob.save!
164
152
  end
165
153
  end
166
-
167
- private
168
- def filter_metadata(metadata)
169
- if metadata.is_a?(Hash)
170
- metadata.without(*PROTECTED_METADATA)
171
- else
172
- metadata
173
- end
174
- end
175
154
  end
176
155
 
177
156
  include Analyzable
@@ -210,22 +189,22 @@ class ActiveStorage::Blob < ActiveStorage::Record
210
189
 
211
190
  # Returns true if the content_type of this blob is in the image range, like image/png.
212
191
  def image?
213
- content_type&.start_with?("image")
192
+ content_type.start_with?("image")
214
193
  end
215
194
 
216
195
  # Returns true if the content_type of this blob is in the audio range, like audio/mpeg.
217
196
  def audio?
218
- content_type&.start_with?("audio")
197
+ content_type.start_with?("audio")
219
198
  end
220
199
 
221
200
  # Returns true if the content_type of this blob is in the video range, like video/mp4.
222
201
  def video?
223
- content_type&.start_with?("video")
202
+ content_type.start_with?("video")
224
203
  end
225
204
 
226
205
  # Returns true if the content_type of this blob is in the text range, like text/plain.
227
206
  def text?
228
- content_type&.start_with?("text")
207
+ content_type.start_with?("text")
229
208
  end
230
209
 
231
210
  # Returns the URL of the blob on the service. This returns a permanent URL for public files, and returns a
@@ -353,7 +332,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
353
332
  def compute_checksum_in_chunks(io)
354
333
  raise ArgumentError, "io must be rewindable" unless io.respond_to?(:rewind)
355
334
 
356
- OpenSSL::Digest::MD5.new.tap do |checksum|
335
+ ActiveStorage.checksum_implementation.new.tap do |checksum|
357
336
  read_buffer = "".b
358
337
  while io.read(5.megabytes, read_buffer)
359
338
  checksum << read_buffer
@@ -64,6 +64,7 @@ class ActiveStorage::Filename
64
64
  def to_s
65
65
  sanitized.to_s
66
66
  end
67
+ alias_method :to_str, :to_s
67
68
 
68
69
  def as_json(*)
69
70
  to_s
@@ -8,29 +8,29 @@
8
8
  #
9
9
  # Variants rely on {ImageProcessing}[https://github.com/janko/image_processing] gem for the actual transformations
10
10
  # of the file, so you must add <tt>gem "image_processing"</tt> to your Gemfile if you wish to use variants. By
11
- # default, images will be processed with {ImageMagick}[http://imagemagick.org] using the
12
- # {MiniMagick}[https://github.com/minimagick/minimagick] gem, but you can also switch to the
13
- # {libvips}[http://libvips.github.io/libvips/] processor operated by the {ruby-vips}[https://github.com/libvips/ruby-vips]
11
+ # default, images will be processed with {libvips}[http://libvips.github.io/libvips/] using the
12
+ # {ruby-vips}[https://github.com/libvips/ruby-vips] gem, but you can also switch to the
13
+ # {ImageMagick}[http://imagemagick.org] processor operated by the {MiniMagick}[https://github.com/minimagick/minimagick]
14
14
  # gem).
15
15
  #
16
16
  # Rails.application.config.active_storage.variant_processor
17
- # # => :mini_magick
18
- #
19
- # Rails.application.config.active_storage.variant_processor = :vips
20
17
  # # => :vips
21
18
  #
19
+ # Rails.application.config.active_storage.variant_processor = :mini_magick
20
+ # # => :mini_magick
21
+ #
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::Representations::ProxyController and ActiveStorage::Representations::RedirectController.
25
+ # ActiveStorage::RepresentationsController.
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::Representations::ProxyController
33
- # or ActiveStorage::Representations::RedirectController can then produce on-demand.
32
+ # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
33
+ # 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
@@ -77,8 +77,8 @@ class ActiveStorage::Variant
77
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::Representations::ProxyController or ActiveStorage::Representations::RedirectController,
81
- # which in turn will use this +service_call+ method for its redirection.
80
+ # for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method
81
+ # 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
@@ -82,6 +82,6 @@ class ActiveStorage::Variation
82
82
 
83
83
  private
84
84
  def transformer
85
- ActiveStorage::Transformers::ImageProcessingTransformer.new(transformations.except(:format))
85
+ ActiveStorage.variant_transformer.new(transformations.except(:format))
86
86
  end
87
87
  end
@@ -1,22 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ begin
4
+ gem "mini_magick"
5
+ require "mini_magick"
6
+ rescue LoadError => error
7
+ raise error unless error.message.include?("mini_magick")
8
+ end
9
+
3
10
  module ActiveStorage
4
11
  # This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. MiniMagick requires
5
12
  # the {ImageMagick}[http://www.imagemagick.org] system library.
6
13
  class Analyzer::ImageAnalyzer::ImageMagick < Analyzer::ImageAnalyzer
7
- def self.accept?(blob)
8
- super && ActiveStorage.variant_processor == :mini_magick
9
- end
10
-
11
14
  private
12
15
  def read_image
13
- begin
14
- require "mini_magick"
15
- rescue LoadError
16
- logger.info "Skipping image analysis because the mini_magick gem isn't installed"
17
- return {}
18
- end
19
-
20
16
  download_blob_to_tempfile do |file|
21
17
  image = instrument("mini_magick") do
22
18
  MiniMagick::Image.new(file.path)
@@ -1,22 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ begin
4
+ gem "ruby-vips"
5
+ require "ruby-vips"
6
+ rescue LoadError => error
7
+ raise error unless error.message.include?("ruby-vips")
8
+ end
9
+
3
10
  module ActiveStorage
4
11
  # This analyzer relies on the third-party {ruby-vips}[https://github.com/libvips/ruby-vips] gem. Ruby-vips requires
5
12
  # the {libvips}[https://libvips.github.io/libvips/] system library.
6
13
  class Analyzer::ImageAnalyzer::Vips < Analyzer::ImageAnalyzer
7
- def self.accept?(blob)
8
- super && ActiveStorage.variant_processor == :vips
9
- end
10
-
11
14
  private
12
15
  def read_image
13
- begin
14
- require "ruby-vips"
15
- rescue LoadError
16
- logger.info "Skipping image analysis because the ruby-vips gem isn't installed"
17
- return {}
18
- end
19
-
20
16
  download_blob_to_tempfile do |file|
21
17
  image = instrument("vips") do
22
18
  # ruby-vips will raise Vips::Error if it can't find an appropriate loader for the file
@@ -35,6 +31,9 @@ module ActiveStorage
35
31
  logger.error "Skipping image analysis due to a Vips error: #{error.message}"
36
32
  {}
37
33
  end
34
+ rescue ::Vips::Error => error
35
+ logger.error "Skipping image analysis due to an Vips error: #{error.message}"
36
+ {}
38
37
  end
39
38
 
40
39
  ROTATIONS = /Right-top|Left-bottom|Top-right|Bottom-left/
@@ -12,6 +12,11 @@ module ActiveStorage
12
12
  # ActiveStorage::Analyzer::ImageAnalyzer::ImageMagick.new(blob).metadata
13
13
  # # => { width: 4104, height: 2736 }
14
14
  class Analyzer::ImageAnalyzer < Analyzer
15
+ extend ActiveSupport::Autoload
16
+
17
+ autoload :Vips
18
+ autoload :ImageMagick
19
+
15
20
  def self.accept?(blob)
16
21
  blob.image?
17
22
  end