activestorage 8.0.5 → 8.1.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -115
- data/README.md +5 -2
- 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 +0 -4
- data/app/controllers/concerns/active_storage/streaming.rb +1 -8
- 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 +5 -26
- 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 -10
- data/lib/active_storage/errors.rb +0 -4
- 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 +3 -47
- 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 +4 -5
- metadata +15 -14
- 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,86 +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
|
+
* Remove unnecessary calls to the GCP metadata server.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
Calling Google::Auth.get_application_default triggers an explicit call to
|
|
10
|
+
the metadata server - given it was being called for significant number of
|
|
11
|
+
file operations, it can lead to considerable tail latencies and even metadata
|
|
12
|
+
server overloads. Instead, it's preferable (and significantly more efficient)
|
|
13
|
+
that applications use:
|
|
9
14
|
|
|
10
|
-
|
|
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
|
-
|
|
19
|
-
Content ranges that are too big can result in denial of service.
|
|
26
|
+
* Direct upload progress accounts for server processing time.
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
*Jeremy Daer*
|
|
22
29
|
|
|
23
|
-
|
|
30
|
+
* Delegate `ActiveStorage::Filename#to_str` to `#to_s`
|
|
24
31
|
|
|
25
|
-
|
|
32
|
+
Supports checking String equality:
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
```ruby
|
|
35
|
+
filename = ActiveStorage::Filename.new("file.txt")
|
|
36
|
+
filename == "file.txt" # => true
|
|
37
|
+
filename in "file.txt" # => true
|
|
38
|
+
"file.txt" == filename # => true
|
|
39
|
+
```
|
|
28
40
|
|
|
29
|
-
*
|
|
41
|
+
*Sean Doyle*
|
|
30
42
|
|
|
31
|
-
*
|
|
43
|
+
* Add support for alternative MD5 implementation through `config.active_storage.checksum_implementation`.
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
Also automatically degrade to using the slower `Digest::MD5` implementation if `OpenSSL::Digest::MD5`
|
|
46
|
+
is found to be disabled because of OpenSSL FIPS mode.
|
|
35
47
|
|
|
36
|
-
|
|
37
|
-
example containing null bytes or having an incompatible encoding. Previously, the exception
|
|
38
|
-
raised may have been `ArgumentError` or `Encoding::CompatibilityError`.
|
|
39
|
-
|
|
40
|
-
`DiskController` now explicitly rescues `InvalidKeyError` with appropriate HTTP status codes.
|
|
41
|
-
|
|
42
|
-
[CVE-2026-33195]
|
|
43
|
-
|
|
44
|
-
*Mike Dalessio*
|
|
45
|
-
|
|
46
|
-
* Prevent glob injection in `DiskService#delete_prefixed`.
|
|
47
|
-
|
|
48
|
-
Escape glob metacharacters in the resolved path before passing to `Dir.glob`.
|
|
49
|
-
|
|
50
|
-
Note that this change breaks any existing code that is relying on `delete_prefixed` to expand
|
|
51
|
-
glob metacharacters. This change presumes that is unintended behavior (as other storage services
|
|
52
|
-
do not respect these metacharacters).
|
|
53
|
-
|
|
54
|
-
[CVE-2026-33202]
|
|
55
|
-
|
|
56
|
-
*Mike Dalessio*
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
## Rails 8.0.4 (October 28, 2025) ##
|
|
60
|
-
|
|
61
|
-
* No changes.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
## Rails 8.0.3 (September 22, 2025) ##
|
|
65
|
-
|
|
66
|
-
* Address deprecation of `Aws::S3::Object#upload_stream` in `ActiveStorage::Service::S3Service`.
|
|
67
|
-
|
|
68
|
-
*Joshua Young*
|
|
69
|
-
|
|
70
|
-
* Fix `config.active_storage.touch_attachment_records` to work with eager loading.
|
|
71
|
-
|
|
72
|
-
*fatkodima*
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
## Rails 8.0.2.1 (August 13, 2025) ##
|
|
76
|
-
|
|
77
|
-
* Remove dangerous transformations
|
|
78
|
-
|
|
79
|
-
[CVE-2025-24293]
|
|
80
|
-
|
|
81
|
-
*Zack Deveau*
|
|
82
|
-
|
|
83
|
-
## Rails 8.0.2 (March 12, 2025) ##
|
|
48
|
+
*Matt Pasquini*, *Jean Boussier*
|
|
84
49
|
|
|
85
50
|
* A Blob will no longer autosave associated Attachment.
|
|
86
51
|
|
|
@@ -94,52 +59,4 @@
|
|
|
94
59
|
|
|
95
60
|
*Edouard-chin*
|
|
96
61
|
|
|
97
|
-
|
|
98
|
-
## Rails 8.0.1 (December 13, 2024) ##
|
|
99
|
-
|
|
100
|
-
* No changes.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
## Rails 8.0.0.1 (December 10, 2024) ##
|
|
104
|
-
|
|
105
|
-
* No changes.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
## Rails 8.0.0 (November 07, 2024) ##
|
|
109
|
-
|
|
110
|
-
* No changes.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
## Rails 8.0.0.rc2 (October 30, 2024) ##
|
|
114
|
-
|
|
115
|
-
* No changes.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
## Rails 8.0.0.rc1 (October 19, 2024) ##
|
|
119
|
-
|
|
120
|
-
* No changes.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
## Rails 8.0.0.beta1 (September 26, 2024) ##
|
|
124
|
-
|
|
125
|
-
* Deprecate `ActiveStorage::Service::AzureStorageService`.
|
|
126
|
-
|
|
127
|
-
*zzak*
|
|
128
|
-
|
|
129
|
-
* Improve `ActiveStorage::Filename#sanitized` method to handle special characters more effectively.
|
|
130
|
-
Replace the characters `"*?<>` with `-` if they exist in the Filename to match the Filename convention of Win OS.
|
|
131
|
-
|
|
132
|
-
*Luong Viet Dung(Martin)*
|
|
133
|
-
|
|
134
|
-
* Improve InvariableError, UnpreviewableError and UnrepresentableError message.
|
|
135
|
-
|
|
136
|
-
Include Blob ID and content_type in the messages.
|
|
137
|
-
|
|
138
|
-
*Petrik de Heus*
|
|
139
|
-
|
|
140
|
-
* Mark proxied files as `immutable` in their Cache-Control header
|
|
141
|
-
|
|
142
|
-
*Nate Matykiewicz*
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
Please check [7-2-stable](https://github.com/rails/rails/blob/7-2-stable/activestorage/CHANGELOG.md) for previous changes.
|
|
62
|
+
Please check [8-0-stable](https://github.com/rails/rails/blob/8-0-stable/activestorage/CHANGELOG.md) for previous changes.
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Active Storage
|
|
2
2
|
|
|
3
|
-
Active Storage makes it simple to upload and reference files in cloud services like [Amazon S3](https://aws.amazon.com/s3/), [Google Cloud Storage](https://cloud.google.com/storage/docs/),
|
|
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])";
|
|
@@ -17,8 +17,6 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
|
|
|
17
17
|
end
|
|
18
18
|
rescue Errno::ENOENT
|
|
19
19
|
head :not_found
|
|
20
|
-
rescue ActiveStorage::InvalidKeyError
|
|
21
|
-
head :not_found
|
|
22
20
|
end
|
|
23
21
|
|
|
24
22
|
def update
|
|
@@ -34,8 +32,6 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
|
|
|
34
32
|
end
|
|
35
33
|
rescue ActiveStorage::IntegrityError
|
|
36
34
|
head ActionDispatch::Constants::UNPROCESSABLE_CONTENT
|
|
37
|
-
rescue ActiveStorage::InvalidKeyError
|
|
38
|
-
head ActionDispatch::Constants::UNPROCESSABLE_CONTENT
|
|
39
35
|
end
|
|
40
36
|
|
|
41
37
|
private
|
|
@@ -14,8 +14,7 @@ module ActiveStorage::Streaming
|
|
|
14
14
|
def send_blob_byte_range_data(blob, range_header, disposition: nil)
|
|
15
15
|
ranges = Rack::Utils.get_byte_ranges(range_header, blob.byte_size)
|
|
16
16
|
|
|
17
|
-
return head(:range_not_satisfiable)
|
|
18
|
-
return head(:range_not_satisfiable) if ranges.length > ActiveStorage.streaming_max_ranges
|
|
17
|
+
return head(:range_not_satisfiable) if ranges.blank? || ranges.all?(&:blank?)
|
|
19
18
|
|
|
20
19
|
if ranges.length == 1
|
|
21
20
|
range = ranges.first
|
|
@@ -52,12 +51,6 @@ module ActiveStorage::Streaming
|
|
|
52
51
|
)
|
|
53
52
|
end
|
|
54
53
|
|
|
55
|
-
def ranges_valid?(ranges)
|
|
56
|
-
return false if ranges.blank? || ranges.all?(&:blank?)
|
|
57
|
-
|
|
58
|
-
ranges.sum { |range| range.end - range.begin } < ActiveStorage.streaming_chunk_max_size
|
|
59
|
-
end
|
|
60
|
-
|
|
61
54
|
# Stream the blob from storage directly to the response. The disposition can be controlled by setting +disposition+.
|
|
62
55
|
# The content type and filename is set directly from the +blob+.
|
|
63
56
|
def send_blob_stream(blob, disposition: nil) # :doc:
|
|
@@ -31,7 +31,8 @@ export class DirectUploadController {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
uploadRequestDidProgress(event) {
|
|
34
|
-
|
|
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
|
|
@@ -16,18 +16,10 @@
|
|
|
16
16
|
# Blobs are intended to be immutable in as-so-far as their reference to a specific file goes. You're allowed to
|
|
17
17
|
# update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file.
|
|
18
18
|
# If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one.
|
|
19
|
-
#
|
|
20
|
-
# When using a custom +key+, the value is treated as trusted. Using untrusted user input
|
|
21
|
-
# as the key may result in unexpected behavior.
|
|
22
19
|
class ActiveStorage::Blob < ActiveStorage::Record
|
|
23
20
|
MINIMUM_TOKEN_LENGTH = 28
|
|
24
21
|
|
|
25
22
|
has_secure_token :key, length: MINIMUM_TOKEN_LENGTH
|
|
26
|
-
|
|
27
|
-
# FIXME: these property should never have been stored in the metadata.
|
|
28
|
-
# The blob table should be migrated to have dedicated columns for theses.
|
|
29
|
-
PROTECTED_METADATA = %w(analyzed identified composed)
|
|
30
|
-
private_constant :PROTECTED_METADATA
|
|
31
23
|
store :metadata, accessors: [ :analyzed, :identified, :composed ], coder: ActiveRecord::Coders::JSON
|
|
32
24
|
|
|
33
25
|
class_attribute :services, default: {}
|
|
@@ -100,9 +92,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
100
92
|
# be saved before the upload begins to prevent the upload clobbering another due to key collisions.
|
|
101
93
|
# When providing a content type, pass <tt>identify: false</tt> to bypass
|
|
102
94
|
# automatic content type inference.
|
|
103
|
-
#
|
|
104
|
-
# The optional +key+ parameter is treated as trusted. Using untrusted user input
|
|
105
|
-
# as the key may result in unexpected behavior.
|
|
106
95
|
def create_and_upload!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil)
|
|
107
96
|
create_after_unfurling!(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify).tap do |blob|
|
|
108
97
|
blob.upload_without_unfurling(io)
|
|
@@ -115,7 +104,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
115
104
|
# Once the form using the direct upload is submitted, the blob can be associated with the right record using
|
|
116
105
|
# the signed ID.
|
|
117
106
|
def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, service_name: nil, record: nil)
|
|
118
|
-
metadata = filter_metadata(metadata)
|
|
119
107
|
create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata, service_name: service_name
|
|
120
108
|
end
|
|
121
109
|
|
|
@@ -163,15 +151,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
163
151
|
combined_blob.save!
|
|
164
152
|
end
|
|
165
153
|
end
|
|
166
|
-
|
|
167
|
-
private
|
|
168
|
-
def filter_metadata(metadata)
|
|
169
|
-
if metadata.is_a?(Hash)
|
|
170
|
-
metadata.without(*PROTECTED_METADATA)
|
|
171
|
-
else
|
|
172
|
-
metadata
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
154
|
end
|
|
176
155
|
|
|
177
156
|
include Analyzable
|
|
@@ -210,22 +189,22 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
210
189
|
|
|
211
190
|
# Returns true if the content_type of this blob is in the image range, like image/png.
|
|
212
191
|
def image?
|
|
213
|
-
content_type
|
|
192
|
+
content_type.start_with?("image")
|
|
214
193
|
end
|
|
215
194
|
|
|
216
195
|
# Returns true if the content_type of this blob is in the audio range, like audio/mpeg.
|
|
217
196
|
def audio?
|
|
218
|
-
content_type
|
|
197
|
+
content_type.start_with?("audio")
|
|
219
198
|
end
|
|
220
199
|
|
|
221
200
|
# Returns true if the content_type of this blob is in the video range, like video/mp4.
|
|
222
201
|
def video?
|
|
223
|
-
content_type
|
|
202
|
+
content_type.start_with?("video")
|
|
224
203
|
end
|
|
225
204
|
|
|
226
205
|
# Returns true if the content_type of this blob is in the text range, like text/plain.
|
|
227
206
|
def text?
|
|
228
|
-
content_type
|
|
207
|
+
content_type.start_with?("text")
|
|
229
208
|
end
|
|
230
209
|
|
|
231
210
|
# Returns the URL of the blob on the service. This returns a permanent URL for public files, and returns a
|
|
@@ -353,7 +332,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
353
332
|
def compute_checksum_in_chunks(io)
|
|
354
333
|
raise ArgumentError, "io must be rewindable" unless io.respond_to?(:rewind)
|
|
355
334
|
|
|
356
|
-
|
|
335
|
+
ActiveStorage.checksum_implementation.new.tap do |checksum|
|
|
357
336
|
read_buffer = "".b
|
|
358
337
|
while io.read(5.megabytes, read_buffer)
|
|
359
338
|
checksum << read_buffer
|
|
@@ -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
|