activestorage 8.0.3 → 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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -59
  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/javascript/activestorage/direct_upload_controller.js +48 -1
  7. data/app/models/active_storage/blob/representable.rb +2 -2
  8. data/app/models/active_storage/blob.rb +1 -1
  9. data/app/models/active_storage/filename.rb +1 -0
  10. data/app/models/active_storage/variant.rb +11 -11
  11. data/app/models/active_storage/variation.rb +1 -1
  12. data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +7 -11
  13. data/lib/active_storage/analyzer/image_analyzer/vips.rb +10 -11
  14. data/lib/active_storage/analyzer/image_analyzer.rb +5 -0
  15. data/lib/active_storage/attached.rb +0 -1
  16. data/lib/active_storage/downloader.rb +1 -1
  17. data/lib/active_storage/engine.rb +44 -9
  18. data/lib/active_storage/gem_version.rb +3 -3
  19. data/lib/active_storage/service/configurator.rb +6 -0
  20. data/lib/active_storage/service/disk_service.rb +1 -1
  21. data/lib/active_storage/service/gcs_service.rb +10 -2
  22. data/lib/active_storage/service/mirror_service.rb +1 -1
  23. data/lib/active_storage/service/registry.rb +6 -0
  24. data/lib/active_storage/service.rb +4 -1
  25. data/lib/active_storage/transformers/image_magick.rb +72 -0
  26. data/lib/active_storage/transformers/image_processing_transformer.rb +5 -67
  27. data/lib/active_storage/transformers/vips.rb +11 -0
  28. data/lib/active_storage.rb +13 -0
  29. metadata +14 -13
  30. 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: 022126fe7ad33aff5dd19e9cb369c9a4264b20b87e33af853f07cf8946bc6b4a
4
- data.tar.gz: e32c5d37045bb9e94d86ae04c24b290a538fe9931eca6d8debdbc8d5d049cdd4
3
+ metadata.gz: 8959531cd3a50439e1d30882ac0398b7d361da8a6859049b01c31e1a35e58862
4
+ data.tar.gz: c4027f992ab9f941cecbe88a7a415d52f5a3800b86e69fe8789567f320a9b9ea
5
5
  SHA512:
6
- metadata.gz: 16d1b5343105d7b22c8b3e85b2d3200ab1fa9d4b88e0313a2154f26b0a8dcfcf3039ad86acaad6a631bccb6e91bf560f570c5e0ab9f962881edf7d52d75da5ba
7
- data.tar.gz: f6c5e9f142ee4ddce2be8cb7ae89009d60249ab454160ed3ab4be9fdc55336d2021f9ba411957bdb1aa128d0c7de74d5b4a45e9b5cd4c26500abc75fe0903945
6
+ metadata.gz: 2750ae72f60c4e388b74813b74fa2580a5ebe48887eca53905b1d1173a08bac4ff931f22fadc3fb2ffec0d96417f7661fe22aba44cdaa36e7480c7df4b0f52d0
7
+ data.tar.gz: 3201606c2dce4e6e41649c8aca6432dbd2c0635c966366809bc6552ad07cc074596d3d973054bce87f79f3eca9ffb68d91e116d9b8f81ac5dad7d59b8040cd5b
data/CHANGELOG.md CHANGED
@@ -1,23 +1,51 @@
1
- ## Rails 8.0.3 (September 22, 2025) ##
1
+ ## Rails 8.1.0.beta1 (September 04, 2025) ##
2
2
 
3
- * Address deprecation of `Aws::S3::Object#upload_stream` in `ActiveStorage::Service::S3Service`.
3
+ * Remove deprecated `:azure` storage service.
4
4
 
5
- *Joshua Young*
5
+ *Rafael Mendonça França*
6
6
 
7
- * Fix `config.active_storage.touch_attachment_records` to work with eager loading.
7
+ * Remove unnecessary calls to the GCP metadata server.
8
8
 
9
- *fatkodima*
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:
10
14
 
15
+ ```ruby
16
+ Google::Apis::RequestOptions.default.authorization = Google::Auth.get_application_default(...)
17
+ ```
11
18
 
