activestorage 8.0.2.1 → 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 +40 -54
- data/README.md +6 -3
- data/app/assets/javascripts/activestorage.esm.js +37 -1
- data/app/assets/javascripts/activestorage.js +37 -1
- data/app/controllers/active_storage/disk_controller.rb +2 -2
- data/app/javascript/activestorage/direct_upload_controller.js +48 -1
- data/app/models/active_storage/blob/representable.rb +66 -0
- 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 +7 -7
- 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/model.rb +4 -4
- data/lib/active_storage/attached.rb +0 -1
- data/lib/active_storage/downloader.rb +1 -1
- data/lib/active_storage/engine.rb +48 -10
- data/lib/active_storage/fixture_set.rb +1 -1
- 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/s3_service.rb +19 -4
- 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,76 +1,62 @@
|
|
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
|
|
11
|
-
|
15
|
+
```ruby
|
16
|
+
Google::Apis::RequestOptions.default.authorization = Google::Auth.get_application_default(...)
|
17
|
+
```
|
12
18
|
|
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
|
-
*
|
17
|
-
|
18
|
-
This fixes an issue where a record with an attachment would have
|
19
|
-
its dirty attributes reset, preventing your `after commit` callbacks
|
20
|
-
on that record to behave as expected.
|
21
|
-
|
22
|
-
Note that this change doesn't require any changes on your application
|
23
|
-
and is supposed to be internal. Active Storage Attachment will continue
|
24
|
-
to be autosaved (through a different relation).
|
25
|
-
|
26
|
-
*Edouard-chin*
|
27
|
-
|
28
|
-
|
29
|
-
## Rails 8.0.1 (December 13, 2024) ##
|
30
|
-
|
31
|
-
* No changes.
|
32
|
-
|
33
|
-
|
34
|
-
## Rails 8.0.0.1 (December 10, 2024) ##
|
35
|
-
|
36
|
-
* No changes.
|
24
|
+
*Alex Coomans*
|
37
25
|
|
26
|
+
* Direct upload progress accounts for server processing time.
|
38
27
|
|
39
|
-
|
28
|
+
*Jeremy Daer*
|
40
29
|
|
41
|
-
*
|
30
|
+
* Delegate `ActiveStorage::Filename#to_str` to `#to_s`
|
42
31
|
|
32
|
+
Supports checking String equality:
|
43
33
|
|
44
|
-
|
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
|
+
```
|
45
40
|
|
46
|
-
*
|
41
|
+
*Sean Doyle*
|
47
42
|
|
43
|
+
* Add support for alternative MD5 implementation through `config.active_storage.checksum_implementation`.
|
48
44
|
|
49
|
-
|
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.
|
50
47
|
|
51
|
-
*
|
48
|
+
*Matt Pasquini*, *Jean Boussier*
|
52
49
|
|
50
|
+
* A Blob will no longer autosave associated Attachment.
|
53
51
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
*zzak*
|
59
|
-
|
60
|
-
* Improve `ActiveStorage::Filename#sanitized` method to handle special characters more effectively.
|
61
|
-
Replace the characters `"*?<>` with `-` if they exist in the Filename to match the Filename convention of Win OS.
|
62
|
-
|
63
|
-
*Luong Viet Dung(Martin)*
|
64
|
-
|
65
|
-
* Improve InvariableError, UnpreviewableError and UnrepresentableError message.
|
66
|
-
|
67
|
-
Include Blob ID and content_type in the messages.
|
68
|
-
|
69
|
-
*Petrik de Heus*
|
70
|
-
|
71
|
-
* Mark proxied files as `immutable` in their Cache-Control header
|
52
|
+
This fixes an issue where a record with an attachment would have
|
53
|
+
its dirty attributes reset, preventing your `after commit` callbacks
|
54
|
+
on that record to behave as expected.
|
72
55
|
|
73
|
-
|
56
|
+
Note that this change doesn't require any changes on your application
|
57
|
+
and is supposed to be internal. Active Storage Attachment will continue
|
58
|
+
to be autosaved (through a different relation).
|
74
59
|
|
60
|
+
*Edouard-chin*
|
75
61
|
|
76
|
-
Please check [
|
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
|
|
@@ -203,6 +206,6 @@ Bug reports for the Ruby on \Rails project can be filed here:
|
|
203
206
|
|
204
207
|
* https://github.com/rails/rails/issues
|
205
208
|
|
206
|
-
Feature requests should be discussed on the
|
209
|
+
Feature requests should be discussed on the rubyonrails-core forum here:
|
207
210
|
|
208
211
|
* https://discuss.rubyonrails.org/c/rubyonrails-core
|
@@ -672,7 +672,7 @@ class DirectUploadController {
|
|
672
672
|
}));
|
673
673
|
}
|
674
674
|
uploadRequestDidProgress(event) {
|
675
|
-
const progress = event.loaded / event.total *
|
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])";
|
@@ -25,13 +25,13 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
|
|
25
25
|
named_disk_service(token[:service_name]).upload token[:key], request.body, checksum: token[:checksum]
|
26
26
|
head :no_content
|
27
27
|
else
|
28
|
-
head
|
28
|
+
head ActionDispatch::Constants::UNPROCESSABLE_CONTENT
|
29
29
|
end
|
30
30
|
else
|
31
31
|
head :not_found
|
32
32
|
end
|
33
33
|
rescue ActiveStorage::IntegrityError
|
34
|
-
head
|
34
|
+
head ActionDispatch::Constants::UNPROCESSABLE_CONTENT
|
35
35
|
end
|
36
36
|
|
37
37
|
private
|
@@ -31,7 +31,8 @@ export class DirectUploadController {
|
|
31
31
|
}
|
32
32
|
|
33
33
|
uploadRequestDidProgress(event) {
|
34
|
-
|
34
|
+
// Scale upload progress to 0-90% range
|
35
|
+
const progress = (event.loaded / event.total) * 90
|
35
36
|
if (progress) {
|
36
37
|
this.dispatch("progress", { progress })
|
37
38
|
}
|
@@ -63,5 +64,51 @@ export class DirectUploadController {
|
|
63
64
|
directUploadWillStoreFileWithXHR(xhr) {
|
64
65
|
this.dispatch("before-storage-request", { xhr })
|
65
66
|
xhr.upload.addEventListener("progress", event => this.uploadRequestDidProgress(event))
|
67
|
+
|
68
|
+
// Start simulating progress after upload completes
|
69
|
+
xhr.upload.addEventListener("loadend", () => {
|
70
|
+
this.simulateResponseProgress(xhr)
|
71
|
+
})
|
72
|
+
}
|
73
|
+
|
74
|
+
simulateResponseProgress(xhr) {
|
75
|
+
let progress = 90
|
76
|
+
const startTime = Date.now()
|
77
|
+
|
78
|
+
const updateProgress = () => {
|
79
|
+
// Simulate progress from 90% to 99% over estimated time
|
80
|
+
const elapsed = Date.now() - startTime
|
81
|
+
const estimatedResponseTime = this.estimateResponseTime()
|
82
|
+
const responseProgress = Math.min(elapsed / estimatedResponseTime, 1)
|
83
|
+
progress = 90 + (responseProgress * 9) // 90% to 99%
|
84
|
+
|
85
|
+
this.dispatch("progress", { progress })
|
86
|
+
|
87
|
+
// Continue until response arrives or we hit 99%
|
88
|
+
if (xhr.readyState !== XMLHttpRequest.DONE && progress < 99) {
|
89
|
+
requestAnimationFrame(updateProgress)
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
// Stop simulation when response arrives
|
94
|
+
xhr.addEventListener("loadend", () => {
|
95
|
+
this.dispatch("progress", { progress: 100 })
|
96
|
+
})
|
97
|
+
|
98
|
+
requestAnimationFrame(updateProgress)
|
99
|
+
}
|
100
|
+
|
101
|
+
estimateResponseTime() {
|
102
|
+
// Base estimate: 1 second for small files, scaling up for larger files
|
103
|
+
const fileSize = this.file.size
|
104
|
+
const MB = 1024 * 1024
|
105
|
+
|
106
|
+
if (fileSize < MB) {
|
107
|
+
return 1000 // 1 second for files under 1MB
|
108
|
+
} else if (fileSize < 10 * MB) {
|
109
|
+
return 2000 // 2 seconds for files 1-10MB
|
110
|
+
} else {
|
111
|
+
return 3000 + (fileSize / MB * 50) // 3+ seconds for larger files
|
112
|
+
}
|
66
113
|
}
|
67
114
|
}
|
@@ -31,6 +31,72 @@ module ActiveStorage::Blob::Representable
|
|
31
31
|
# Raises ActiveStorage::InvariableError if the variant processor cannot
|
32
32
|
# transform the blob. To determine whether a blob is variable, call
|
33
33
|
# ActiveStorage::Blob#variable?.
|
34
|
+
#
|
35
|
+
# ==== Options
|
36
|
+
#
|
37
|
+
# Options are defined by the {image_processing gem}[https://github.com/janko/image_processing],
|
38
|
+
# and depend on which variant processor you are using:
|
39
|
+
# {Vips}[https://github.com/janko/image_processing/blob/master/doc/vips.md] or
|
40
|
+
# {MiniMagick}[https://github.com/janko/image_processing/blob/master/doc/minimagick.md].
|
41
|
+
# However, both variant processors support the following options:
|
42
|
+
#
|
43
|
+
# [+:resize_to_limit+]
|
44
|
+
# Downsizes the image to fit within the specified dimensions while retaining
|
45
|
+
# the original aspect ratio. Will only resize the image if it's larger than
|
46
|
+
# the specified dimensions.
|
47
|
+
#
|
48
|
+
# user.avatar.variant(resize_to_limit: [100, 100])
|
49
|
+
#
|
50
|
+
# [+:resize_to_fit+]
|
51
|
+
# Resizes the image to fit within the specified dimensions while retaining
|
52
|
+
# the original aspect ratio. Will downsize the image if it's larger than the
|
53
|
+
# specified dimensions or upsize if it's smaller.
|
54
|
+
#
|
55
|
+
# user.avatar.variant(resize_to_fit: [100, 100])
|
56
|
+
#
|
57
|
+
# [+:resize_to_fill+]
|
58
|
+
# Resizes the image to fill the specified dimensions while retaining the
|
59
|
+
# original aspect ratio. If necessary, will crop the image in the larger
|
60
|
+
# dimension.
|
61
|
+
#
|
62
|
+
# user.avatar.variant(resize_to_fill: [100, 100])
|
63
|
+
#
|
64
|
+
# [+:resize_and_pad+]
|
65
|
+
# Resizes the image to fit within the specified dimensions while retaining
|
66
|
+
# the original aspect ratio. If necessary, will pad the remaining area with
|
67
|
+
# transparent color if source image has alpha channel, black otherwise.
|
68
|
+
#
|
69
|
+
# user.avatar.variant(resize_and_pad: [100, 100])
|
70
|
+
#
|
71
|
+
# [+:crop+]
|
72
|
+
# Extracts an area from an image. The first two arguments are the left and
|
73
|
+
# top edges of area to extract, while the last two arguments are the width
|
74
|
+
# and height of the area to extract.
|
75
|
+
#
|
76
|
+
# user.avatar.variant(crop: [20, 50, 300, 300])
|
77
|
+
#
|
78
|
+
# [+:rotate+]
|
79
|
+
# Rotates the image by the specified angle.
|
80
|
+
#
|
81
|
+
# user.avatar.variant(rotate: 90)
|
82
|
+
#
|
83
|
+
# Some options, including those listed above, can accept additional
|
84
|
+
# processor-specific values which can be passed as a trailing hash:
|
85
|
+
#
|
86
|
+
# <!-- Vips supports configuring `crop` for many of its transformations -->
|
87
|
+
# <%= image_tag user.avatar.variant(resize_to_fill: [100, 100, { crop: :centre }]) %>
|
88
|
+
#
|
89
|
+
# If migrating an existing application between MiniMagick and Vips, you will
|
90
|
+
# need to update processor-specific options:
|
91
|
+
#
|
92
|
+
# <!-- MiniMagick -->
|
93
|
+
# <%= image_tag user.avatar.variant(resize_to_limit: [100, 100], format: :jpeg,
|
94
|
+
# sampling_factor: "4:2:0", strip: true, interlace: "JPEG", colorspace: "sRGB", quality: 80) %>
|
95
|
+
#
|
96
|
+
# <!-- Vips -->
|
97
|
+
# <%= image_tag user.avatar.variant(resize_to_limit: [100, 100], format: :jpeg,
|
98
|
+
# saver: { subsample_mode: "on", strip: true, interlace: true, quality: 80 }) %>
|
99
|
+
#
|
34
100
|
def variant(transformations)
|
35
101
|
if variable?
|
36
102
|
variant_class.new(self, ActiveStorage::Variation.wrap(transformations).default_to(default_variant_transformations))
|
@@ -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,17 +8,17 @@
|
|
8
8
|
#
|
9
9
|
# Variants rely on {ImageProcessing}[https://github.com/janko/image_processing] gem for the actual transformations
|
10
10
|
# of the file, so you must add <tt>gem "image_processing"</tt> to your Gemfile if you wish to use variants. By
|
11
|
-
# default, images will be processed with {
|
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
|
@@ -74,7 +74,7 @@ class ActiveStorage::Variant
|
|
74
74
|
"variants/#{blob.key}/#{OpenSSL::Digest::SHA256.hexdigest(variation.key)}"
|
75
75
|
end
|
76
76
|
|
77
|
-
# Returns the URL of the blob variant on the service. See
|
77
|
+
# Returns the URL of the blob variant on the service. See ActiveStorage::Blob#url for details.
|
78
78
|
#
|
79
79
|
# Use <tt>url_for(variant)</tt> (or the implied form, like <tt>link_to variant</tt> or <tt>redirect_to variant</tt>) to get the stable URL
|
80
80
|
# for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method
|
@@ -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
|
@@ -61,8 +61,8 @@ module ActiveStorage
|
|
61
61
|
# There is no column defined on the model side, Active Storage takes
|
62
62
|
# care of the mapping between your records and the attachment.
|
63
63
|
#
|
64
|
-
# Under the covers, this relationship is implemented as a +has_one+ association to
|
65
|
-
# ActiveStorage::Attachment record and a +has_one-through+ association to
|
64
|
+
# Under the covers, this relationship is implemented as a +has_one+ association to an
|
65
|
+
# ActiveStorage::Attachment record and a +has_one-through+ association to an
|
66
66
|
# ActiveStorage::Blob record. These associations are available as +avatar_attachment+
|
67
67
|
# and +avatar_blob+. But you shouldn't need to work with these associations directly in
|
68
68
|
# most circumstances.
|
@@ -163,8 +163,8 @@ module ActiveStorage
|
|
163
163
|
# There are no columns defined on the model side, Active Storage takes
|
164
164
|
# care of the mapping between your records and the attachments.
|
165
165
|
#
|
166
|
-
# Under the covers, this relationship is implemented as a +has_many+ association to
|
167
|
-
# ActiveStorage::Attachment record and a +has_many-through+ association to
|
166
|
+
# Under the covers, this relationship is implemented as a +has_many+ association to an
|
167
|
+
# ActiveStorage::Attachment record and a +has_many-through+ association to an
|
168
168
|
# ActiveStorage::Blob record. These associations are available as +photos_attachments+
|
169
169
|
# and +photos_blobs+. But you shouldn't need to work with these associations directly in
|
170
170
|
# most circumstances.
|
@@ -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
|