activestorage 8.0.0 → 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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -18
  3. data/README.md +6 -3
  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 +2 -2
  7. data/app/javascript/activestorage/direct_upload_controller.js +48 -1
  8. data/app/models/active_storage/blob/representable.rb +66 -0
  9. data/app/models/active_storage/blob.rb +2 -18
  10. data/app/models/active_storage/filename.rb +1 -0
  11. data/app/models/active_storage/variant.rb +7 -7
  12. data/app/models/active_storage/variation.rb +1 -1
  13. data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +7 -11
  14. data/lib/active_storage/analyzer/image_analyzer/vips.rb +11 -12
  15. data/lib/active_storage/analyzer/image_analyzer.rb +5 -0
  16. data/lib/active_storage/attached/changes/create_one.rb +1 -1
  17. data/lib/active_storage/attached/model.rb +25 -6
  18. data/lib/active_storage/attached.rb +0 -1
  19. data/lib/active_storage/downloader.rb +1 -1
  20. data/lib/active_storage/engine.rb +48 -10
  21. data/lib/active_storage/fixture_set.rb +1 -1
  22. data/lib/active_storage/gem_version.rb +2 -2
  23. data/lib/active_storage/service/configurator.rb +6 -0
  24. data/lib/active_storage/service/disk_service.rb +1 -1
  25. data/lib/active_storage/service/gcs_service.rb +10 -2
  26. data/lib/active_storage/service/mirror_service.rb +1 -1
  27. data/lib/active_storage/service/registry.rb +6 -0
  28. data/lib/active_storage/service/s3_service.rb +19 -4
  29. data/lib/active_storage/service.rb +4 -1
  30. data/lib/active_storage/transformers/image_magick.rb +72 -0
  31. data/lib/active_storage/transformers/image_processing_transformer.rb +5 -67
  32. data/lib/active_storage/transformers/vips.rb +11 -0
  33. data/lib/active_storage.rb +13 -3
  34. metadata +16 -18
  35. 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: b8928950e01f17e627b8e131697473f5b112c352e9e5ea6319b7ca05982aad6c
4
- data.tar.gz: '098bda3f6c33584da65c77915d970e081c4d96d15b8b69025297b82dbd05bd18'
3
+ metadata.gz: 8959531cd3a50439e1d30882ac0398b7d361da8a6859049b01c31e1a35e58862
4
+ data.tar.gz: c4027f992ab9f941cecbe88a7a415d52f5a3800b86e69fe8789567f320a9b9ea
5
5
  SHA512:
6
- metadata.gz: b49759ed05fb3755feae6f4235b4c8907a70cf32bd758ab94078c8bd5ad14e77ecb74d585d9eb5eb43d8e0aadf97b471fd1fa0a7b037e2bfecdfd83663b8ea39
7
- data.tar.gz: 7341c693d6a14493d4e1c1a8cf04a15582d4e1cb0dc341a07573c3dc9a89148f7770d296676fed9a26f5393eae65d4d5e79c87aba1d56ad3f4e08f19866aeb63
6
+ metadata.gz: 2750ae72f60c4e388b74813b74fa2580a5ebe48887eca53905b1d1173a08bac4ff931f22fadc3fb2ffec0d96417f7661fe22aba44cdaa36e7480c7df4b0f52d0
7
+ data.tar.gz: 3201606c2dce4e6e41649c8aca6432dbd2c0635c966366809bc6552ad07cc074596d3d973054bce87f79f3eca9ffb68d91e116d9b8f81ac5dad7d59b8040cd5b
data/CHANGELOG.md CHANGED
@@ -1,38 +1,62 @@
1
- ## Rails 8.0.0 (November 07, 2024) ##
1
+ ## Rails 8.1.0.beta1 (September 04, 2025) ##
2
2
 
3
- * No changes.
3
+ * Remove deprecated `:azure` storage service.
4
4
 
5
+ *Rafael Mendonça França*
5
6
 
6
- ## Rails 8.0.0.rc2 (October 30, 2024) ##
7
+ * Remove unnecessary calls to the GCP metadata server.
7
8
 
8
- * No changes.
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
 
15
+ ```ruby
16
+ Google::Apis::RequestOptions.default.authorization = Google::Auth.get_application_default(...)
17
+ ```
10
18
 
11
- ## Rails 8.0.0.rc1 (October 19, 2024) ##
19
+ In the cases applications do not set that, the GCP libraries automatically determine credentials.
12
20
 
13
- * No changes.
21
+ This also enables using credentials other than those of the associated GCP
22
+ service account like when using impersonation.
14
23
 
24
+ *Alex Coomans*
15
25
 
16
- ## Rails 8.0.0.beta1 (September 26, 2024) ##
26
+ * Direct upload progress accounts for server processing time.
17
27
 
