activestorage 7.2.2.1 → 8.1.2
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 +76 -64
- data/README.md +8 -5
- data/app/assets/javascripts/activestorage.esm.js +38 -2
- data/app/assets/javascripts/activestorage.js +38 -1
- data/app/controllers/active_storage/direct_uploads_controller.rb +1 -1
- data/app/controllers/active_storage/disk_controller.rb +2 -2
- data/app/controllers/concerns/active_storage/streaming.rb +9 -0
- data/app/javascript/activestorage/direct_upload_controller.js +48 -1
- data/app/javascript/activestorage/index.js +2 -1
- data/app/models/active_storage/blob/representable.rb +71 -5
- data/app/models/active_storage/blob.rb +3 -20
- data/app/models/active_storage/filename.rb +2 -1
- data/app/models/active_storage/variant.rb +12 -12
- 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 +23 -5
- data/lib/active_storage/analyzer/image_analyzer.rb +5 -0
- data/lib/active_storage/attached/changes/create_one.rb +1 -1
- data/lib/active_storage/attached/model.rb +41 -18
- data/lib/active_storage/attached.rb +0 -1
- data/lib/active_storage/engine.rb +37 -7
- data/lib/active_storage/fixture_set.rb +1 -1
- data/lib/active_storage/gem_version.rb +3 -3
- 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 +13 -4
- 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 +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 -3
- metadata +19 -19
- data/lib/active_storage/service/azure_storage_service.rb +0 -194
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b700aaa6ff149a5ce6ff88794c94141d8d7e0895c6561578756727ecf5a3128b
|
|
4
|
+
data.tar.gz: 2eecb99972e95b8495aafbf2458d775bae6a7bcabf4abbc1fc0dd2286b1b140e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ddb19a88f6c95a4be6d198a7342e830a53d03a24c2f62e63ae2b41d1a94cb863dde34079d168a90e7c661736974691395605f991987ade4ff5164e187ad860e4
|
|
7
|
+
data.tar.gz: 3499297b88322fa207ace2e9f140a53f678f88c7fbe06646d2924c2b29c17e1d8fd8723058382310d307960ba3813733fb5848f232f17c868f09c2d75be6b373
|
data/CHANGELOG.md
CHANGED
|
@@ -1,103 +1,115 @@
|
|
|
1
|
-
## Rails
|
|
2
|
-
|
|
3
|
-
* No changes.
|
|
1
|
+
## Rails 8.1.2 (January 08, 2026) ##
|
|
4
2
|
|
|
3
|
+
* Restore ADC when signing URLs with IAM for GCS
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
ADC was previously used for automatic authorization when signing URLs with IAM.
|
|
6
|
+
Now it is again, but the auth client is memoized so that new credentials are only
|
|
7
|
+
requested when the current ones expire. Other auth methods can now be used
|
|
8
|
+
instead by setting the authorization on `ActiveStorage::Service::GCSService#iam_client`.
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
```ruby
|
|
11
|
+
ActiveStorage::Blob.service.iam_client.authorization = Google::Auth::ImpersonatedServiceAccountCredentials.new(options)
|
|
12
|
+
```
|
|
9
13
|
|
|
14
|
+
This is safer than setting `Google::Apis::RequestOptions.default.authorization`
|
|
15
|
+
because it only applies to Active Storage and does not affect other Google API
|
|
16
|
+
clients.
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* No changes.
|
|
18
|
+
*Justin Malčić*
|
|
14
19
|
|
|
15
20
|
|
|
16
|
-
## Rails
|
|
21
|
+
## Rails 8.1.1 (October 28, 2025) ##
|
|
17
22
|
|
|
18
23
|
* No changes.
|
|
19
24
|
|
|
20
25
|
|
|
21
|
-
## Rails
|
|
22
|
-
|
|
23
|
-
* No changes.
|
|
26
|
+
## Rails 8.1.0 (October 22, 2025) ##
|
|
24
27
|
|
|
28
|
+
* Add structured events for Active Storage:
|
|
29
|
+
- `active_storage.service_upload`
|
|
30
|
+
- `active_storage.service_download`
|
|
31
|
+
- `active_storage.service_streaming_download`
|
|
32
|
+
- `active_storage.preview`
|
|
33
|
+
- `active_storage.service_delete`
|
|
34
|
+
- `active_storage.service_delete_prefixed`
|
|
35
|
+
- `active_storage.service_exist`
|
|
36
|
+
- `active_storage.service_url`
|
|
37
|
+
- `active_storage.service_mirror`
|
|
25
38
|
|
|
26
|
-
|
|
39
|
+
*Gannon McGibbon*
|
|
27
40
|
|
|
28
|
-
*
|
|
41
|
+
* Allow analyzers and variant transformer to be fully configurable
|
|
29
42
|
|
|
30
|
-
|
|
43
|
+
```ruby
|
|
44
|
+
# ActiveStorage.analyzers can be set to an empty array:
|
|
45
|
+
config.active_storage.analyzers = []
|
|
46
|
+
# => ActiveStorage.analyzers = []
|
|
31
47
|
|
|
32
|
-
|
|
48
|
+
# or use custom analyzer:
|
|
49
|
+
config.active_storage.analyzers = [ CustomAnalyzer ]
|
|
50
|
+
# => ActiveStorage.analyzers = [ CustomAnalyzer ]
|
|
51
|
+
```
|
|
33
52
|
|
|
34
|
-
|
|
53
|
+
If no configuration is provided, it will use the default analyzers.
|
|
35
54
|
|
|
36
|
-
|
|
55
|
+
You can also disable variant processor to remove warnings on startup about missing gems.
|
|
37
56
|
|
|
38
|
-
|
|
57
|
+
```ruby
|
|
58
|
+
config.active_storage.variant_processor = :disabled
|
|
59
|
+
```
|
|
39
60
|
|
|
40
|
-
*
|
|
41
|
-
is set.
|
|
61
|
+
*zzak*, *Alexandre Ruban*
|
|
42
62
|
|
|
43
|
-
|
|
63
|
+
* Remove deprecated `:azure` storage service.
|
|
44
64
|
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
*Jonathan del Strother*
|
|
48
|
-
|
|
49
|
-
* Fix N+1 query when fetching preview images for non-image assets.
|
|
50
|
-
|
|
51
|
-
*Aaron Patterson & Justin Searls*
|
|
52
|
-
|
|
53
|
-
* Fix all Active Storage database related models to respect
|
|
54
|
-
`ActiveRecord::Base.table_name_prefix` configuration.
|
|
55
|
-
|
|
56
|
-
*Chedli Bourguiba*
|
|
57
|
-
|
|
58
|
-
* Fix `ActiveStorage::Representations::ProxyController` not returning the proper
|
|
59
|
-
preview image variant for previewable files.
|
|
60
|
-
|
|
61
|
-
*Chedli Bourguiba*
|
|
65
|
+
*Rafael Mendonça França*
|
|
62
66
|
|
|
63
|
-
*
|
|
64
|
-
variants.
|
|
67
|
+
* Remove unnecessary calls to the GCP metadata server.
|
|
65
68
|
|
|
66
|
-
|
|
69
|
+
Calling Google::Auth.get_application_default triggers an explicit call to
|
|
70
|
+
the metadata server - given it was being called for significant number of
|
|
71
|
+
file operations, it can lead to considerable tail latencies and even metadata
|
|
72
|
+
server overloads. Instead, it's preferable (and significantly more efficient)
|
|
73
|
+
that applications use:
|
|
67
74
|
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
```ruby
|
|
76
|
+
Google::Apis::RequestOptions.default.authorization = Google::Auth.get_application_default(...)
|
|
77
|
+
```
|
|
70
78
|
|
|
71
|
-
|
|
79
|
+
In the cases applications do not set that, the GCP libraries automatically determine credentials.
|
|
72
80
|
|
|
73
|
-
|
|
81
|
+
This also enables using credentials other than those of the associated GCP
|
|
82
|
+
service account like when using impersonation.
|
|
74
83
|
|
|
75
|
-
|
|
76
|
-
image instead of generating a variant with the exact same dimensions.
|
|
84
|
+
*Alex Coomans*
|
|
77
85
|
|
|
78
|
-
|
|
86
|
+
* Direct upload progress accounts for server processing time.
|
|
79
87
|
|
|
80
|
-
*
|
|
88
|
+
*Jeremy Daer*
|
|
81
89
|
|
|
82
|
-
|
|
83
|
-
generate the full-sized preview image and the `:thumb` variant of it.
|
|
84
|
-
Previously, the `:thumb` variant would not be generated until a further call
|
|
85
|
-
to e.g. `processed.url`.
|
|
90
|
+
* Delegate `ActiveStorage::Filename#to_str` to `#to_s`
|
|
86
91
|
|
|
87
|
-
|
|
92
|
+
Supports checking String equality:
|
|
88
93
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
```ruby
|
|
95
|
+
filename = ActiveStorage::Filename.new("file.txt")
|
|
96
|
+
filename == "file.txt" # => true
|
|
97
|
+
filename in "file.txt" # => true
|
|
98
|
+
"file.txt" == filename # => true
|
|
99
|
+
```
|
|
92
100
|
|
|
93
|
-
*
|
|
101
|
+
*Sean Doyle*
|
|
94
102
|
|
|
95
|
-
*
|
|
103
|
+
* A Blob will no longer autosave associated Attachment.
|
|
96
104
|
|
|
97
|
-
|
|
105
|
+
This fixes an issue where a record with an attachment would have
|
|
106
|
+
its dirty attributes reset, preventing your `after commit` callbacks
|
|
107
|
+
on that record to behave as expected.
|
|
98
108
|
|
|
99
|
-
|
|
109
|
+
Note that this change doesn't require any changes on your application
|
|
110
|
+
and is supposed to be internal. Active Storage Attachment will continue
|
|
111
|
+
to be autosaved (through a different relation).
|
|
100
112
|
|
|
101
|
-
*
|
|
113
|
+
*Edouard-chin*
|
|
102
114
|
|
|
103
|
-
Please check [
|
|
115
|
+
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
|
|
|
@@ -73,7 +73,7 @@ end
|
|
|
73
73
|
```erb
|
|
74
74
|
<%= form_with model: @message, local: true do |form| %>
|
|
75
75
|
<%= form.text_field :title, placeholder: "Title" %><br>
|
|
76
|
-
<%= form.
|
|
76
|
+
<%= form.textarea :content %><br><br>
|
|
77
77
|
|
|
78
78
|
<%= form.file_field :images, multiple: true %><br>
|
|
79
79
|
<%= form.submit %>
|
|
@@ -88,7 +88,7 @@ class MessagesController < ApplicationController
|
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
def create
|
|
91
|
-
message = Message.create! params.
|
|
91
|
+
message = Message.create! params.expect(message: [ :title, :content, images: [] ])
|
|
92
92
|
redirect_to message
|
|
93
93
|
end
|
|
94
94
|
|
|
@@ -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
|
|
|
@@ -845,4 +881,4 @@ function autostart() {
|
|
|
845
881
|
|
|
846
882
|
setTimeout(autostart, 1);
|
|
847
883
|
|
|
848
|
-
export { DirectUpload, DirectUploadController, DirectUploadsController, start };
|
|
884
|
+
export { DirectUpload, DirectUploadController, DirectUploadsController, dispatchEvent, start };
|
|
@@ -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])";
|
|
@@ -822,6 +858,7 @@
|
|
|
822
858
|
exports.DirectUpload = DirectUpload;
|
|
823
859
|
exports.DirectUploadController = DirectUploadController;
|
|
824
860
|
exports.DirectUploadsController = DirectUploadsController;
|
|
861
|
+
exports.dispatchEvent = dispatchEvent;
|
|
825
862
|
exports.start = start;
|
|
826
863
|
Object.defineProperty(exports, "__esModule", {
|
|
827
864
|
value: true
|
|
@@ -11,7 +11,7 @@ class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
|
|
|
11
11
|
|
|
12
12
|
private
|
|
13
13
|
def blob_args
|
|
14
|
-
params.
|
|
14
|
+
params.expect(blob: [:filename, :byte_size, :checksum, :content_type, metadata: {}]).to_h.symbolize_keys
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def direct_upload_json(blob)
|
|
@@ -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
|
|
@@ -61,6 +61,15 @@ module ActiveStorage::Streaming
|
|
|
61
61
|
blob.download do |chunk|
|
|
62
62
|
stream.write chunk
|
|
63
63
|
end
|
|
64
|
+
rescue ActiveStorage::FileNotFoundError
|
|
65
|
+
expires_now
|
|
66
|
+
head :not_found
|
|
67
|
+
rescue
|
|
68
|
+
# Status and caching headers are already set, but not committed.
|
|
69
|
+
# Change the status to 500 manually.
|
|
70
|
+
expires_now
|
|
71
|
+
head :internal_server_error
|
|
72
|
+
raise
|
|
64
73
|
end
|
|
65
74
|
end
|
|
66
75
|
end
|
|
@@ -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
|
}
|
|
@@ -2,7 +2,8 @@ import { start } from "./ujs"
|
|
|
2
2
|
import { DirectUpload } from "./direct_upload"
|
|
3
3
|
import { DirectUploadController } from "./direct_upload_controller"
|
|
4
4
|
import { DirectUploadsController } from "./direct_uploads_controller"
|
|
5
|
-
|
|
5
|
+
import { dispatchEvent } from "./helpers"
|
|
6
|
+
export { start, DirectUpload, DirectUploadController, DirectUploadsController, dispatchEvent }
|
|
6
7
|
|
|
7
8
|
function autostart() {
|
|
8
9
|
if (window.ActiveStorage) {
|
|
@@ -25,17 +25,83 @@ 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
|
-
# can then produce on-demand.
|
|
28
|
+
# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::Representations::ProxyController
|
|
29
|
+
# or ActiveStorage::Representations::RedirectController can then produce on-demand.
|
|
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
|
|
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))
|
|
37
103
|
else
|
|
38
|
-
raise ActiveStorage::InvariableError
|
|
104
|
+
raise ActiveStorage::InvariableError, "Can't transform blob with ID=#{id} and content_type=#{content_type}"
|
|
39
105
|
end
|
|
40
106
|
end
|
|
41
107
|
|
|
@@ -64,7 +130,7 @@ module ActiveStorage::Blob::Representable
|
|
|
64
130
|
if previewable?
|
|
65
131
|
ActiveStorage::Preview.new(self, transformations)
|
|
66
132
|
else
|
|
67
|
-
raise ActiveStorage::UnpreviewableError
|
|
133
|
+
raise ActiveStorage::UnpreviewableError, "No previewer found for blob with ID=#{id} and content_type=#{content_type}"
|
|
68
134
|
end
|
|
69
135
|
end
|
|
70
136
|
|
|
@@ -89,7 +155,7 @@ module ActiveStorage::Blob::Representable
|
|
|
89
155
|
when variable?
|
|
90
156
|
variant transformations
|
|
91
157
|
else
|
|
92
|
-
raise ActiveStorage::UnrepresentableError
|
|
158
|
+
raise ActiveStorage::UnrepresentableError, "No previewer found and can't transform blob with ID=#{id} and content_type=#{content_type}"
|
|
93
159
|
end
|
|
94
160
|
end
|
|
95
161
|
|
|
@@ -29,7 +29,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
29
29
|
# :method:
|
|
30
30
|
#
|
|
31
31
|
# Returns the associated ActiveStorage::Attachment instances.
|
|
32
|
-
has_many :attachments
|
|
32
|
+
has_many :attachments, autosave: false
|
|
33
33
|
|
|
34
34
|
##
|
|
35
35
|
# :singleton-method:
|
|
@@ -71,9 +71,8 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# Works like +find_signed+, but will raise an +ActiveSupport::MessageVerifier::InvalidSignature+
|
|
74
|
-
# exception if the +signed_id+ has either expired, has a purpose mismatch,
|
|
75
|
-
#
|
|
76
|
-
# the valid signed id can't find a record.
|
|
74
|
+
# exception if the +signed_id+ has either expired, has a purpose mismatch, or has been tampered with.
|
|
75
|
+
# It will also raise an +ActiveRecord::RecordNotFound+ exception if the valid signed id can't find a record.
|
|
77
76
|
def find_signed!(id, record: nil, purpose: :blob_id)
|
|
78
77
|
super(id, purpose: purpose)
|
|
79
78
|
end
|
|
@@ -152,22 +151,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
152
151
|
combined_blob.save!
|
|
153
152
|
end
|
|
154
153
|
end
|
|
155
|
-
|
|
156
|
-
def validate_service_configuration(service_name, model_class, association_name) # :nodoc:
|
|
157
|
-
if service_name
|
|
158
|
-
services.fetch(service_name) do
|
|
159
|
-
raise ArgumentError, "Cannot configure service #{service_name.inspect} for #{model_class}##{association_name}"
|
|
160
|
-
end
|
|
161
|
-
else
|
|
162
|
-
validate_global_service_configuration
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def validate_global_service_configuration # :nodoc:
|
|
167
|
-
if connected? && table_exists? && Rails.configuration.active_storage.service.nil?
|
|
168
|
-
raise RuntimeError, "Missing Active Storage service name. Specify Active Storage service name for config.active_storage.service in config/environments/#{Rails.env}.rb"
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
154
|
end
|
|
172
155
|
|
|
173
156
|
include Analyzable
|
|
@@ -57,13 +57,14 @@ class ActiveStorage::Filename
|
|
|
57
57
|
#
|
|
58
58
|
# Characters considered unsafe for storage (e.g. \, $, and the RTL override character) are replaced with a dash.
|
|
59
59
|
def sanitized
|
|
60
|
-
@filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}
|
|
60
|
+
@filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/<>?*\"\t\r\n\\", "-")
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
# Returns the sanitized version of the filename.
|
|
64
64
|
def to_s
|
|
65
65
|
sanitized.to_s
|
|
66
66
|
end
|
|
67
|
+
alias_method :to_str, :to_s
|
|
67
68
|
|
|
68
69
|
def as_json(*)
|
|
69
70
|
to_s
|