activestorage 8.0.5 → 8.1.3
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 +80 -51
- 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/filename.rb +1 -0
- data/app/models/active_storage/variant.rb +6 -6
- data/app/models/active_storage/variation.rb +1 -1
- data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +11 -4
- data/lib/active_storage/analyzer/image_analyzer/vips.rb +22 -4
- data/lib/active_storage/analyzer/image_analyzer.rb +5 -0
- data/lib/active_storage/attached.rb +0 -1
- data/lib/active_storage/engine.rb +33 -6
- data/lib/active_storage/gem_version.rb +2 -2
- data/lib/active_storage/log_subscriber.rb +1 -1
- data/lib/active_storage/service/configurator.rb +6 -0
- data/lib/active_storage/service/gcs_service.rb +15 -5
- 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 +5 -1
- data/lib/active_storage/structured_event_subscriber.rb +79 -0
- 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/null_transformer.rb +12 -0
- data/lib/active_storage/transformers/vips.rb +11 -0
- data/lib/active_storage.rb +5 -0
- metadata +16 -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: 2a42fb3f7bbc7e2032f582d6d934bfb06245afdd8fe2db1b07b35d6c8223a617
|
|
4
|
+
data.tar.gz: aefc082d3ed704bf9c675e40560d9eb3be6f2e315ea28d7313ceabb493a95b93
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5c63ac998f475f5b3b9fb11d6bd57f0697b1ece83903f4283b35eefe490a0b0a6edb4e9b8d6d2fc4673c67948296c66af79bc25821a16ed9ad3144d76bdef5e
|
|
7
|
+
data.tar.gz: 12412531395d736326f067fdb7cdbe23f4a99a3167d4921d1882191baf4d5c79d6260d9ef598b1bcc90c0ffc35a78a2b898bcad9d8d2332c5f5749378eceeaca
|
data/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
## Rails 8.
|
|
1
|
+
## Rails 8.1.3 (March 24, 2026) ##
|
|
2
2
|
|
|
3
3
|
* Fix `ActiveStorage::Blob` content type predicate methods to handle `nil`.
|
|
4
4
|
|
|
5
5
|
*Daichi KUDO*
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
## Rails 8.
|
|
8
|
+
## Rails 8.1.2.1 (March 23, 2026) ##
|
|
9
9
|
|
|
10
10
|
* Filter user supplied metadata in DirectUploadController
|
|
11
11
|
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
*Jean Boussier*
|
|
30
30
|
|
|
31
|
+
|
|
31
32
|
* Prevent path traversal in `DiskService`.
|
|
32
33
|
|
|
33
34
|
`DiskService#path_for` now raises an `InvalidKeyError` when passed keys with dot segments (".",
|
|
@@ -56,90 +57,118 @@
|
|
|
56
57
|
*Mike Dalessio*
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
## Rails 8.
|
|
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.
|
|
60
|
+
## Rails 8.1.2 (January 08, 2026) ##
|
|
71
61
|
|
|
72
|
-
|
|
62
|
+
* Restore ADC when signing URLs with IAM for GCS
|
|
73
63
|
|
|
64
|
+
ADC was previously used for automatic authorization when signing URLs with IAM.
|
|
65
|
+
Now it is again, but the auth client is memoized so that new credentials are only
|
|
66
|
+
requested when the current ones expire. Other auth methods can now be used
|
|
67
|
+
instead by setting the authorization on `ActiveStorage::Service::GCSService#iam_client`.
|
|
74
68
|
|
|
75
|
-
|
|
69
|
+
```ruby
|
|
70
|
+
ActiveStorage::Blob.service.iam_client.authorization = Google::Auth::ImpersonatedServiceAccountCredentials.new(options)
|
|
71
|
+
```
|
|
76
72
|
|
|
77
|
-
|
|
73
|
+
This is safer than setting `Google::Apis::RequestOptions.default.authorization`
|
|
74
|
+
because it only applies to Active Storage and does not affect other Google API
|
|
75
|
+
clients.
|
|
78
76
|
|
|
79
|
-
|
|
77
|
+
*Justin Malčić*
|
|
80
78
|
|
|
81
|
-
*Zack Deveau*
|
|
82
79
|
|
|
83
|
-
## Rails 8.
|
|
80
|
+
## Rails 8.1.1 (October 28, 2025) ##
|
|
84
81
|
|
|
85
|
-
*
|
|
86
|
-
|
|
87
|
-
This fixes an issue where a record with an attachment would have
|
|
88
|
-
its dirty attributes reset, preventing your `after commit` callbacks
|
|
89
|
-
on that record to behave as expected.
|
|
82
|
+
* No changes.
|
|
90
83
|
|
|
91
|
-
Note that this change doesn't require any changes on your application
|
|
92
|
-
and is supposed to be internal. Active Storage Attachment will continue
|
|
93
|
-
to be autosaved (through a different relation).
|
|
94
84
|
|
|
95
|
-
|
|
85
|
+
## Rails 8.1.0 (October 22, 2025) ##
|
|
96
86
|
|
|
87
|
+
* Add structured events for Active Storage:
|
|
88
|
+
- `active_storage.service_upload`
|
|
89
|
+
- `active_storage.service_download`
|
|
90
|
+
- `active_storage.service_streaming_download`
|
|
91
|
+
- `active_storage.preview`
|
|
92
|
+
- `active_storage.service_delete`
|
|
93
|
+
- `active_storage.service_delete_prefixed`
|
|
94
|
+
- `active_storage.service_exist`
|
|
95
|
+
- `active_storage.service_url`
|
|
96
|
+
- `active_storage.service_mirror`
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
*Gannon McGibbon*
|
|
99
99
|
|
|
100
|
-
*
|
|
100
|
+
* Allow analyzers and variant transformer to be fully configurable
|
|
101
101
|
|
|
102
|
+
```ruby
|
|
103
|
+
# ActiveStorage.analyzers can be set to an empty array:
|
|
104
|
+
config.active_storage.analyzers = []
|
|
105
|
+
# => ActiveStorage.analyzers = []
|
|
102
106
|
|
|
103
|
-
|
|
107
|
+
# or use custom analyzer:
|
|
108
|
+
config.active_storage.analyzers = [ CustomAnalyzer ]
|
|
109
|
+
# => ActiveStorage.analyzers = [ CustomAnalyzer ]
|
|
110
|
+
```
|
|
104
111
|
|
|
105
|
-
|
|
112
|
+
If no configuration is provided, it will use the default analyzers.
|
|
106
113
|
|
|
114
|
+
You can also disable variant processor to remove warnings on startup about missing gems.
|
|
107
115
|
|
|
108
|
-
|
|
116
|
+
```ruby
|
|
117
|
+
config.active_storage.variant_processor = :disabled
|
|
118
|
+
```
|
|
109
119
|
|
|
110
|
-
*
|
|
120
|
+
*zzak*, *Alexandre Ruban*
|
|
111
121
|
|
|
122
|
+
* Remove deprecated `:azure` storage service.
|
|
112
123
|
|
|
113
|
-
|
|
124
|
+
*Rafael Mendonça França*
|
|
114
125
|
|
|
115
|
-
*
|
|
126
|
+
* Remove unnecessary calls to the GCP metadata server.
|
|
116
127
|
|
|
128
|
+
Calling Google::Auth.get_application_default triggers an explicit call to
|
|
129
|
+
the metadata server - given it was being called for significant number of
|
|
130
|
+
file operations, it can lead to considerable tail latencies and even metadata
|
|
131
|
+
server overloads. Instead, it's preferable (and significantly more efficient)
|
|
132
|
+
that applications use:
|
|
117
133
|
|
|
118
|
-
|
|
134
|
+
```ruby
|
|
135
|
+
Google::Apis::RequestOptions.default.authorization = Google::Auth.get_application_default(...)
|
|
136
|
+
```
|
|
119
137
|
|
|
120
|
-
|
|
138
|
+
In the cases applications do not set that, the GCP libraries automatically determine credentials.
|
|
121
139
|
|
|
140
|
+
This also enables using credentials other than those of the associated GCP
|
|
141
|
+
service account like when using impersonation.
|
|
122
142
|
|
|
123
|
-
|
|
143
|
+
*Alex Coomans*
|
|
124
144
|
|
|
125
|
-
*
|
|
145
|
+
* Direct upload progress accounts for server processing time.
|
|
126
146
|
|
|
127
|
-
*
|
|
147
|
+
*Jeremy Daer*
|
|
128
148
|
|
|
129
|
-
*
|
|
130
|
-
Replace the characters `"*?<>` with `-` if they exist in the Filename to match the Filename convention of Win OS.
|
|
149
|
+
* Delegate `ActiveStorage::Filename#to_str` to `#to_s`
|
|
131
150
|
|
|
132
|
-
|
|
151
|
+
Supports checking String equality:
|
|
133
152
|
|
|
134
|
-
|
|
153
|
+
```ruby
|
|
154
|
+
filename = ActiveStorage::Filename.new("file.txt")
|
|
155
|
+
filename == "file.txt" # => true
|
|
156
|
+
filename in "file.txt" # => true
|
|
157
|
+
"file.txt" == filename # => true
|
|
158
|
+
```
|
|
135
159
|
|
|
136
|
-
|
|
160
|
+
*Sean Doyle*
|
|
137
161
|
|
|
138
|
-
|
|
162
|
+
* A Blob will no longer autosave associated Attachment.
|
|
139
163
|
|
|
140
|
-
|
|
164
|
+
This fixes an issue where a record with an attachment would have
|
|
165
|
+
its dirty attributes reset, preventing your `after commit` callbacks
|
|
166
|
+
on that record to behave as expected.
|
|
141
167
|
|
|
142
|
-
|
|
168
|
+
Note that this change doesn't require any changes on your application
|
|
169
|
+
and is supposed to be internal. Active Storage Attachment will continue
|
|
170
|
+
to be autosaved (through a different relation).
|
|
143
171
|
|
|
172
|
+
*Edouard-chin*
|
|
144
173
|
|
|
145
|
-
Please check [
|
|
174
|
+
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
|
}
|
|
@@ -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
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
begin
|
|
4
|
+
gem "mini_magick"
|
|
5
|
+
require "mini_magick"
|
|
6
|
+
ActiveStorage::MINIMAGICK_AVAILABLE = true # :nodoc:
|
|
7
|
+
rescue LoadError => error
|
|
8
|
+
ActiveStorage::MINIMAGICK_AVAILABLE = false # :nodoc:
|
|
9
|
+
raise error unless error.message.include?("mini_magick")
|
|
10
|
+
end
|
|
11
|
+
|
|
3
12
|
module ActiveStorage
|
|
4
13
|
# This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. MiniMagick requires
|
|
5
14
|
# the {ImageMagick}[http://www.imagemagick.org] system library.
|
|
@@ -10,10 +19,8 @@ module ActiveStorage
|
|
|
10
19
|
|
|
11
20
|
private
|
|
12
21
|
def read_image
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
rescue LoadError
|
|
16
|
-
logger.info "Skipping image analysis because the mini_magick gem isn't installed"
|
|
22
|
+
unless MINIMAGICK_AVAILABLE
|
|
23
|
+
logger.error "Skipping image analysis because the mini_magick gem isn't installed"
|
|
17
24
|
return {}
|
|
18
25
|
end
|
|
19
26
|
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
begin
|
|
4
|
+
require "nokogiri"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# Ensure nokogiri is loaded before vips, which also depends on libxml2.
|
|
7
|
+
# See Nokogiri RFC: Stop exporting symbols:
|
|
8
|
+
# https://github.com/sparklemotion/nokogiri/discussions/2746
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
begin
|
|
12
|
+
gem "ruby-vips"
|
|
13
|
+
require "ruby-vips"
|
|
14
|
+
ActiveStorage::VIPS_AVAILABLE = true # :nodoc:
|
|
15
|
+
rescue LoadError => error
|
|
16
|
+
ActiveStorage::VIPS_AVAILABLE = false # :nodoc:
|
|
17
|
+
raise error unless error.message.match?(/libvips|ruby-vips/)
|
|
18
|
+
end
|
|
19
|
+
|
|
3
20
|
module ActiveStorage
|
|
4
21
|
# This analyzer relies on the third-party {ruby-vips}[https://github.com/libvips/ruby-vips] gem. Ruby-vips requires
|
|
5
22
|
# the {libvips}[https://libvips.github.io/libvips/] system library.
|
|
@@ -10,10 +27,8 @@ module ActiveStorage
|
|
|
10
27
|
|
|
11
28
|
private
|
|
12
29
|
def read_image
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
rescue LoadError
|
|
16
|
-
logger.info "Skipping image analysis because the ruby-vips gem isn't installed"
|
|
30
|
+
unless VIPS_AVAILABLE
|
|
31
|
+
logger.error "Skipping image analysis because the ruby-vips gem isn't installed"
|
|
17
32
|
return {}
|
|
18
33
|
end
|
|
19
34
|
|
|
@@ -35,6 +50,9 @@ module ActiveStorage
|
|
|
35
50
|
logger.error "Skipping image analysis due to a Vips error: #{error.message}"
|
|
36
51
|
{}
|
|
37
52
|
end
|
|
53
|
+
rescue ::Vips::Error => error
|
|
54
|
+
logger.error "Skipping image analysis due to an Vips error: #{error.message}"
|
|
55
|
+
{}
|
|
38
56
|
end
|
|
39
57
|
|
|
40
58
|
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
|
|
@@ -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
|
|
|
@@ -93,6 +91,35 @@ module ActiveStorage
|
|
|
93
91
|
ActiveStorage.variant_processor = app.config.active_storage.variant_processor || :mini_magick
|
|
94
92
|
ActiveStorage.previewers = app.config.active_storage.previewers || []
|
|
95
93
|
ActiveStorage.analyzers = app.config.active_storage.analyzers || []
|
|
94
|
+
|
|
95
|
+
begin
|
|
96
|
+
ActiveStorage.variant_transformer =
|
|
97
|
+
case ActiveStorage.variant_processor
|
|
98
|
+
when :disabled
|
|
99
|
+
ActiveStorage::Transformers::NullTransformer
|
|
100
|
+
when :vips
|
|
101
|
+
ActiveStorage::Transformers::Vips
|
|
102
|
+
when :mini_magick
|
|
103
|
+
ActiveStorage::Transformers::ImageMagick
|
|
104
|
+
end
|
|
105
|
+
rescue LoadError => error
|
|
106
|
+
case error.message
|
|
107
|
+
when /libvips/
|
|
108
|
+
ActiveStorage.logger.warn <<~WARNING.squish
|
|
109
|
+
Using vips to process variants requires the libvips library.
|
|
110
|
+
Please install libvips using the instructions on the libvips website.
|
|
111
|
+
WARNING
|
|
112
|
+
when /image_processing/
|
|
113
|
+
ActiveStorage.logger.warn <<~WARNING.squish
|
|
114
|
+
Generating image variants require the image_processing gem.
|
|
115
|
+
Please add `gem "image_processing", "~> 1.2"` to your Gemfile
|
|
116
|
+
or set `config.active_storage.variant_processor = :disabled`.
|
|
117
|
+
WARNING
|
|
118
|
+
else
|
|
119
|
+
raise
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
96
123
|
ActiveStorage.paths = app.config.active_storage.paths || {}
|
|
97
124
|
ActiveStorage.routes_prefix = app.config.active_storage.routes_prefix || "/rails/active_storage"
|
|
98
125
|
ActiveStorage.draw_routes = app.config.active_storage.draw_routes != false
|
|
@@ -140,9 +167,9 @@ module ActiveStorage
|
|
|
140
167
|
end
|
|
141
168
|
end
|
|
142
169
|
|
|
143
|
-
initializer "active_storage.services" do
|
|
170
|
+
initializer "active_storage.services" do |app|
|
|
144
171
|
ActiveSupport.on_load(:active_storage_blob) do
|
|
145
|
-
configs =
|
|
172
|
+
configs = app.config.active_storage.service_configurations ||=
|
|
146
173
|
begin
|
|
147
174
|
config_file = Rails.root.join("config/storage/#{Rails.env}.yml")
|
|
148
175
|
config_file = Rails.root.join("config/storage.yml") unless config_file.exist?
|
|
@@ -153,7 +180,7 @@ module ActiveStorage
|
|
|
153
180
|
|
|
154
181
|
ActiveStorage::Blob.services = ActiveStorage::Service::Registry.new(configs)
|
|
155
182
|
|
|
156
|
-
if config_choice =
|
|
183
|
+
if config_choice = app.config.active_storage.service
|
|
157
184
|
ActiveStorage::Blob.service = ActiveStorage::Blob.services.fetch(config_choice)
|
|
158
185
|
end
|
|
159
186
|
end
|
|
@@ -175,7 +202,7 @@ module ActiveStorage
|
|
|
175
202
|
initializer "action_view.configuration" do
|
|
176
203
|
config.after_initialize do |app|
|
|
177
204
|
ActiveSupport.on_load(:action_view) do
|
|
178
|
-
multiple_file_field_include_hidden = app.config.active_storage.
|
|
205
|
+
multiple_file_field_include_hidden = app.config.active_storage.multiple_file_field_include_hidden
|
|
179
206
|
|
|
180
207
|
unless multiple_file_field_include_hidden.nil?
|
|
181
208
|
ActionView::Helpers::FormHelper.multiple_file_field_include_hidden = multiple_file_field_include_hidden
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "active_support/log_subscriber"
|
|
4
4
|
|
|
5
5
|
module ActiveStorage
|
|
6
|
-
class LogSubscriber < ActiveSupport::LogSubscriber
|
|
6
|
+
class LogSubscriber < ActiveSupport::LogSubscriber # :nodoc:
|
|
7
7
|
def service_upload(event)
|
|
8
8
|
message = "Uploaded file to key: #{key_in(event)}"
|
|
9
9
|
message += " (checksum: #{event.payload[:checksum]})" if event.payload[:checksum]
|
|
@@ -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
|