18
- * Deprecate `ActiveStorage::Service::AzureStorageService`.
28
+ *Jeremy Daer*
19
29
 
20
- *zzak*
30
+ * Delegate `ActiveStorage::Filename#to_str` to `#to_s`
21
31
 
22
- * Improve `ActiveStorage::Filename#sanitized` method to handle special characters more effectively.
23
- Replace the characters `"*?<>` with `-` if they exist in the Filename to match the Filename convention of Win OS.
32
+ Supports checking String equality:
24
33
 
25
- *Luong Viet Dung(Martin)*
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
+ ```
26
40
 
27
- * Improve InvariableError, UnpreviewableError and UnrepresentableError message.
41
+ *Sean Doyle*
28
42
 
29
- Include Blob ID and content_type in the messages.
43
+ * Add support for alternative MD5 implementation through `config.active_storage.checksum_implementation`.
30
44
 
31
- *Petrik de Heus*
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.
32
47
 
33
- * Mark proxied files as `immutable` in their Cache-Control header
48
+ *Matt Pasquini*, *Jean Boussier*
34
49
 
35
- *Nate Matykiewicz*
50
+ * A Blob will no longer autosave associated Attachment.
36
51
 
52
+ This fixes an issue where a record with an attachment would have
53
+ its dirty attributes reset, preventing your `after commit` callbacks
54
+ on that record to behave as expected.
37
55
 
38
- Please check [7-2-stable](https://github.com/rails/rails/blob/7-2-stable/activestorage/CHANGELOG.md) for previous changes.
56
+ Note that this change doesn't require any changes on your application
57
+ and is supposed to be internal. Active Storage Attachment will continue
58
+ to be autosaved (through a different relation).
59
+
60
+ *Edouard-chin*
61
+
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
 
@@ -203,6 +206,6 @@ Bug reports for the Ruby on \Rails project can be filed here:
203
206
 
204
207
  * https://github.com/rails/rails/issues
205
208
 
206
- Feature requests should be discussed on the rails-core mailing list here:
209
+ Feature requests should be discussed on the rubyonrails-core forum here:
207
210
 
208
211
  * https://discuss.rubyonrails.org/c/rubyonrails-core
@@ -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])";
@@ -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
@@ -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
  }
@@ -31,6 +31,72 @@ module ActiveStorage::Blob::Representable
31
31
  # Raises ActiveStorage::InvariableError if the variant processor cannot
32
32
  # transform the blob. To determine whether a blob is variable, call
33
33
  # ActiveStorage::Blob#variable?.
34
+ #
35
+ # ==== Options
36
+ #
37
+ # Options are defined by the {image_processing gem}[https://github.com/janko/image_processing],
38
+ # and depend on which variant processor you are using:
39
+ # {Vips}[https://github.com/janko/image_processing/blob/master/doc/vips.md] or
40
+ # {MiniMagick}[https://github.com/janko/image_processing/blob/master/doc/minimagick.md].
41
+ # However, both variant processors support the following options:
42
+ #
43
+ # [+:resize_to_limit+]
44
+ # Downsizes the image to fit within the specified dimensions while retaining
45
+ # the original aspect ratio. Will only resize the image if it's larger than
46
+ # the specified dimensions.
47
+ #
48
+ # user.avatar.variant(resize_to_limit: [100, 100])
49
+ #
50
+ # [+:resize_to_fit+]
51
+ # Resizes the image to fit within the specified dimensions while retaining
52
+ # the original aspect ratio. Will downsize the image if it's larger than the
53
+ # specified dimensions or upsize if it's smaller.
54
+ #
55
+ # user.avatar.variant(resize_to_fit: [100, 100])
56
+ #
57
+ # [+:resize_to_fill+]
58
+ # Resizes the image to fill the specified dimensions while retaining the
59
+ # original aspect ratio. If necessary, will crop the image in the larger
60
+ # dimension.
61
+ #
62
+ # user.avatar.variant(resize_to_fill: [100, 100])
63
+ #
64
+ # [+:resize_and_pad+]
65
+ # Resizes the image to fit within the specified dimensions while retaining
66
+ # the original aspect ratio. If necessary, will pad the remaining area with
67
+ # transparent color if source image has alpha channel, black otherwise.
68
+ #
69
+ # user.avatar.variant(resize_and_pad: [100, 100])
70
+ #
71
+ # [+:crop+]
72
+ # Extracts an area from an image. The first two arguments are the left and
73
+ # top edges of area to extract, while the last two arguments are the width
74
+ # and height of the area to extract.
75
+ #
76
+ # user.avatar.variant(crop: [20, 50, 300, 300])
77
+ #
78
+ # [+:rotate+]
79
+ # Rotates the image by the specified angle.
80
+ #
81
+ # user.avatar.variant(rotate: 90)
82
+ #
83
+ # Some options, including those listed above, can accept additional
84
+ # processor-specific values which can be passed as a trailing hash:
85
+ #
86
+ # <!-- Vips supports configuring `crop` for many of its transformations -->
87
+ # <%= image_tag user.avatar.variant(resize_to_fill: [100, 100, { crop: :centre }]) %>
88
+ #
89
+ # If migrating an existing application between MiniMagick and Vips, you will
90
+ # need to update processor-specific options:
91
+ #
92
+ # <!-- MiniMagick -->
93
+ # <%= image_tag user.avatar.variant(resize_to_limit: [100, 100], format: :jpeg,
94
+ # sampling_factor: "4:2:0", strip: true, interlace: "JPEG", colorspace: "sRGB", quality: 80) %>
95
+ #
96
+ # <!-- Vips -->
97
+ # <%= image_tag user.avatar.variant(resize_to_limit: [100, 100], format: :jpeg,
98
+ # saver: { subsample_mode: "on", strip: true, interlace: true, quality: 80 }) %>
99
+ #
34
100
  def variant(transformations)
35
101
  if variable?
36
102
  variant_class.new(self, ActiveStorage::Variation.wrap(transformations).default_to(default_variant_transformations))
@@ -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:
@@ -151,22 +151,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
151
151
  combined_blob.save!
152
152
  end
153
153
  end
154
-
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}"
159
- end
160
- else
161
- validate_global_service_configuration
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
170
154
  end
171
155
 
172
156
  include Analyzable
@@ -348,7 +332,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
348
332
  def compute_checksum_in_chunks(io)
349
333
  raise ArgumentError, "io must be rewindable" unless io.respond_to?(:rewind)
350
334
 
351
- OpenSSL::Digest::MD5.new.tap do |checksum|
335
+ ActiveStorage.checksum_implementation.new.tap do |checksum|
352
336
  read_buffer = "".b
353
337
  while io.read(5.megabytes, read_buffer)
354
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,17 +8,17 @@
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
@@ -74,7 +74,7 @@ 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
80
  # for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method
@@ -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
@@ -32,9 +28,12 @@ module ActiveStorage
32
28
  {}
33
29
  end
34
30
  rescue ::Vips::Error => error
35
- logger.error "Skipping image analysis due to an Vips error: #{error.message}"
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
@@ -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,8 +61,8 @@ 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
- # Under the covers, this relationship is implemented as a +has_one+ association to a
65
- # 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
66
66
  # ActiveStorage::Blob record. These associations are available as +avatar_attachment+
67
67
  # and +avatar_blob+. But you shouldn't need to work with these associations directly in
68
68
  # most circumstances.
@@ -106,7 +106,7 @@ module ActiveStorage
106
106
  # <tt>active_storage_attachments.record_type</tt> polymorphic type column of
107
107
  # the corresponding rows.
108
108
  def has_one_attached(name, dependent: :purge_later, service: nil, strict_loading: false)
109
- 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)
110
110
 
111
111
  generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
112
112
  # frozen_string_literal: true
@@ -163,8 +163,8 @@ module ActiveStorage
163
163
  # There are no columns defined on the model side, Active Storage takes
164
164
  # care of the mapping between your records and the attachments.
165
165
  #
166
- # Under the covers, this relationship is implemented as a +has_many+ association to a
167
- # 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
168
168
  # ActiveStorage::Blob record. These associations are available as +photos_attachments+
169
169
  # and +photos_blobs+. But you shouldn't need to work with these associations directly in
170
170
  # most circumstances.
@@ -208,7 +208,7 @@ module ActiveStorage
208
208
  # <tt>active_storage_attachments.record_type</tt> polymorphic type column of
209
209
  # the corresponding rows.
210
210
  def has_many_attached(name, dependent: :purge_later, service: nil, strict_loading: false)
211
- 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)
212
212
 
213
213
  generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
214
214
  # frozen_string_literal: true
@@ -259,6 +259,25 @@ module ActiveStorage
259
259
  end
260
260
  end
261
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
+
262
281
  def attachment_changes # :nodoc:
263
282
  @attachment_changes ||= {}
264
283
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/module/delegation"
4
3
 
5
4
  module ActiveStorage
6
5
  # = Active Storage \Attached
@@ -35,7 +35,7 @@ module ActiveStorage
35
35
  end
36
36
 
37
37
  def verify_integrity_of(file, checksum:)
38
- unless OpenSSL::Digest::MD5.file(file).base64digest == checksum
38
+ unless ActiveStorage.checksum_implementation.file(file).base64digest == checksum
39
39
  raise ActiveStorage::IntegrityError
40
40
  end
41
41
  end