activestorage 7.0.0.alpha2 → 7.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activestorage might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -0
- data/app/assets/javascripts/activestorage.esm.js +17 -5
- data/app/assets/javascripts/activestorage.js +17 -5
- data/app/controllers/active_storage/blobs/proxy_controller.rb +2 -2
- data/app/controllers/active_storage/blobs/redirect_controller.rb +1 -1
- data/app/controllers/active_storage/direct_uploads_controller.rb +7 -1
- data/app/controllers/active_storage/disk_controller.rb +1 -0
- data/app/controllers/active_storage/representations/proxy_controller.rb +2 -2
- data/app/controllers/active_storage/representations/redirect_controller.rb +1 -1
- data/app/javascript/activestorage/blob_record.js +10 -3
- data/app/javascript/activestorage/direct_upload.js +4 -2
- data/app/javascript/activestorage/direct_upload_controller.js +9 -1
- data/app/models/active_storage/blob.rb +66 -9
- data/db/migrate/20170806125915_create_active_storage_tables.rb +3 -3
- data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +1 -1
- data/db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb +5 -0
- data/lib/active_storage/attached/model.rb +4 -2
- data/lib/active_storage/direct_upload_token.rb +59 -0
- data/lib/active_storage/downloader.rb +2 -2
- data/lib/active_storage/engine.rb +14 -0
- data/lib/active_storage/errors.rb +3 -0
- data/lib/active_storage/gem_version.rb +1 -1
- data/lib/active_storage/service/azure_storage_service.rb +27 -5
- data/lib/active_storage/service/disk_service.rb +11 -1
- data/lib/active_storage/service/gcs_service.rb +21 -7
- data/lib/active_storage/service/mirror_service.rb +1 -1
- data/lib/active_storage/service/s3_service.rb +33 -11
- data/lib/active_storage/service.rb +10 -2
- data/lib/active_storage.rb +3 -0
- metadata +21 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 85a5cf62726ae4b98295bb1b62926f4f2fa15257babdb8d6251cff37c682c354
|
4
|
+
data.tar.gz: 65f662b546c7f3e5eaf0775f065174f6d7162721578d763213eefd2875565a7b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3ff136a959f672bb822be34f573209a3017744a8ba476e8f6e2ec3de11130b9fb1bf4e876ee2b0233b1a2bfa9acd66d211b2b9b329f533b9449fd8d084482ec6
|
7
|
+
data.tar.gz: 1d38c4402a276a86847c492c1d373fab6ace21b6149e03426744561a1528307947f4353645b53db5bdf32cfa4776aa0c15cfd0591631bdc554be66b481f5d77e
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,49 @@
|
|
1
|
+
## Rails 7.0.0 (December 15, 2021) ##
|
2
|
+
|
3
|
+
* Support transforming empty-ish `has_many_attached` value into `[]` (e.g. `[""]`).
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
@user.highlights = [""]
|
7
|
+
@user.highlights # => []
|
8
|
+
```
|
9
|
+
|
10
|
+
*Sean Doyle*
|
11
|
+
|
12
|
+
|
13
|
+
## Rails 7.0.0.rc3 (December 14, 2021) ##
|
14
|
+
|
15
|
+
* No changes.
|
16
|
+
|
17
|
+
|
18
|
+
## Rails 7.0.0.rc2 (December 14, 2021) ##
|
19
|
+
|
20
|
+
* No changes.
|
21
|
+
|
22
|
+
## Rails 7.0.0.rc1 (December 06, 2021) ##
|
23
|
+
|
24
|
+
* `Add ActiveStorage::Blob.compose` to concatenate multiple blobs.
|
25
|
+
|
26
|
+
*Gannon McGibbon*
|
27
|
+
|
28
|
+
* Setting custom metadata on blobs are now persisted to remote storage.
|
29
|
+
|
30
|
+
*joshuamsager*
|
31
|
+
|
32
|
+
* Support direct uploads to multiple services.
|
33
|
+
|
34
|
+
*Dmitry Tsepelev*
|
35
|
+
|
36
|
+
* Invalid default content types are deprecated
|
37
|
+
|
38
|
+
Blobs created with content_type `image/jpg`, `image/pjpeg`, `image/bmp`, `text/javascript` will now produce
|
39
|
+
a deprecation warning, since these are not valid content types.
|
40
|
+
|
41
|
+
These content types will be removed from the defaults in Rails 7.1.
|
42
|
+
|
43
|
+
You can set `config.active_storage.silence_invalid_content_types_warning = true` to dismiss the warning.
|
44
|
+
|
45
|
+
*Alex Ghiculescu*
|
46
|
+
|
1
47
|
## Rails 7.0.0.alpha2 (September 15, 2021) ##
|
2
48
|
|
3
49
|
* No changes.
|
@@ -508,7 +508,7 @@ function toArray(value) {
|
|
508
508
|
}
|
509
509
|
|
510
510
|
class BlobRecord {
|
511
|
-
constructor(file, checksum, url) {
|
511
|
+
constructor(file, checksum, url, directUploadToken, attachmentName) {
|
512
512
|
this.file = file;
|
513
513
|
this.attributes = {
|
514
514
|
filename: file.name,
|
@@ -516,6 +516,8 @@ class BlobRecord {
|
|
516
516
|
byte_size: file.size,
|
517
517
|
checksum: checksum
|
518
518
|
};
|
519
|
+
this.directUploadToken = directUploadToken;
|
520
|
+
this.attachmentName = attachmentName;
|
519
521
|
this.xhr = new XMLHttpRequest;
|
520
522
|
this.xhr.open("POST", url, true);
|
521
523
|
this.xhr.responseType = "json";
|
@@ -543,7 +545,9 @@ class BlobRecord {
|
|
543
545
|
create(callback) {
|
544
546
|
this.callback = callback;
|
545
547
|
this.xhr.send(JSON.stringify({
|
546
|
-
blob: this.attributes
|
548
|
+
blob: this.attributes,
|
549
|
+
direct_upload_token: this.directUploadToken,
|
550
|
+
attachment_name: this.attachmentName
|
547
551
|
}));
|
548
552
|
}
|
549
553
|
requestDidLoad(event) {
|
@@ -604,10 +608,12 @@ class BlobUpload {
|
|
604
608
|
let id = 0;
|
605
609
|
|
606
610
|
class DirectUpload {
|
607
|
-
constructor(file, url, delegate) {
|
611
|
+
constructor(file, url, serviceName, attachmentName, delegate) {
|
608
612
|
this.id = ++id;
|
609
613
|
this.file = file;
|
610
614
|
this.url = url;
|
615
|
+
this.serviceName = serviceName;
|
616
|
+
this.attachmentName = attachmentName;
|
611
617
|
this.delegate = delegate;
|
612
618
|
}
|
613
619
|
create(callback) {
|
@@ -616,7 +622,7 @@ class DirectUpload {
|
|
616
622
|
callback(error);
|
617
623
|
return;
|
618
624
|
}
|
619
|
-
const blob = new BlobRecord(this.file, checksum, this.url);
|
625
|
+
const blob = new BlobRecord(this.file, checksum, this.url, this.serviceName, this.attachmentName);
|
620
626
|
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
|
621
627
|
blob.create((error => {
|
622
628
|
if (error) {
|
@@ -647,7 +653,7 @@ class DirectUploadController {
|
|
647
653
|
constructor(input, file) {
|
648
654
|
this.input = input;
|
649
655
|
this.file = file;
|
650
|
-
this.directUpload = new DirectUpload(this.file, this.url, this);
|
656
|
+
this.directUpload = new DirectUpload(this.file, this.url, this.directUploadToken, this.attachmentName, this);
|
651
657
|
this.dispatch("initialize");
|
652
658
|
}
|
653
659
|
start(callback) {
|
@@ -678,6 +684,12 @@ class DirectUploadController {
|
|
678
684
|
get url() {
|
679
685
|
return this.input.getAttribute("data-direct-upload-url");
|
680
686
|
}
|
687
|
+
get directUploadToken() {
|
688
|
+
return this.input.getAttribute("data-direct-upload-token");
|
689
|
+
}
|
690
|
+
get attachmentName() {
|
691
|
+
return this.input.getAttribute("data-direct-upload-attachment-name");
|
692
|
+
}
|
681
693
|
dispatch(name, detail = {}) {
|
682
694
|
detail.file = this.file;
|
683
695
|
detail.id = this.directUpload.id;
|
@@ -503,7 +503,7 @@
|
|
503
503
|
}
|
504
504
|
}
|
505
505
|
class BlobRecord {
|
506
|
-
constructor(file, checksum, url) {
|
506
|
+
constructor(file, checksum, url, directUploadToken, attachmentName) {
|
507
507
|
this.file = file;
|
508
508
|
this.attributes = {
|
509
509
|
filename: file.name,
|
@@ -511,6 +511,8 @@
|
|
511
511
|
byte_size: file.size,
|
512
512
|
checksum: checksum
|
513
513
|
};
|
514
|
+
this.directUploadToken = directUploadToken;
|
515
|
+
this.attachmentName = attachmentName;
|
514
516
|
this.xhr = new XMLHttpRequest;
|
515
517
|
this.xhr.open("POST", url, true);
|
516
518
|
this.xhr.responseType = "json";
|
@@ -538,7 +540,9 @@
|
|
538
540
|
create(callback) {
|
539
541
|
this.callback = callback;
|
540
542
|
this.xhr.send(JSON.stringify({
|
541
|
-
blob: this.attributes
|
543
|
+
blob: this.attributes,
|
544
|
+
direct_upload_token: this.directUploadToken,
|
545
|
+
attachment_name: this.attachmentName
|
542
546
|
}));
|
543
547
|
}
|
544
548
|
requestDidLoad(event) {
|
@@ -596,10 +600,12 @@
|
|
596
600
|
}
|
597
601
|
let id = 0;
|
598
602
|
class DirectUpload {
|
599
|
-
constructor(file, url, delegate) {
|
603
|
+
constructor(file, url, serviceName, attachmentName, delegate) {
|
600
604
|
this.id = ++id;
|
601
605
|
this.file = file;
|
602
606
|
this.url = url;
|
607
|
+
this.serviceName = serviceName;
|
608
|
+
this.attachmentName = attachmentName;
|
603
609
|
this.delegate = delegate;
|
604
610
|
}
|
605
611
|
create(callback) {
|
@@ -608,7 +614,7 @@
|
|
608
614
|
callback(error);
|
609
615
|
return;
|
610
616
|
}
|
611
|
-
const blob = new BlobRecord(this.file, checksum, this.url);
|
617
|
+
const blob = new BlobRecord(this.file, checksum, this.url, this.serviceName, this.attachmentName);
|
612
618
|
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
|
613
619
|
blob.create((error => {
|
614
620
|
if (error) {
|
@@ -637,7 +643,7 @@
|
|
637
643
|
constructor(input, file) {
|
638
644
|
this.input = input;
|
639
645
|
this.file = file;
|
640
|
-
this.directUpload = new DirectUpload(this.file, this.url, this);
|
646
|
+
this.directUpload = new DirectUpload(this.file, this.url, this.directUploadToken, this.attachmentName, this);
|
641
647
|
this.dispatch("initialize");
|
642
648
|
}
|
643
649
|
start(callback) {
|
@@ -668,6 +674,12 @@
|
|
668
674
|
get url() {
|
669
675
|
return this.input.getAttribute("data-direct-upload-url");
|
670
676
|
}
|
677
|
+
get directUploadToken() {
|
678
|
+
return this.input.getAttribute("data-direct-upload-token");
|
679
|
+
}
|
680
|
+
get attachmentName() {
|
681
|
+
return this.input.getAttribute("data-direct-upload-attachment-name");
|
682
|
+
}
|
671
683
|
dispatch(name, detail = {}) {
|
672
684
|
detail.file = this.file;
|
673
685
|
detail.id = this.directUpload.id;
|
@@ -5,7 +5,7 @@
|
|
5
5
|
# WARNING: All Active Storage controllers are publicly accessible by default. The
|
6
6
|
# generated URLs are hard to guess, but permanent by design. If your files
|
7
7
|
# require a higher level of protection consider implementing
|
8
|
-
# {Authenticated Controllers}[https://
|
8
|
+
# {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
|
9
9
|
class ActiveStorage::Blobs::ProxyController < ActiveStorage::BaseController
|
10
10
|
include ActiveStorage::SetBlob
|
11
11
|
|
@@ -17,7 +17,7 @@ class ActiveStorage::Blobs::ProxyController < ActiveStorage::BaseController
|
|
17
17
|
response.headers["Accept-Ranges"] = "bytes"
|
18
18
|
response.headers["Content-Length"] = @blob.byte_size.to_s
|
19
19
|
|
20
|
-
send_blob_stream @blob
|
20
|
+
send_blob_stream @blob, disposition: params[:disposition]
|
21
21
|
end
|
22
22
|
end
|
23
23
|
end
|
@@ -5,7 +5,7 @@
|
|
5
5
|
# WARNING: All Active Storage controllers are publicly accessible by default. The
|
6
6
|
# generated URLs are hard to guess, but permanent by design. If your files
|
7
7
|
# require a higher level of protection consider implementing
|
8
|
-
# {Authenticated Controllers}[https://
|
8
|
+
# {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
|
9
9
|
class ActiveStorage::Blobs::RedirectController < ActiveStorage::BaseController
|
10
10
|
include ActiveStorage::SetBlob
|
11
11
|
|
@@ -4,8 +4,10 @@
|
|
4
4
|
# When the client-side upload is completed, the signed_blob_id can be submitted as part of the form to reference
|
5
5
|
# the blob that was created up front.
|
6
6
|
class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
|
7
|
+
include ActiveStorage::DirectUploadToken
|
8
|
+
|
7
9
|
def create
|
8
|
-
blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_args)
|
10
|
+
blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_args.merge(service_name: verified_service_name))
|
9
11
|
render json: direct_upload_json(blob)
|
10
12
|
end
|
11
13
|
|
@@ -14,6 +16,10 @@ class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
|
|
14
16
|
params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, metadata: {}).to_h.symbolize_keys
|
15
17
|
end
|
16
18
|
|
19
|
+
def verified_service_name
|
20
|
+
ActiveStorage::DirectUploadToken.verify_direct_upload_token(params[:direct_upload_token], params[:attachment_name], session)
|
21
|
+
end
|
22
|
+
|
17
23
|
def direct_upload_json(blob)
|
18
24
|
blob.as_json(root: false, methods: :signed_id).merge(direct_upload: {
|
19
25
|
url: blob.service_url_for_direct_upload,
|
@@ -23,6 +23,7 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
|
|
23
23
|
if token = decode_verified_token
|
24
24
|
if acceptable_content?(token)
|
25
25
|
named_disk_service(token[:service_name]).upload token[:key], request.body, checksum: token[:checksum]
|
26
|
+
head :no_content
|
26
27
|
else
|
27
28
|
head :unprocessable_entity
|
28
29
|
end
|
@@ -5,11 +5,11 @@
|
|
5
5
|
# WARNING: All Active Storage controllers are publicly accessible by default. The
|
6
6
|
# generated URLs are hard to guess, but permanent by design. If your files
|
7
7
|
# require a higher level of protection consider implementing
|
8
|
-
# {Authenticated Controllers}[https://
|
8
|
+
# {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
|
9
9
|
class ActiveStorage::Representations::ProxyController < ActiveStorage::Representations::BaseController
|
10
10
|
def show
|
11
11
|
http_cache_forever public: true do
|
12
|
-
send_blob_stream @representation.image
|
12
|
+
send_blob_stream @representation.image, disposition: params[:disposition]
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
@@ -5,7 +5,7 @@
|
|
5
5
|
# WARNING: All Active Storage controllers are publicly accessible by default. The
|
6
6
|
# generated URLs are hard to guess, but permanent by design. If your files
|
7
7
|
# require a higher level of protection consider implementing
|
8
|
-
# {Authenticated Controllers}[https://
|
8
|
+
# {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
|
9
9
|
class ActiveStorage::Representations::RedirectController < ActiveStorage::Representations::BaseController
|
10
10
|
def show
|
11
11
|
expires_in ActiveStorage.service_urls_expire_in
|
@@ -1,16 +1,19 @@
|
|
1
1
|
import { getMetaValue } from "./helpers"
|
2
2
|
|
3
3
|
export class BlobRecord {
|
4
|
-
constructor(file, checksum, url) {
|
4
|
+
constructor(file, checksum, url, directUploadToken, attachmentName) {
|
5
5
|
this.file = file
|
6
6
|
|
7
7
|
this.attributes = {
|
8
8
|
filename: file.name,
|
9
9
|
content_type: file.type || "application/octet-stream",
|
10
10
|
byte_size: file.size,
|
11
|
-
checksum: checksum
|
11
|
+
checksum: checksum,
|
12
12
|
}
|
13
13
|
|
14
|
+
this.directUploadToken = directUploadToken
|
15
|
+
this.attachmentName = attachmentName
|
16
|
+
|
14
17
|
this.xhr = new XMLHttpRequest
|
15
18
|
this.xhr.open("POST", url, true)
|
16
19
|
this.xhr.responseType = "json"
|
@@ -43,7 +46,11 @@ export class BlobRecord {
|
|
43
46
|
|
44
47
|
create(callback) {
|
45
48
|
this.callback = callback
|
46
|
-
this.xhr.send(JSON.stringify({
|
49
|
+
this.xhr.send(JSON.stringify({
|
50
|
+
blob: this.attributes,
|
51
|
+
direct_upload_token: this.directUploadToken,
|
52
|
+
attachment_name: this.attachmentName
|
53
|
+
}))
|
47
54
|
}
|
48
55
|
|
49
56
|
requestDidLoad(event) {
|
@@ -5,10 +5,12 @@ import { BlobUpload } from "./blob_upload"
|
|
5
5
|
let id = 0
|
6
6
|
|
7
7
|
export class DirectUpload {
|
8
|
-
constructor(file, url, delegate) {
|
8
|
+
constructor(file, url, serviceName, attachmentName, delegate) {
|
9
9
|
this.id = ++id
|
10
10
|
this.file = file
|
11
11
|
this.url = url
|
12
|
+
this.serviceName = serviceName
|
13
|
+
this.attachmentName = attachmentName
|
12
14
|
this.delegate = delegate
|
13
15
|
}
|
14
16
|
|
@@ -19,7 +21,7 @@ export class DirectUpload {
|
|
19
21
|
return
|
20
22
|
}
|
21
23
|
|
22
|
-
const blob = new BlobRecord(this.file, checksum, this.url)
|
24
|
+
const blob = new BlobRecord(this.file, checksum, this.url, this.serviceName, this.attachmentName)
|
23
25
|
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr)
|
24
26
|
|
25
27
|
blob.create(error => {
|
@@ -5,7 +5,7 @@ export class DirectUploadController {
|
|
5
5
|
constructor(input, file) {
|
6
6
|
this.input = input
|
7
7
|
this.file = file
|
8
|
-
this.directUpload = new DirectUpload(this.file, this.url, this)
|
8
|
+
this.directUpload = new DirectUpload(this.file, this.url, this.directUploadToken, this.attachmentName, this)
|
9
9
|
this.dispatch("initialize")
|
10
10
|
}
|
11
11
|
|
@@ -41,6 +41,14 @@ export class DirectUploadController {
|
|
41
41
|
return this.input.getAttribute("data-direct-upload-url")
|
42
42
|
}
|
43
43
|
|
44
|
+
get directUploadToken() {
|
45
|
+
return this.input.getAttribute("data-direct-upload-token")
|
46
|
+
}
|
47
|
+
|
48
|
+
get attachmentName() {
|
49
|
+
return this.input.getAttribute("data-direct-upload-attachment-name")
|
50
|
+
}
|
51
|
+
|
44
52
|
dispatch(name, detail = {}) {
|
45
53
|
detail.file = this.file
|
46
54
|
detail.id = this.directUpload.id
|
@@ -39,7 +39,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
39
39
|
MINIMUM_TOKEN_LENGTH = 28
|
40
40
|
|
41
41
|
has_secure_token :key, length: MINIMUM_TOKEN_LENGTH
|
42
|
-
store :metadata, accessors: [ :analyzed, :identified ], coder: ActiveRecord::Coders::JSON
|
42
|
+
store :metadata, accessors: [ :analyzed, :identified, :composed ], coder: ActiveRecord::Coders::JSON
|
43
43
|
|
44
44
|
class_attribute :services, default: {}
|
45
45
|
class_attribute :service, instance_accessor: false
|
@@ -52,13 +52,14 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
52
52
|
self.service_name ||= self.class.service&.name
|
53
53
|
end
|
54
54
|
|
55
|
-
after_update_commit :update_service_metadata, if:
|
55
|
+
after_update_commit :update_service_metadata, if: -> { content_type_previously_changed? || metadata_previously_changed? }
|
56
56
|
|
57
57
|
before_destroy(prepend: true) do
|
58
58
|
raise ActiveRecord::InvalidForeignKey if attachments.exists?
|
59
59
|
end
|
60
60
|
|
61
61
|
validates :service_name, presence: true
|
62
|
+
validates :checksum, presence: true, unless: :composed
|
62
63
|
|
63
64
|
validate do
|
64
65
|
if service_name_changed? && service_name.present?
|
@@ -145,6 +146,18 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
145
146
|
all
|
146
147
|
end
|
147
148
|
end
|
149
|
+
|
150
|
+
# Concatenate multiple blobs into a single "composed" blob.
|
151
|
+
def compose(blobs, filename:, content_type: nil, metadata: nil)
|
152
|
+
raise ActiveRecord::RecordNotSaved, "All blobs must be persisted." if blobs.any?(&:new_record?)
|
153
|
+
|
154
|
+
content_type ||= blobs.pluck(:content_type).compact.first
|
155
|
+
|
156
|
+
new(filename: filename, content_type: content_type, metadata: metadata, byte_size: blobs.sum(&:byte_size)).tap do |combined_blob|
|
157
|
+
combined_blob.compose(blobs.pluck(:key))
|
158
|
+
combined_blob.save!
|
159
|
+
end
|
160
|
+
end
|
148
161
|
end
|
149
162
|
|
150
163
|
# Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering.
|
@@ -168,6 +181,14 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
168
181
|
ActiveStorage::Filename.new(self[:filename])
|
169
182
|
end
|
170
183
|
|
184
|
+
def custom_metadata
|
185
|
+
self[:metadata][:custom] || {}
|
186
|
+
end
|
187
|
+
|
188
|
+
def custom_metadata=(metadata)
|
189
|
+
self[:metadata] = self[:metadata].merge(custom: metadata)
|
190
|
+
end
|
191
|
+
|
171
192
|
# Returns true if the content_type of this blob is in the image range, like image/png.
|
172
193
|
def image?
|
173
194
|
content_type.start_with?("image")
|
@@ -200,12 +221,12 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
200
221
|
# Returns a URL that can be used to directly upload a file for this blob on the service. This URL is intended to be
|
201
222
|
# short-lived for security and only generated on-demand by the client-side JavaScript responsible for doing the uploading.
|
202
223
|
def service_url_for_direct_upload(expires_in: ActiveStorage.service_urls_expire_in)
|
203
|
-
service.url_for_direct_upload key, expires_in: expires_in, content_type: content_type, content_length: byte_size, checksum: checksum
|
224
|
+
service.url_for_direct_upload key, expires_in: expires_in, content_type: content_type, content_length: byte_size, checksum: checksum, custom_metadata: custom_metadata
|
204
225
|
end
|
205
226
|
|
206
227
|
# Returns a Hash of headers for +service_url_for_direct_upload+ requests.
|
207
228
|
def service_headers_for_direct_upload
|
208
|
-
service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum
|
229
|
+
service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum, custom_metadata: custom_metadata
|
209
230
|
end
|
210
231
|
|
211
232
|
def content_type_for_serving # :nodoc:
|
@@ -247,6 +268,11 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
247
268
|
service.upload key, io, checksum: checksum, **service_metadata
|
248
269
|
end
|
249
270
|
|
271
|
+
def compose(keys) # :nodoc:
|
272
|
+
self.composed = true
|
273
|
+
service.compose(keys, key, **service_metadata)
|
274
|
+
end
|
275
|
+
|
250
276
|
# Downloads the file associated with this blob. If no block is given, the entire file is read into memory and returned.
|
251
277
|
# That'll use a lot of RAM for very large files. If a block is given, then the download is streamed and yielded in chunks.
|
252
278
|
def download(&block)
|
@@ -272,8 +298,14 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
272
298
|
#
|
273
299
|
# Raises ActiveStorage::IntegrityError if the downloaded data does not match the blob's checksum.
|
274
300
|
def open(tmpdir: nil, &block)
|
275
|
-
service.open
|
276
|
-
|
301
|
+
service.open(
|
302
|
+
key,
|
303
|
+
checksum: checksum,
|
304
|
+
verify: !composed,
|
305
|
+
name: [ "ActiveStorage-#{id}-", filename.extension_with_delimiter ],
|
306
|
+
tmpdir: tmpdir,
|
307
|
+
&block
|
308
|
+
)
|
277
309
|
end
|
278
310
|
|
279
311
|
def mirror_later # :nodoc:
|
@@ -308,6 +340,31 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
308
340
|
services.fetch(service_name)
|
309
341
|
end
|
310
342
|
|
343
|
+
def content_type=(value)
|
344
|
+
unless ActiveStorage.silence_invalid_content_types_warning
|
345
|
+
if INVALID_VARIABLE_CONTENT_TYPES_DEPRECATED_IN_RAILS_7.include?(value)
|
346
|
+
ActiveSupport::Deprecation.warn(<<-MSG.squish)
|
347
|
+
#{value} is not a valid content type, it should not be used when creating a blob, and support for it will be removed in Rails 7.1.
|
348
|
+
If you want to keep supporting this content type past Rails 7.1, add it to `config.active_storage.variable_content_types`.
|
349
|
+
Dismiss this warning by setting `config.active_storage.silence_invalid_content_types_warning = true`.
|
350
|
+
MSG
|
351
|
+
end
|
352
|
+
|
353
|
+
if INVALID_VARIABLE_CONTENT_TYPES_TO_SERVE_AS_BINARY_DEPRECATED_IN_RAILS_7.include?(value)
|
354
|
+
ActiveSupport::Deprecation.warn(<<-MSG.squish)
|
355
|
+
#{value} is not a valid content type, it should not be used when creating a blob, and support for it will be removed in Rails 7.1.
|
356
|
+
If you want to keep supporting this content type past Rails 7.1, add it to `config.active_storage.content_types_to_serve_as_binary`.
|
357
|
+
Dismiss this warning by setting `config.active_storage.silence_invalid_content_types_warning = true`.
|
358
|
+
MSG
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
super
|
363
|
+
end
|
364
|
+
|
365
|
+
INVALID_VARIABLE_CONTENT_TYPES_DEPRECATED_IN_RAILS_7 = ["image/jpg", "image/pjpeg", "image/bmp"]
|
366
|
+
INVALID_VARIABLE_CONTENT_TYPES_TO_SERVE_AS_BINARY_DEPRECATED_IN_RAILS_7 = ["text/javascript"]
|
367
|
+
|
311
368
|
private
|
312
369
|
def compute_checksum_in_chunks(io)
|
313
370
|
OpenSSL::Digest::MD5.new.tap do |checksum|
|
@@ -337,11 +394,11 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
337
394
|
|
338
395
|
def service_metadata
|
339
396
|
if forcibly_serve_as_binary?
|
340
|
-
{ content_type: ActiveStorage.binary_content_type, disposition: :attachment, filename: filename }
|
397
|
+
{ content_type: ActiveStorage.binary_content_type, disposition: :attachment, filename: filename, custom_metadata: custom_metadata }
|
341
398
|
elsif !allowed_inline?
|
342
|
-
{ content_type: content_type, disposition: :attachment, filename: filename }
|
399
|
+
{ content_type: content_type, disposition: :attachment, filename: filename, custom_metadata: custom_metadata }
|
343
400
|
else
|
344
|
-
{ content_type: content_type }
|
401
|
+
{ content_type: content_type, custom_metadata: custom_metadata }
|
345
402
|
end
|
346
403
|
end
|
347
404
|
|
@@ -10,7 +10,7 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
|
|
10
10
|
t.text :metadata
|
11
11
|
t.string :service_name, null: false
|
12
12
|
t.bigint :byte_size, null: false
|
13
|
-
t.string :checksum
|
13
|
+
t.string :checksum
|
14
14
|
|
15
15
|
if connection.supports_datetime_with_precision?
|
16
16
|
t.datetime :created_at, precision: 6, null: false
|
@@ -32,7 +32,7 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
|
|
32
32
|
t.datetime :created_at, null: false
|
33
33
|
end
|
34
34
|
|
35
|
-
t.index [ :record_type, :record_id, :name, :blob_id ], name:
|
35
|
+
t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
|
36
36
|
t.foreign_key :active_storage_blobs, column: :blob_id
|
37
37
|
end
|
38
38
|
|
@@ -40,7 +40,7 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
|
|
40
40
|
t.belongs_to :blob, null: false, index: false, type: foreign_key_type
|
41
41
|
t.string :variation_digest, null: false
|
42
42
|
|
43
|
-
t.index
|
43
|
+
t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
|
44
44
|
t.foreign_key :active_storage_blobs, column: :blob_id
|
45
45
|
end
|
46
46
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
|
2
2
|
def change
|
3
3
|
# Use Active Record's configured type for primary key
|
4
|
-
create_table :active_storage_variant_records, id: primary_key_type do |t|
|
4
|
+
create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t|
|
5
5
|
t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type
|
6
6
|
t.string :variation_digest, null: false
|
7
7
|
|
@@ -137,9 +137,11 @@ module ActiveStorage
|
|
137
137
|
end
|
138
138
|
|
139
139
|
def #{name}=(attachables)
|
140
|
+
attachables = Array(attachables).compact_blank
|
141
|
+
|
140
142
|
if ActiveStorage.replace_on_assign_to_many
|
141
143
|
attachment_changes["#{name}"] =
|
142
|
-
if
|
144
|
+
if attachables.none?
|
143
145
|
ActiveStorage::Attached::Changes::DeleteMany.new("#{name}", self)
|
144
146
|
else
|
145
147
|
ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, attachables)
|
@@ -151,7 +153,7 @@ module ActiveStorage
|
|
151
153
|
"To append new attachables to the Active Storage association, prefer using `attach`. " \
|
152
154
|
"Using association setter would result in purging the existing attached attachments and replacing them with new ones."
|
153
155
|
|
154
|
-
if
|
156
|
+
if attachables.any?
|
155
157
|
attachment_changes["#{name}"] =
|
156
158
|
ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, #{name}.blobs + attachables)
|
157
159
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage
|
4
|
+
module DirectUploadToken
|
5
|
+
extend self
|
6
|
+
|
7
|
+
SEPARATOR = "."
|
8
|
+
DIRECT_UPLOAD_TOKEN_LENGTH = 32
|
9
|
+
|
10
|
+
def generate_direct_upload_token(attachment_name, service_name, session)
|
11
|
+
token = direct_upload_token(session, attachment_name)
|
12
|
+
encode_direct_upload_token([service_name, token].join(SEPARATOR))
|
13
|
+
end
|
14
|
+
|
15
|
+
def verify_direct_upload_token(token, attachment_name, session)
|
16
|
+
raise ActiveStorage::InvalidDirectUploadTokenError if token.nil?
|
17
|
+
|
18
|
+
service_name, *token_components = decode_token(token).split(SEPARATOR)
|
19
|
+
decoded_token = token_components.join(SEPARATOR)
|
20
|
+
|
21
|
+
return service_name if valid_direct_upload_token?(decoded_token, attachment_name, session)
|
22
|
+
|
23
|
+
raise ActiveStorage::InvalidDirectUploadTokenError
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def direct_upload_token(session, attachment_name) # :doc:
|
28
|
+
direct_upload_token_hmac(session, "direct_upload##{attachment_name}")
|
29
|
+
end
|
30
|
+
|
31
|
+
def valid_direct_upload_token?(token, attachment_name, session) # :doc:
|
32
|
+
correct_token = direct_upload_token(session, attachment_name)
|
33
|
+
ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, correct_token)
|
34
|
+
rescue ArgumentError
|
35
|
+
raise ActiveStorage::InvalidDirectUploadTokenError
|
36
|
+
end
|
37
|
+
|
38
|
+
def direct_upload_token_hmac(session, identifier) # :doc:
|
39
|
+
OpenSSL::HMAC.digest(
|
40
|
+
OpenSSL::Digest::SHA256.new,
|
41
|
+
real_direct_upload_token(session),
|
42
|
+
identifier
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def real_direct_upload_token(session) # :doc:
|
47
|
+
session[:_direct_upload_token] ||= SecureRandom.urlsafe_base64(DIRECT_UPLOAD_TOKEN_LENGTH, padding: false)
|
48
|
+
encode_direct_upload_token(session[:_direct_upload_token])
|
49
|
+
end
|
50
|
+
|
51
|
+
def decode_token(encoded_token) # :nodoc:
|
52
|
+
Base64.urlsafe_decode64(encoded_token)
|
53
|
+
end
|
54
|
+
|
55
|
+
def encode_direct_upload_token(raw_token) # :nodoc:
|
56
|
+
Base64.urlsafe_encode64(raw_token)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -8,10 +8,10 @@ module ActiveStorage
|
|
8
8
|
@service = service
|
9
9
|
end
|
10
10
|
|
11
|
-
def open(key, checksum
|
11
|
+
def open(key, checksum: nil, verify: true, name: "ActiveStorage-", tmpdir: nil)
|
12
12
|
open_tempfile(name, tmpdir) do |file|
|
13
13
|
download key, file
|
14
|
-
verify_integrity_of
|
14
|
+
verify_integrity_of(file, checksum: checksum) if verify
|
15
15
|
yield file
|
16
16
|
end
|
17
17
|
end
|
@@ -101,6 +101,8 @@ module ActiveStorage
|
|
101
101
|
ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream"
|
102
102
|
ActiveStorage.video_preview_arguments = app.config.active_storage.video_preview_arguments || "-y -vframes 1 -f image2"
|
103
103
|
|
104
|
+
ActiveStorage.silence_invalid_content_types_warning = app.config.active_storage.silence_invalid_content_types_warning || false
|
105
|
+
|
104
106
|
ActiveStorage.replace_on_assign_to_many = app.config.active_storage.replace_on_assign_to_many || false
|
105
107
|
ActiveStorage.track_variants = app.config.active_storage.track_variants || false
|
106
108
|
end
|
@@ -152,6 +154,18 @@ module ActiveStorage
|
|
152
154
|
end
|
153
155
|
end
|
154
156
|
|
157
|
+
initializer "action_view.configuration" do
|
158
|
+
config.after_initialize do |app|
|
159
|
+
ActiveSupport.on_load(:action_view) do
|
160
|
+
multiple_file_field_include_hidden = app.config.active_storage.delete(:multiple_file_field_include_hidden)
|
161
|
+
|
162
|
+
unless multiple_file_field_include_hidden.nil?
|
163
|
+
ActionView::Helpers::FormHelper.multiple_file_field_include_hidden = multiple_file_field_include_hidden
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
155
169
|
initializer "active_storage.asset" do
|
156
170
|
if Rails.application.config.respond_to?(:assets)
|
157
171
|
Rails.application.config.assets.precompile += %w( activestorage activestorage.esm )
|
@@ -19,12 +19,12 @@ module ActiveStorage
|
|
19
19
|
@public = public
|
20
20
|
end
|
21
21
|
|
22
|
-
def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, **)
|
22
|
+
def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, custom_metadata: {}, **)
|
23
23
|
instrument :upload, key: key, checksum: checksum do
|
24
24
|
handle_errors do
|
25
25
|
content_disposition = content_disposition_with(filename: filename, type: disposition) if disposition && filename
|
26
26
|
|
27
|
-
client.create_block_blob(container, key, IO.try_convert(io) || io, content_md5: checksum, content_type: content_type, content_disposition: content_disposition)
|
27
|
+
client.create_block_blob(container, key, IO.try_convert(io) || io, content_md5: checksum, content_type: content_type, content_disposition: content_disposition, metadata: custom_metadata)
|
28
28
|
end
|
29
29
|
end
|
30
30
|
end
|
@@ -86,7 +86,7 @@ module ActiveStorage
|
|
86
86
|
end
|
87
87
|
end
|
88
88
|
|
89
|
-
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
|
89
|
+
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, custom_metadata: {})
|
90
90
|
instrument :url, key: key do |payload|
|
91
91
|
generated_url = signer.signed_uri(
|
92
92
|
uri_for(key), false,
|
@@ -101,10 +101,28 @@ module ActiveStorage
|
|
101
101
|
end
|
102
102
|
end
|
103
103
|
|
104
|
-
def headers_for_direct_upload(key, content_type:, checksum:, filename: nil, disposition: nil, **)
|
104
|
+
def headers_for_direct_upload(key, content_type:, checksum:, filename: nil, disposition: nil, custom_metadata: {}, **)
|
105
105
|
content_disposition = content_disposition_with(type: disposition, filename: filename) if filename
|
106
106
|
|
107
|
-
{ "Content-Type" => content_type, "Content-MD5" => checksum, "x-ms-blob-content-disposition" => content_disposition, "x-ms-blob-type" => "BlockBlob" }
|
107
|
+
{ "Content-Type" => content_type, "Content-MD5" => checksum, "x-ms-blob-content-disposition" => content_disposition, "x-ms-blob-type" => "BlockBlob", **custom_metadata_headers(custom_metadata) }
|
108
|
+
end
|
109
|
+
|
110
|
+
def compose(source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
|
111
|
+
content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
|
112
|
+
|
113
|
+
client.create_append_blob(
|
114
|
+
container,
|
115
|
+
destination_key,
|
116
|
+
content_type: content_type,
|
117
|
+
content_disposition: content_disposition,
|
118
|
+
metadata: custom_metadata,
|
119
|
+
).tap do |blob|
|
120
|
+
source_keys.each do |source_key|
|
121
|
+
stream(source_key) do |chunk|
|
122
|
+
client.append_blob_block(container, blob.name, chunk)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
108
126
|
end
|
109
127
|
|
110
128
|
private
|
@@ -166,5 +184,9 @@ module ActiveStorage
|
|
166
184
|
raise
|
167
185
|
end
|
168
186
|
end
|
187
|
+
|
188
|
+
def custom_metadata_headers(metadata)
|
189
|
+
metadata.transform_keys { |key| "x-ms-meta-#{key}" }
|
190
|
+
end
|
169
191
|
end
|
170
192
|
end
|
@@ -72,7 +72,7 @@ module ActiveStorage
|
|
72
72
|
end
|
73
73
|
end
|
74
74
|
|
75
|
-
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
|
75
|
+
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, custom_metadata: {})
|
76
76
|
instrument :url, key: key do |payload|
|
77
77
|
verified_token_with_expiration = ActiveStorage.verifier.generate(
|
78
78
|
{
|
@@ -100,6 +100,16 @@ module ActiveStorage
|
|
100
100
|
File.join root, folder_for(key), key
|
101
101
|
end
|
102
102
|
|
103
|
+
def compose(source_keys, destination_key, **)
|
104
|
+
File.open(make_path_for(destination_key), "w") do |destination_file|
|
105
|
+
source_keys.each do |source_key|
|
106
|
+
File.open(path_for(source_key), "rb") do |source_file|
|
107
|
+
IO.copy_stream(source_file, destination_file)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
103
113
|
private
|
104
114
|
def private_url(key, expires_in:, filename:, content_type:, disposition:, **)
|
105
115
|
generate_url(key, expires_in: expires_in, filename: filename, content_type: content_type, disposition: disposition)
|
@@ -16,14 +16,14 @@ module ActiveStorage
|
|
16
16
|
@public = public
|
17
17
|
end
|
18
18
|
|
19
|
-
def upload(key, io, checksum: nil, content_type: nil, disposition: nil, filename: nil)
|
19
|
+
def upload(key, io, checksum: nil, content_type: nil, disposition: nil, filename: nil, custom_metadata: {})
|
20
20
|
instrument :upload, key: key, checksum: checksum do
|
21
21
|
# GCS's signed URLs don't include params such as response-content-type response-content_disposition
|
22
22
|
# in the signature, which means an attacker can modify them and bypass our effort to force these to
|
23
23
|
# binary and attachment when the file's content type requires it. The only way to force them is to
|
24
24
|
# store them as object's metadata.
|
25
25
|
content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
|
26
|
-
bucket.create_file(io, key, md5: checksum, cache_control: @config[:cache_control], content_type: content_type, content_disposition: content_disposition)
|
26
|
+
bucket.create_file(io, key, md5: checksum, cache_control: @config[:cache_control], content_type: content_type, content_disposition: content_disposition, metadata: custom_metadata)
|
27
27
|
rescue Google::Cloud::InvalidArgumentError
|
28
28
|
raise ActiveStorage::IntegrityError
|
29
29
|
end
|
@@ -43,11 +43,12 @@ module ActiveStorage
|
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
46
|
-
def update_metadata(key, content_type:, disposition: nil, filename: nil)
|
46
|
+
def update_metadata(key, content_type:, disposition: nil, filename: nil, custom_metadata: {})
|
47
47
|
instrument :update_metadata, key: key, content_type: content_type, disposition: disposition do
|
48
48
|
file_for(key).update do |file|
|
49
49
|
file.content_type = content_type
|
50
50
|
file.content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
|
51
|
+
file.metadata = custom_metadata
|
51
52
|
end
|
52
53
|
end
|
53
54
|
end
|
@@ -86,7 +87,7 @@ module ActiveStorage
|
|
86
87
|
end
|
87
88
|
end
|
88
89
|
|
89
|
-
def url_for_direct_upload(key, expires_in:, checksum:, **)
|
90
|
+
def url_for_direct_upload(key, expires_in:, checksum:, custom_metadata: {}, **)
|
90
91
|
instrument :url, key: key do |payload|
|
91
92
|
headers = {}
|
92
93
|
version = :v2
|
@@ -99,6 +100,8 @@ module ActiveStorage
|
|
99
100
|
version = :v4
|
100
101
|
end
|
101
102
|
|
103
|
+
headers.merge!(custom_metadata_headers(custom_metadata))
|
104
|
+
|
102
105
|
args = {
|
103
106
|
content_md5: checksum,
|
104
107
|
expires: expires_in,
|
@@ -120,11 +123,10 @@ module ActiveStorage
|
|
120
123
|
end
|
121
124
|
end
|
122
125
|
|
123
|
-
def headers_for_direct_upload(key, checksum:, filename: nil, disposition: nil, **)
|
126
|
+
def headers_for_direct_upload(key, checksum:, filename: nil, disposition: nil, custom_metadata: {}, **)
|
124
127
|
content_disposition = content_disposition_with(type: disposition, filename: filename) if filename
|
125
128
|
|
126
|
-
headers = { "Content-MD5" => checksum, "Content-Disposition" => content_disposition }
|
127
|
-
|
129
|
+
headers = { "Content-MD5" => checksum, "Content-Disposition" => content_disposition, **custom_metadata_headers(custom_metadata) }
|
128
130
|
if @config[:cache_control].present?
|
129
131
|
headers["Cache-Control"] = @config[:cache_control]
|
130
132
|
end
|
@@ -132,6 +134,14 @@ module ActiveStorage
|
|
132
134
|
headers
|
133
135
|
end
|
134
136
|
|
137
|
+
def compose(source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
|
138
|
+
bucket.compose(source_keys, destination_key).update do |file|
|
139
|
+
file.content_type = content_type
|
140
|
+
file.content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
|
141
|
+
file.metadata = custom_metadata
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
135
145
|
private
|
136
146
|
def private_url(key, expires_in:, filename:, content_type:, disposition:, **)
|
137
147
|
args = {
|
@@ -223,5 +233,9 @@ module ActiveStorage
|
|
223
233
|
response.signed_blob
|
224
234
|
end
|
225
235
|
end
|
236
|
+
|
237
|
+
def custom_metadata_headers(metadata)
|
238
|
+
metadata.transform_keys { |key| "x-goog-meta-#{key}" }
|
239
|
+
end
|
226
240
|
end
|
227
241
|
end
|
@@ -14,7 +14,7 @@ module ActiveStorage
|
|
14
14
|
attr_reader :primary, :mirrors
|
15
15
|
|
16
16
|
delegate :download, :download_chunk, :exist?, :url,
|
17
|
-
:url_for_direct_upload, :headers_for_direct_upload, :path_for, to: :primary
|
17
|
+
:url_for_direct_upload, :headers_for_direct_upload, :path_for, :compose, to: :primary
|
18
18
|
|
19
19
|
# Stitch together from named services.
|
20
20
|
def self.build(primary:, mirrors:, name:, configurator:, **options) # :nodoc:
|
@@ -23,14 +23,14 @@ module ActiveStorage
|
|
23
23
|
@upload_options[:acl] = "public-read" if public?
|
24
24
|
end
|
25
25
|
|
26
|
-
def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, **)
|
26
|
+
def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, custom_metadata: {}, **)
|
27
27
|
instrument :upload, key: key, checksum: checksum do
|
28
28
|
content_disposition = content_disposition_with(filename: filename, type: disposition) if disposition && filename
|
29
29
|
|
30
30
|
if io.size < multipart_upload_threshold
|
31
|
-
upload_with_single_part key, io, checksum: checksum, content_type: content_type, content_disposition: content_disposition
|
31
|
+
upload_with_single_part key, io, checksum: checksum, content_type: content_type, content_disposition: content_disposition, custom_metadata: custom_metadata
|
32
32
|
else
|
33
|
-
upload_with_multipart key, io, content_type: content_type, content_disposition: content_disposition
|
33
|
+
upload_with_multipart key, io, content_type: content_type, content_disposition: content_disposition, custom_metadata: custom_metadata
|
34
34
|
end
|
35
35
|
end
|
36
36
|
end
|
@@ -77,11 +77,11 @@ module ActiveStorage
|
|
77
77
|
end
|
78
78
|
end
|
79
79
|
|
80
|
-
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
|
80
|
+
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, custom_metadata: {})
|
81
81
|
instrument :url, key: key do |payload|
|
82
82
|
generated_url = object_for(key).presigned_url :put, expires_in: expires_in.to_i,
|
83
83
|
content_type: content_type, content_length: content_length, content_md5: checksum,
|
84
|
-
whitelist_headers: ["content-length"], **upload_options
|
84
|
+
metadata: custom_metadata, whitelist_headers: ["content-length"], **upload_options
|
85
85
|
|
86
86
|
payload[:url] = generated_url
|
87
87
|
|
@@ -89,10 +89,28 @@ module ActiveStorage
|
|
89
89
|
end
|
90
90
|
end
|
91
91
|
|
92
|
-
def headers_for_direct_upload(key, content_type:, checksum:, filename: nil, disposition: nil, **)
|
92
|
+
def headers_for_direct_upload(key, content_type:, checksum:, filename: nil, disposition: nil, custom_metadata: {}, **)
|
93
93
|
content_disposition = content_disposition_with(type: disposition, filename: filename) if filename
|
94
94
|
|
95
|
-
{ "Content-Type" => content_type, "Content-MD5" => checksum, "Content-Disposition" => content_disposition }
|
95
|
+
{ "Content-Type" => content_type, "Content-MD5" => checksum, "Content-Disposition" => content_disposition, **custom_metadata_headers(custom_metadata) }
|
96
|
+
end
|
97
|
+
|
98
|
+
def compose(source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
|
99
|
+
content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
|
100
|
+
|
101
|
+
object_for(destination_key).upload_stream(
|
102
|
+
content_type: content_type,
|
103
|
+
content_disposition: content_disposition,
|
104
|
+
part_size: MINIMUM_UPLOAD_PART_SIZE,
|
105
|
+
metadata: custom_metadata,
|
106
|
+
**upload_options
|
107
|
+
) do |out|
|
108
|
+
source_keys.each do |source_key|
|
109
|
+
stream(source_key) do |chunk|
|
110
|
+
IO.copy_stream(StringIO.new(chunk), out)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
96
114
|
end
|
97
115
|
|
98
116
|
private
|
@@ -110,16 +128,16 @@ module ActiveStorage
|
|
110
128
|
MAXIMUM_UPLOAD_PARTS_COUNT = 10000
|
111
129
|
MINIMUM_UPLOAD_PART_SIZE = 5.megabytes
|
112
130
|
|
113
|
-
def upload_with_single_part(key, io, checksum: nil, content_type: nil, content_disposition: nil)
|
114
|
-
object_for(key).put(body: io, content_md5: checksum, content_type: content_type, content_disposition: content_disposition, **upload_options)
|
131
|
+
def upload_with_single_part(key, io, checksum: nil, content_type: nil, content_disposition: nil, custom_metadata: {})
|
132
|
+
object_for(key).put(body: io, content_md5: checksum, content_type: content_type, content_disposition: content_disposition, metadata: custom_metadata, **upload_options)
|
115
133
|
rescue Aws::S3::Errors::BadDigest
|
116
134
|
raise ActiveStorage::IntegrityError
|
117
135
|
end
|
118
136
|
|
119
|
-
def upload_with_multipart(key, io, content_type: nil, content_disposition: nil)
|
137
|
+
def upload_with_multipart(key, io, content_type: nil, content_disposition: nil, custom_metadata: {})
|
120
138
|
part_size = [ io.size.fdiv(MAXIMUM_UPLOAD_PARTS_COUNT).ceil, MINIMUM_UPLOAD_PART_SIZE ].max
|
121
139
|
|
122
|
-
object_for(key).upload_stream(content_type: content_type, content_disposition: content_disposition, part_size: part_size, **upload_options) do |out|
|
140
|
+
object_for(key).upload_stream(content_type: content_type, content_disposition: content_disposition, part_size: part_size, metadata: custom_metadata, **upload_options) do |out|
|
123
141
|
IO.copy_stream(io, out)
|
124
142
|
end
|
125
143
|
end
|
@@ -143,5 +161,9 @@ module ActiveStorage
|
|
143
161
|
offset += chunk_size
|
144
162
|
end
|
145
163
|
end
|
164
|
+
|
165
|
+
def custom_metadata_headers(metadata)
|
166
|
+
metadata.transform_keys { |key| "x-amz-meta-#{key}" }
|
167
|
+
end
|
146
168
|
end
|
147
169
|
end
|
@@ -90,6 +90,11 @@ module ActiveStorage
|
|
90
90
|
ActiveStorage::Downloader.new(self).open(*args, **options, &block)
|
91
91
|
end
|
92
92
|
|
93
|
+
# Concatenate multiple files into a single "composed" file.
|
94
|
+
def compose(source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
|
95
|
+
raise NotImplementedError
|
96
|
+
end
|
97
|
+
|
93
98
|
# Delete the file at the +key+.
|
94
99
|
def delete(key)
|
95
100
|
raise NotImplementedError
|
@@ -128,12 +133,12 @@ module ActiveStorage
|
|
128
133
|
# The URL will be valid for the amount of seconds specified in +expires_in+.
|
129
134
|
# You must also provide the +content_type+, +content_length+, and +checksum+ of the file
|
130
135
|
# that will be uploaded. All these attributes will be validated by the service upon upload.
|
131
|
-
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
|
136
|
+
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, custom_metadata: {})
|
132
137
|
raise NotImplementedError
|
133
138
|
end
|
134
139
|
|
135
140
|
# Returns a Hash of headers for +url_for_direct_upload+ requests.
|
136
|
-
def headers_for_direct_upload(key, filename:, content_type:, content_length:, checksum:)
|
141
|
+
def headers_for_direct_upload(key, filename:, content_type:, content_length:, checksum:, custom_metadata: {})
|
137
142
|
{}
|
138
143
|
end
|
139
144
|
|
@@ -150,6 +155,9 @@ module ActiveStorage
|
|
150
155
|
raise NotImplementedError
|
151
156
|
end
|
152
157
|
|
158
|
+
def custom_metadata_headers(metadata)
|
159
|
+
raise NotImplementedError
|
160
|
+
end
|
153
161
|
|
154
162
|
def instrument(operation, payload = {}, &block)
|
155
163
|
ActiveSupport::Notifications.instrument(
|
data/lib/active_storage.rb
CHANGED
@@ -41,6 +41,7 @@ module ActiveStorage
|
|
41
41
|
autoload :Service
|
42
42
|
autoload :Previewer
|
43
43
|
autoload :Analyzer
|
44
|
+
autoload :DirectUploadToken
|
44
45
|
|
45
46
|
mattr_accessor :logger
|
46
47
|
mattr_accessor :verifier
|
@@ -71,6 +72,8 @@ module ActiveStorage
|
|
71
72
|
|
72
73
|
mattr_accessor :video_preview_arguments, default: "-y -vframes 1 -f image2"
|
73
74
|
|
75
|
+
mattr_accessor :silence_invalid_content_types_warning, default: false
|
76
|
+
|
74
77
|
module Transformers
|
75
78
|
extend ActiveSupport::Autoload
|
76
79
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activestorage
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 7.0.0
|
4
|
+
version: 7.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Heinemeier Hansson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-12-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -16,70 +16,70 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - '='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 7.0.0
|
19
|
+
version: 7.0.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - '='
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 7.0.0
|
26
|
+
version: 7.0.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: actionpack
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - '='
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 7.0.0
|
33
|
+
version: 7.0.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - '='
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 7.0.0
|
40
|
+
version: 7.0.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: activejob
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - '='
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: 7.0.0
|
47
|
+
version: 7.0.0
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - '='
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: 7.0.0
|
54
|
+
version: 7.0.0
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: activerecord
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - '='
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: 7.0.0
|
61
|
+
version: 7.0.0
|
62
62
|
type: :runtime
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - '='
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: 7.0.0
|
68
|
+
version: 7.0.0
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: marcel
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: 1.0
|
75
|
+
version: '1.0'
|
76
76
|
type: :runtime
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: 1.0
|
82
|
+
version: '1.0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: mini_mime
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -147,6 +147,7 @@ files:
|
|
147
147
|
- db/migrate/20170806125915_create_active_storage_tables.rb
|
148
148
|
- db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb
|
149
149
|
- db/update_migrate/20191206030411_create_active_storage_variant_records.rb
|
150
|
+
- db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb
|
150
151
|
- lib/active_storage.rb
|
151
152
|
- lib/active_storage/analyzer.rb
|
152
153
|
- lib/active_storage/analyzer/audio_analyzer.rb
|
@@ -169,6 +170,7 @@ files:
|
|
169
170
|
- lib/active_storage/attached/many.rb
|
170
171
|
- lib/active_storage/attached/model.rb
|
171
172
|
- lib/active_storage/attached/one.rb
|
173
|
+
- lib/active_storage/direct_upload_token.rb
|
172
174
|
- lib/active_storage/downloader.rb
|
173
175
|
- lib/active_storage/engine.rb
|
174
176
|
- lib/active_storage/errors.rb
|
@@ -197,10 +199,11 @@ licenses:
|
|
197
199
|
- MIT
|
198
200
|
metadata:
|
199
201
|
bug_tracker_uri: https://github.com/rails/rails/issues
|
200
|
-
changelog_uri: https://github.com/rails/rails/blob/v7.0.0
|
201
|
-
documentation_uri: https://api.rubyonrails.org/v7.0.0
|
202
|
+
changelog_uri: https://github.com/rails/rails/blob/v7.0.0/activestorage/CHANGELOG.md
|
203
|
+
documentation_uri: https://api.rubyonrails.org/v7.0.0/
|
202
204
|
mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
|
203
|
-
source_code_uri: https://github.com/rails/rails/tree/v7.0.0
|
205
|
+
source_code_uri: https://github.com/rails/rails/tree/v7.0.0/activestorage
|
206
|
+
rubygems_mfa_required: 'true'
|
204
207
|
post_install_message:
|
205
208
|
rdoc_options: []
|
206
209
|
require_paths:
|
@@ -212,11 +215,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
212
215
|
version: 2.7.0
|
213
216
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
214
217
|
requirements:
|
215
|
-
- - "
|
218
|
+
- - ">="
|
216
219
|
- !ruby/object:Gem::Version
|
217
|
-
version:
|
220
|
+
version: '0'
|
218
221
|
requirements: []
|
219
|
-
rubygems_version: 3.
|
222
|
+
rubygems_version: 3.2.32
|
220
223
|
signing_key:
|
221
224
|
specification_version: 4
|
222
225
|
summary: Local and cloud file storage framework.
|