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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -59
- data/README.md +5 -2
- data/app/assets/javascripts/activestorage.esm.js +37 -1
- data/app/assets/javascripts/activestorage.js +37 -1
- data/app/javascript/activestorage/direct_upload_controller.js +48 -1
- data/app/models/active_storage/blob/representable.rb +2 -2
- data/app/models/active_storage/blob.rb +1 -1
- data/app/models/active_storage/filename.rb +1 -0
- data/app/models/active_storage/variant.rb +11 -11
- data/app/models/active_storage/variation.rb +1 -1
- data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +7 -11
- data/lib/active_storage/analyzer/image_analyzer/vips.rb +10 -11
- data/lib/active_storage/analyzer/image_analyzer.rb +5 -0
- data/lib/active_storage/attached.rb +0 -1
- data/lib/active_storage/downloader.rb +1 -1
- data/lib/active_storage/engine.rb +44 -9
- data/lib/active_storage/gem_version.rb +3 -3
- data/lib/active_storage/service/configurator.rb +6 -0
- data/lib/active_storage/service/disk_service.rb +1 -1
- data/lib/active_storage/service/gcs_service.rb +10 -2
- data/lib/active_storage/service/mirror_service.rb +1 -1
- data/lib/active_storage/service/registry.rb +6 -0
- data/lib/active_storage/service.rb +4 -1
- data/lib/active_storage/transformers/image_magick.rb +72 -0
- data/lib/active_storage/transformers/image_processing_transformer.rb +5 -67
- data/lib/active_storage/transformers/vips.rb +11 -0
- data/lib/active_storage.rb +13 -0
- metadata +14 -13
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8959531cd3a50439e1d30882ac0398b7d361da8a6859049b01c31e1a35e58862
|
4
|
+
data.tar.gz: c4027f992ab9f941cecbe88a7a415d52f5a3800b86e69fe8789567f320a9b9ea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2750ae72f60c4e388b74813b74fa2580a5ebe48887eca53905b1d1173a08bac4ff931f22fadc3fb2ffec0d96417f7661fe22aba44cdaa36e7480c7df4b0f52d0
|
7
|
+
data.tar.gz: 3201606c2dce4e6e41649c8aca6432dbd2c0635c966366809bc6552ad07cc074596d3d973054bce87f79f3eca9ffb68d91e116d9b8f81ac5dad7d59b8040cd5b
|
data/CHANGELOG.md
CHANGED
@@ -1,23 +1,51 @@
|
|
1
|
-
## Rails 8.0.
|
1
|
+
## Rails 8.1.0.beta1 (September 04, 2025) ##
|
2
2
|
|
3
|
-
*
|
3
|
+
* Remove deprecated `:azure` storage service.
|
4
4
|
|
5
|
-
*
|
5
|
+
*Rafael Mendonça França*
|
6
6
|
|
7
|
-
*
|
7
|
+
* Remove unnecessary calls to the GCP metadata server.
|
8
8
|
|
9
|
-
|
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
|
-
|
19
|
+
In the cases applications do not set that, the GCP libraries automatically determine credentials.
|
13
20
|
|
14
|
-
|
21
|
+
This also enables using credentials other than those of the associated GCP
|
22
|
+
service account like when using impersonation.
|
15
23
|
|
16
|
-
|
24
|
+
*Alex Coomans*
|
17
25
|
|
18
|
-
|
26
|
+
* Direct upload progress accounts for server processing time.
|
19
27
|
|
20
|
-
|
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/),
|
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
|
-
|
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 *
|
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 *
|
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
|
-
|
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::
|
29
|
-
#
|
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
|
-
|
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
|
@@ -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 {
|
12
|
-
# {
|
13
|
-
# {
|
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::
|
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::
|
33
|
-
#
|
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::
|
81
|
-
#
|
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
|
@@ -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
|
@@ -35,7 +35,7 @@ module ActiveStorage
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def verify_integrity_of(file, checksum:)
|
38
|
-
unless
|
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::
|
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
|
91
|
+
ActiveStorage.variant_processor = app.config.active_storage.variant_processor
|
94
92
|
ActiveStorage.previewers = app.config.active_storage.previewers || []
|
95
|
-
|
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 =
|
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 =
|
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.
|
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
|
@@ -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
|
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
|
-
|
217
|
-
|
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
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
data/lib/active_storage.rb
CHANGED
@@ -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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
193
|
-
documentation_uri: https://api.rubyonrails.org/v8.0.
|
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.
|
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
|