12
- ## Rails 8.0.2.1 (August 13, 2025) ##
19
+ In the cases applications do not set that, the GCP libraries automatically determine credentials.
13
20
 
14
- * Remove dangerous transformations
21
+ This also enables using credentials other than those of the associated GCP
22
+ service account like when using impersonation.
15
23
 
16
- [CVE-2025-24293]
24
+ *Alex Coomans*
17
25
 
18
- *Zack Deveau*
26
+ * Direct upload progress accounts for server processing time.
19
27
 
20
- ## Rails 8.0.2 (March 12, 2025) ##
28
+ *Jeremy Daer*
29
+
30
+ * Delegate `ActiveStorage::Filename#to_str` to `#to_s`
31
+
32
+ Supports checking String equality:
33
+
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
+ ```
40
+
41
+ *Sean Doyle*
42
+
43
+ * Add support for alternative MD5 implementation through `config.active_storage.checksum_implementation`.
44
+
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.
47
+
48
+ *Matt Pasquini*, *Jean Boussier*
21
49
 
22
50
  * A Blob will no longer autosave associated Attachment.
23
51
 
@@ -31,52 +59,4 @@
31
59
 
32
60
  *Edouard-chin*
33
61
 
34
-
35
- ## Rails 8.0.1 (December 13, 2024) ##
36
-
37
- * No changes.
38
-
39
-
40
- ## Rails 8.0.0.1 (December 10, 2024) ##
41
-
42
- * No changes.
43
-
44
-
45
- ## Rails 8.0.0 (November 07, 2024) ##
46
-
47
- * No changes.
48
-
49
-
50
- ## Rails 8.0.0.rc2 (October 30, 2024) ##
51
-
52
- * No changes.
53
-
54
-
55
- ## Rails 8.0.0.rc1 (October 19, 2024) ##
56
-
57
- * No changes.
58
-
59
-
60
- ## Rails 8.0.0.beta1 (September 26, 2024) ##
61
-
62
- * Deprecate `ActiveStorage::Service::AzureStorageService`.
63
-
64
- *zzak*
65
-
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.
68
-
69
- *Luong Viet Dung(Martin)*
70
-
71
- * Improve InvariableError, UnpreviewableError and UnrepresentableError message.
72
-
73
- Include Blob ID and content_type in the messages.
74
-
75
- *Petrik de Heus*
76
-
77
- * Mark proxied files as `immutable` in their Cache-Control header
78
-
79
- *Nate Matykiewicz*
80
-
81
-
82
- 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])";
@@ -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
@@ -332,7 +332,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
332
332
  def compute_checksum_in_chunks(io)
333
333
  raise ArgumentError, "io must be rewindable" unless io.respond_to?(:rewind)
334
334
 
335
- OpenSSL::Digest::MD5.new.tap do |checksum|
335
+ ActiveStorage.checksum_implementation.new.tap do |checksum|
336
336
  read_buffer = "".b
337
337
  while io.read(5.megabytes, read_buffer)
338
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
@@ -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
@@ -12,8 +12,6 @@ require "active_storage/previewer/mupdf_previewer"
12
12
  require "active_storage/previewer/video_previewer"
13
13
 
14
14
  require "active_storage/analyzer/image_analyzer"
15
- require "active_storage/analyzer/image_analyzer/image_magick"
16
- require "active_storage/analyzer/image_analyzer/vips"
17
15
  require "active_storage/analyzer/video_analyzer"
18
16
  require "active_storage/analyzer/audio_analyzer"
19
17
 
@@ -27,7 +25,7 @@ module ActiveStorage
27
25
 
28
26
  config.active_storage = ActiveSupport::OrderedOptions.new
29
27
  config.active_storage.previewers = [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
30
- config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer::Vips, ActiveStorage::Analyzer::ImageAnalyzer::ImageMagick, ActiveStorage::Analyzer::VideoAnalyzer, ActiveStorage::Analyzer::AudioAnalyzer ]
28
+ config.active_storage.analyzers = [ ActiveStorage::Analyzer::VideoAnalyzer, ActiveStorage::Analyzer::AudioAnalyzer ]
31
29
  config.active_storage.paths = ActiveSupport::OrderedOptions.new
32
30
  config.active_storage.queues = ActiveSupport::InheritableOptions.new
33
31
  config.active_storage.precompile_assets = true
@@ -90,9 +88,43 @@ module ActiveStorage
90
88
 
91
89
  config.after_initialize do |app|
92
90
  ActiveStorage.logger = app.config.active_storage.logger || Rails.logger
93
- ActiveStorage.variant_processor = app.config.active_storage.variant_processor || :mini_magick
91
+ ActiveStorage.variant_processor = app.config.active_storage.variant_processor
94
92
  ActiveStorage.previewers = app.config.active_storage.previewers || []
95
- ActiveStorage.analyzers = app.config.active_storage.analyzers || []
93
+
94
+ begin
95
+ analyzer, transformer =
96
+ case ActiveStorage.variant_processor
97
+ when :vips
98
+ [
99
+ ActiveStorage::Analyzer::ImageAnalyzer::Vips,
100
+ ActiveStorage::Transformers::Vips
101
+ ]
102
+ when :mini_magick
103
+ [
104
+ ActiveStorage::Analyzer::ImageAnalyzer::ImageMagick,
105
+ ActiveStorage::Transformers::ImageMagick
106
+ ]
107
+ end
108
+
109
+ ActiveStorage.analyzers = [analyzer].compact.concat(app.config.active_storage.analyzers || [])
110
+ ActiveStorage.variant_transformer = transformer
111
+ rescue LoadError => error
112
+ case error.message
113
+ when /libvips/
114
+ ActiveStorage.logger.warn <<~WARNING.squish
115
+ Using vips to process variants requires the libvips library.
116
+ Please install libvips using the instructions on the libvips website.
117
+ WARNING
118
+ when /image_processing/
119
+ ActiveStorage.logger.warn <<~WARNING.squish
120
+ Generating image variants require the image_processing gem.
121
+ Please add `gem 'image_processing', '~> 1.2'` to your Gemfile.
122
+ WARNING
123
+ else
124
+ raise
125
+ end
126
+ end
127
+
96
128
  ActiveStorage.paths = app.config.active_storage.paths || {}
97
129
  ActiveStorage.routes_prefix = app.config.active_storage.routes_prefix || "/rails/active_storage"
98
130
  ActiveStorage.draw_routes = app.config.active_storage.draw_routes != false
@@ -122,6 +154,9 @@ module ActiveStorage
122
154
  ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream"
123
155
  ActiveStorage.video_preview_arguments = app.config.active_storage.video_preview_arguments || "-y -vframes 1 -f image2"
124
156
  ActiveStorage.track_variants = app.config.active_storage.track_variants || false
157
+ if app.config.active_storage.checksum_implementation
158
+ ActiveStorage.checksum_implementation = app.config.active_storage.checksum_implementation
159
+ end
125
160
  end
126
161
  end
127
162
 
@@ -139,9 +174,9 @@ module ActiveStorage
139
174
  end
140
175
  end
141
176
 
142
- initializer "active_storage.services" do
177
+ initializer "active_storage.services" do |app|
143
178
  ActiveSupport.on_load(:active_storage_blob) do
144
- configs = Rails.configuration.active_storage.service_configurations ||=
179
+ configs = app.config.active_storage.service_configurations ||=
145
180
  begin
146
181
  config_file = Rails.root.join("config/storage/#{Rails.env}.yml")
147
182
  config_file = Rails.root.join("config/storage.yml") unless config_file.exist?
@@ -152,7 +187,7 @@ module ActiveStorage
152
187
 
153
188
  ActiveStorage::Blob.services = ActiveStorage::Service::Registry.new(configs)
154
189
 
155
- if config_choice = Rails.configuration.active_storage.service
190
+ if config_choice = app.config.active_storage.service
156
191
  ActiveStorage::Blob.service = ActiveStorage::Blob.services.fetch(config_choice)
157
192
  end
158
193
  end
@@ -174,7 +209,7 @@ module ActiveStorage
174
209
  initializer "action_view.configuration" do
175
210
  config.after_initialize do |app|
176
211
  ActiveSupport.on_load(:action_view) do
177
- multiple_file_field_include_hidden = app.config.active_storage.delete(:multiple_file_field_include_hidden)
212
+ multiple_file_field_include_hidden = app.config.active_storage.multiple_file_field_include_hidden
178
213
 
179
214
  unless multiple_file_field_include_hidden.nil?
180
215
  ActionView::Helpers::FormHelper.multiple_file_field_include_hidden = multiple_file_field_include_hidden
@@ -8,9 +8,9 @@ module ActiveStorage
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 8
11
- MINOR = 0
12
- TINY = 3
13
- PRE = nil
11
+ MINOR = 1
12
+ TINY = 0
13
+ PRE = "beta1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -19,6 +19,12 @@ module ActiveStorage
19
19
  )
20
20
  end
21
21
 
22
+ def inspect # :nodoc:
23
+ attrs = configurations.any? ?
24
+ " configurations=[#{configurations.keys.map(&:inspect).join(", ")}]" : ""
25
+ "#<#{self.class}#{attrs}>"
26
+ end
27
+
22
28
  private
23
29
  def config_for(name)
24
30
  configurations.fetch name do
@@ -161,7 +161,7 @@ module ActiveStorage
161
161
  end
162
162
 
163
163
  def ensure_integrity_of(key, checksum)
164
- unless OpenSSL::Digest::MD5.file(path_for(key)).base64digest == checksum
164
+ unless ActiveStorage.checksum_implementation.file(path_for(key)).base64digest == checksum
165
165
  delete key
166
166
  raise ActiveStorage::IntegrityError
167
167
  end
@@ -213,8 +213,16 @@ module ActiveStorage
213
213
  lambda do |string_to_sign|
214
214
  iam_client = Google::Apis::IamcredentialsV1::IAMCredentialsService.new
215
215
 
216
- scopes = ["https://www.googleapis.com/auth/iam"]
217
- iam_client.authorization = Google::Auth.get_application_default(scopes)
216
+ # We explicitly do not set iam_client.authorization so that it uses the
217
+ # credentials set by the application at Google::Apis::RequestOptions.default.authorization.
218
+ # If the application does not set it, the GCP libraries will automatically
219
+ # determine it on each call. This code previously explicitly set the
220
+ # authorization to Google::Auth.get_application_default which triggers
221
+ # an explicit call to the metadata server - given this lambda is called
222
+ # for a significant number of file operations, it can lead to considerable
223
+ # tail latencies and even metadata server overloads. Additionally, that
224
+ # prevented applications from being able to configure the credentials
225
+ # used to perform the signature operation.
218
226
 
219
227
  request = Google::Apis::IamcredentialsV1::SignBlobRequest.new(
220
228
  payload: string_to_sign
@@ -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 Mirror \Service
@@ -31,6 +30,7 @@ module ActiveStorage
31
30
  def initialize(primary:, mirrors:)
32
31
  @primary, @mirrors = primary, mirrors
33
32
  @executor = Concurrent::ThreadPoolExecutor.new(
33
+ name: "ActiveStorage-mirror-service",
34
34
  min_threads: 1,
35
35
  max_threads: mirrors.size,
36
36
  max_queue: 0,
@@ -22,6 +22,12 @@ module ActiveStorage
22
22
  end
23
23
  end
24
24
 
25
+ def inspect # :nodoc:
26
+ attrs = configurations.any? ?
27
+ " configurations=[#{configurations.keys.map(&:inspect).join(", ")}]" : ""
28
+ "#<#{self.class}#{attrs}>"
29
+ end
30
+
25
31
  private
26
32
  attr_reader :configurations, :services
27
33
 
@@ -15,7 +15,6 @@ module ActiveStorage
15
15
  # * +Disk+, to manage attachments saved directly on the hard drive.
16
16
  # * +GCS+, to manage attachments through Google Cloud Storage.
17
17
  # * +S3+, to manage attachments through Amazon S3.
18
- # * +AzureStorage+, to manage attachments through Microsoft Azure Storage.
19
18
  # * +Mirror+, to be able to use several services to manage attachments.
20
19
  #
21
20
  # Inside a \Rails application, you can set-up your services through the
@@ -148,6 +147,10 @@ module ActiveStorage
148
147
  @public
149
148
  end
150
149
 
150
+ def inspect # :nodoc:
151
+ "#<#{self.class}#{name.present? ? " name=#{name.inspect}" : ""}>"
152
+ end
153
+
151
154
  private
152
155
  def private_url(key, expires_in:, filename:, disposition:, content_type:, **)
153
156
  raise NotImplementedError
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ module Transformers
5
+ class ImageMagick < ImageProcessingTransformer
6
+ private
7
+ def processor
8
+ ImageProcessing::MiniMagick
9
+ end
10
+
11
+ def validate_transformation(name, argument)
12
+ method_name = name.to_s.tr("-", "_")
13
+
14
+ unless ActiveStorage.supported_image_processing_methods.include?(method_name)
15
+ raise UnsupportedImageProcessingMethod, <<~ERROR.squish
16
+ The provided transformation method is not supported: #{method_name}.
17
+ ERROR
18
+ end
19
+
20
+ if argument.present?
21
+ if argument.is_a?(String) || argument.is_a?(Symbol)
22
+ validate_arg_string(argument)
23
+ elsif argument.is_a?(Array)
24
+ validate_arg_array(argument)
25
+ elsif argument.is_a?(Hash)
26
+ validate_arg_hash(argument)
27
+ end
28
+ end
29
+
30
+ super
31
+ end
32
+
33
+ def validate_arg_string(argument)
34
+ unsupported_arguments = ActiveStorage.unsupported_image_processing_arguments.any? do |bad_arg|
35
+ argument.to_s.downcase.include?(bad_arg)
36
+ end
37
+
38
+ raise UnsupportedImageProcessingArgument if unsupported_arguments
39
+ end
40
+
41
+ def validate_arg_array(argument)
42
+ argument.each do |arg|
43
+ if arg.is_a?(Integer) || arg.is_a?(Float)
44
+ next
45
+ elsif arg.is_a?(String) || arg.is_a?(Symbol)
46
+ validate_arg_string(arg)
47
+ elsif arg.is_a?(Array)
48
+ validate_arg_array(arg)
49
+ elsif arg.is_a?(Hash)
50
+ validate_arg_hash(arg)
51
+ end
52
+ end
53
+ end
54
+
55
+ def validate_arg_hash(argument)
56
+ argument.each do |key, value|
57
+ validate_arg_string(key)
58
+
59
+ if value.is_a?(Integer) || value.is_a?(Float)
60
+ next
61
+ elsif value.is_a?(String) || value.is_a?(Symbol)
62
+ validate_arg_string(value)
63
+ elsif value.is_a?(Array)
64
+ validate_arg_array(value)
65
+ elsif value.is_a?(Hash)
66
+ validate_arg_hash(value)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -25,22 +25,9 @@ module ActiveStorage
25
25
  call
26
26
  end
27
27
 
28
- def processor
29
- ImageProcessing.const_get(ActiveStorage.variant_processor.to_s.camelize)
30
- end
31
-
32
28
  def operations
33
29
  transformations.each_with_object([]) do |(name, argument), list|
34
- if ActiveStorage.variant_processor == :mini_magick
35
- validate_transformation(name, argument)
36
- end
37
-
38
- if name.to_s == "combine_options"
39
- raise ArgumentError, <<~ERROR.squish
40
- Active Storage's ImageProcessing transformer doesn't support :combine_options,
41
- as it always generates a single command.
42
- ERROR
43
- end
30
+ validate_transformation(name, argument)
44
31
 
45
32
  if argument.present?
46
33
  list << [ name, argument ]
@@ -49,61 +36,12 @@ module ActiveStorage
49
36
  end
50
37
 
51
38
  def validate_transformation(name, argument)
52
- method_name = name.to_s.tr("-", "_")
53
-
54
- unless ActiveStorage.supported_image_processing_methods.any? { |method| method_name == method }
55
- raise UnsupportedImageProcessingMethod, <<~ERROR.squish
56
- One or more of the provided transformation methods is not supported.
39
+ if name.to_s == "combine_options"
40
+ raise ArgumentError, <<~ERROR.squish
41
+ Active Storage's ImageProcessing transformer doesn't support :combine_options,
42
+ as it always generates a single command.
57
43
  ERROR
58
44
  end
59
-
60
- if argument.present?
61
- if argument.is_a?(String) || argument.is_a?(Symbol)
62
- validate_arg_string(argument)
63
- elsif argument.is_a?(Array)
64
- validate_arg_array(argument)
65
- elsif argument.is_a?(Hash)
66
- validate_arg_hash(argument)
67
- end
68
- end
69
- end
70
-
71
- def validate_arg_string(argument)
72
- unsupported_arguments = ActiveStorage.unsupported_image_processing_arguments.any? do |bad_arg|
73
- argument.to_s.downcase.include?(bad_arg)
74
- end
75
-
76
- raise UnsupportedImageProcessingArgument if unsupported_arguments
77
- end
78
-
79
- def validate_arg_array(argument)
80
- argument.each do |arg|
81
- if arg.is_a?(Integer) || arg.is_a?(Float)
82
- next
83
- elsif arg.is_a?(String) || arg.is_a?(Symbol)
84
- validate_arg_string(arg)
85
- elsif arg.is_a?(Array)
86
- validate_arg_array(arg)
87
- elsif arg.is_a?(Hash)
88
- validate_arg_hash(arg)
89
- end
90
- end
91
- end
92
-
93
- def validate_arg_hash(argument)
94
- argument.each do |key, value|
95
- validate_arg_string(key)
96
-
97
- if value.is_a?(Integer) || value.is_a?(Float)
98
- next
99
- elsif value.is_a?(String) || value.is_a?(Symbol)
100
- validate_arg_string(value)
101
- elsif value.is_a?(Array)
102
- validate_arg_array(value)
103
- elsif value.is_a?(Hash)
104
- validate_arg_hash(value)
105
- end
106
- end
107
45
  end
108
46
  end
109
47
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ module Transformers
5
+ class Vips < ImageProcessingTransformer
6
+ def processor
7
+ ImageProcessing::Vips
8
+ end
9
+ end
10
+ end
11
+ end
@@ -49,6 +49,8 @@ module ActiveStorage
49
49
  mattr_accessor :verifier
50
50
  mattr_accessor :variant_processor, default: :mini_magick
51
51
 
52
+ mattr_accessor :variant_transformer
53
+
52
54
  mattr_accessor :queues, default: {}
53
55
 
54
56
  mattr_accessor :previewers, default: []
@@ -360,6 +362,15 @@ module ActiveStorage
360
362
 
361
363
  mattr_accessor :track_variants, default: false
362
364
 
365
+ singleton_class.attr_accessor :checksum_implementation
366
+ @checksum_implementation = OpenSSL::Digest::MD5
367
+ begin
368
+ @checksum_implementation.hexdigest("test")
369
+ rescue # OpenSSL may have MD5 disabled
370
+ require "digest/md5"
371
+ @checksum_implementation = Digest::MD5
372
+ end
373
+
363
374
  mattr_accessor :video_preview_arguments, default: "-y -vframes 1 -f image2"
364
375
 
365
376
  module Transformers
@@ -367,5 +378,7 @@ module ActiveStorage
367
378
 
368
379
  autoload :Transformer
369
380
  autoload :ImageProcessingTransformer
381
+ autoload :Vips
382
+ autoload :ImageMagick
370
383
  end
371
384
  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: 8.0.3
4
+ version: 8.1.0.beta1
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: 8.0.3
18
+ version: 8.1.0.beta1
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: 8.0.3
25
+ version: 8.1.0.beta1
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: 8.0.3
32
+ version: 8.1.0.beta1
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: 8.0.3
39
+ version: 8.1.0.beta1
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: 8.0.3
46
+ version: 8.1.0.beta1
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: 8.0.3
53
+ version: 8.1.0.beta1
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: 8.0.3
60
+ version: 8.1.0.beta1
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: 8.0.3
67
+ version: 8.1.0.beta1
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: marcel
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -173,15 +173,16 @@ files:
173
173
  - lib/active_storage/previewer/video_previewer.rb
174
174
  - lib/active_storage/reflection.rb
175
175
  - lib/active_storage/service.rb
176
- - lib/active_storage/service/azure_storage_service.rb
177
176
  - lib/active_storage/service/configurator.rb
178
177
  - lib/active_storage/service/disk_service.rb
179
178
  - lib/active_storage/service/gcs_service.rb
180
179
  - lib/active_storage/service/mirror_service.rb
181
180
  - lib/active_storage/service/registry.rb
182
181
  - lib/active_storage/service/s3_service.rb
182
+ - lib/active_storage/transformers/image_magick.rb
183
183
  - lib/active_storage/transformers/image_processing_transformer.rb
184
184
  - lib/active_storage/transformers/transformer.rb
185
+ - lib/active_storage/transformers/vips.rb
185
186
  - lib/active_storage/version.rb
186
187
  - lib/tasks/activestorage.rake
187
188
  homepage: https://rubyonrails.org
@@ -189,10 +190,10 @@ licenses:
189
190
  - MIT
190
191
  metadata:
191
192
  bug_tracker_uri: https://github.com/rails/rails/issues
192
- changelog_uri: https://github.com/rails/rails/blob/v8.0.3/activestorage/CHANGELOG.md
193
- documentation_uri: https://api.rubyonrails.org/v8.0.3/
193
+ changelog_uri: https://github.com/rails/rails/blob/v8.1.0.beta1/activestorage/CHANGELOG.md
194
+ documentation_uri: https://api.rubyonrails.org/v8.1.0.beta1/
194
195
  mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
195
- source_code_uri: https://github.com/rails/rails/tree/v8.0.3/activestorage
196
+ source_code_uri: https://github.com/rails/rails/tree/v8.1.0.beta1/activestorage
196
197
  rubygems_mfa_required: 'true'
197
198
  rdoc_options: []
198
199
  require_paths:
@@ -1,201 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- gem "azure-storage-blob", ">= 2.0"
4
-
5
- require "active_support/core_ext/numeric/bytes"
6
- require "azure/storage/blob"
7
- require "azure/storage/common/core/auth/shared_access_signature"
8
-
9
- module ActiveStorage
10
- # = Active Storage \Azure Storage \Service
11
- #
12
- # Wraps the Microsoft Azure Storage Blob Service as an Active Storage service.
13
- # See ActiveStorage::Service for the generic API documentation that applies to all services.
14
- class Service::AzureStorageService < Service
15
- attr_reader :client, :container, :signer
16
-
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
-
25
- @client = Azure::Storage::Blob::BlobService.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key, **options)
26
- @signer = Azure::Storage::Common::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key)
27
- @container = container
28
- @public = public
29
- end
30
-
31
- def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, custom_metadata: {}, **)
32
- instrument :upload, key: key, checksum: checksum do
33
- handle_errors do
34
- content_disposition = content_disposition_with(filename: filename, type: disposition) if disposition && filename
35
-
36
- client.create_block_blob(container, key, IO.try_convert(io) || io, content_md5: checksum, content_type: content_type, content_disposition: content_disposition, metadata: custom_metadata)
37
- end
38
- end
39
- end
40
-
41
- def download(key, &block)
42
- if block_given?
43
- instrument :streaming_download, key: key do
44
- stream(key, &block)
45
- end
46
- else
47
- instrument :download, key: key do
48
- handle_errors do
49
- _, io = client.get_blob(container, key)
50
- io.force_encoding(Encoding::BINARY)
51
- end
52
- end
53
- end
54
- end
55
-
56
- def download_chunk(key, range)
57
- instrument :download_chunk, key: key, range: range do
58
- handle_errors do
59
- _, io = client.get_blob(container, key, start_range: range.begin, end_range: range.exclude_end? ? range.end - 1 : range.end)
60
- io.force_encoding(Encoding::BINARY)
61
- end
62
- end
63
- end
64
-
65
- def delete(key)
66
- instrument :delete, key: key do
67
- client.delete_blob(container, key)
68
- rescue Azure::Core::Http::HTTPError => e
69
- raise unless e.type == "BlobNotFound"
70
- # Ignore files already deleted
71
- end
72
- end
73
-
74
- def delete_prefixed(prefix)
75
- instrument :delete_prefixed, prefix: prefix do
76
- marker = nil
77
-
78
- loop do
79
- results = client.list_blobs(container, prefix: prefix, marker: marker)
80
-
81
- results.each do |blob|
82
- client.delete_blob(container, blob.name)
83
- end
84
-
85
- break unless marker = results.continuation_token.presence
86
- end
87
- end
88
- end
89
-
90
- def exist?(key)
91
- instrument :exist, key: key do |payload|
92
- answer = blob_for(key).present?
93
- payload[:exist] = answer
94
- answer
95
- end
96
- end
97
-
98
- def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, custom_metadata: {})
99
- instrument :url, key: key do |payload|
100
- generated_url = signer.signed_uri(
101
- uri_for(key), false,
102
- service: "b",
103
- permissions: "rw",
104
- expiry: format_expiry(expires_in)
105
- ).to_s
106
-
107
- payload[:url] = generated_url
108
-
109
- generated_url
110
- end
111
- end
112
-
113
- def headers_for_direct_upload(key, content_type:, checksum:, filename: nil, disposition: nil, custom_metadata: {}, **)
114
- content_disposition = content_disposition_with(type: disposition, filename: filename) if filename
115
-
116
- { "Content-Type" => content_type, "Content-MD5" => checksum, "x-ms-blob-content-disposition" => content_disposition, "x-ms-blob-type" => "BlockBlob", **custom_metadata_headers(custom_metadata) }
117
- end
118
-
119
- def compose(source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
120
- content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
121
-
122
- client.create_append_blob(
123
- container,
124
- destination_key,
125
- content_type: content_type,
126
- content_disposition: content_disposition,
127
- metadata: custom_metadata,
128
- ).tap do |blob|
129
- source_keys.each do |source_key|
130
- stream(source_key) do |chunk|
131
- client.append_blob_block(container, blob.name, chunk)
132
- end
133
- end
134
- end
135
- end
136
-
137
- private
138
- def private_url(key, expires_in:, filename:, disposition:, content_type:, **)
139
- signer.signed_uri(
140
- uri_for(key), false,
141
- service: "b",
142
- permissions: "r",
143
- expiry: format_expiry(expires_in),
144
- content_disposition: content_disposition_with(type: disposition, filename: filename),
145
- content_type: content_type
146
- ).to_s
147
- end
148
-
149
- def public_url(key, **)
150
- uri_for(key).to_s
151
- end
152
-
153
-
154
- def uri_for(key)
155
- client.generate_uri("#{container}/#{key}")
156
- end
157
-
158
- def blob_for(key)
159
- client.get_blob_properties(container, key)
160
- rescue Azure::Core::Http::HTTPError
161
- false
162
- end
163
-
164
- def format_expiry(expires_in)
165
- expires_in ? Time.now.utc.advance(seconds: expires_in).iso8601 : nil
166
- end
167
-
168
- # Reads the object for the given key in chunks, yielding each to the block.
169
- def stream(key)
170
- blob = blob_for(key)
171
-
172
- chunk_size = 5.megabytes
173
- offset = 0
174
-
175
- raise ActiveStorage::FileNotFoundError unless blob.present?
176
-
177
- while offset < blob.properties[:content_length]
178
- _, chunk = client.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1)
179
- yield chunk.force_encoding(Encoding::BINARY)
180
- offset += chunk_size
181
- end
182
- end
183
-
184
- def handle_errors
185
- yield
186
- rescue Azure::Core::Http::HTTPError => e
187
- case e.type
188
- when "BlobNotFound"
189
- raise ActiveStorage::FileNotFoundError
190
- when "Md5Mismatch"
191
- raise ActiveStorage::IntegrityError
192
- else
193
- raise
194
- end
195
- end
196
-
197
- def custom_metadata_headers(metadata)
198
- metadata.transform_keys { |key| "x-ms-meta-#{key}" }
199
- end
200
- end
201
- end