activestorage 7.0.10 → 7.1.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of activestorage might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +123 -381
- data/MIT-LICENSE +1 -1
- data/README.md +5 -5
- data/app/assets/javascripts/activestorage.esm.js +11 -7
- data/app/assets/javascripts/activestorage.js +12 -6
- data/app/controllers/active_storage/disk_controller.rb +4 -2
- data/app/controllers/active_storage/representations/proxy_controller.rb +1 -1
- data/app/controllers/concerns/active_storage/file_server.rb +4 -1
- data/app/javascript/activestorage/blob_record.js +4 -1
- data/app/javascript/activestorage/direct_upload.js +3 -2
- data/app/javascript/activestorage/index.js +3 -1
- data/app/javascript/activestorage/ujs.js +3 -3
- data/app/jobs/active_storage/transform_job.rb +12 -0
- data/app/models/active_storage/attachment.rb +87 -13
- data/app/models/active_storage/blob/analyzable.rb +4 -3
- data/app/models/active_storage/blob/identifiable.rb +1 -0
- data/app/models/active_storage/blob/representable.rb +7 -3
- data/app/models/active_storage/blob.rb +44 -50
- data/app/models/active_storage/current.rb +0 -10
- data/app/models/active_storage/filename.rb +2 -0
- data/app/models/active_storage/named_variant.rb +21 -0
- data/app/models/active_storage/preview.rb +6 -8
- data/app/models/active_storage/variant.rb +8 -3
- data/app/models/active_storage/variant_with_record.rb +16 -11
- data/app/models/active_storage/variation.rb +5 -3
- data/db/migrate/20170806125915_create_active_storage_tables.rb +1 -1
- data/lib/active_storage/analyzer/audio_analyzer.rb +16 -4
- data/lib/active_storage/analyzer/image_analyzer.rb +2 -0
- data/lib/active_storage/analyzer/video_analyzer.rb +3 -1
- data/lib/active_storage/analyzer.rb +2 -0
- data/lib/active_storage/attached/changes/create_many.rb +8 -3
- data/lib/active_storage/attached/changes/create_one.rb +14 -2
- data/lib/active_storage/attached/many.rb +5 -4
- data/lib/active_storage/attached/model.rb +59 -42
- data/lib/active_storage/attached/one.rb +5 -4
- data/lib/active_storage/attached.rb +2 -0
- data/lib/active_storage/deprecator.rb +7 -0
- data/lib/active_storage/engine.rb +11 -7
- data/lib/active_storage/fixture_set.rb +2 -4
- data/lib/active_storage/gem_version.rb +4 -4
- data/lib/active_storage/log_subscriber.rb +12 -0
- data/lib/active_storage/previewer.rb +8 -1
- data/lib/active_storage/reflection.rb +3 -3
- data/lib/active_storage/service/azure_storage_service.rb +2 -0
- data/lib/active_storage/service/disk_service.rb +2 -0
- data/lib/active_storage/service/gcs_service.rb +11 -20
- data/lib/active_storage/service/mirror_service.rb +10 -5
- data/lib/active_storage/service/s3_service.rb +2 -0
- data/lib/active_storage/service.rb +4 -2
- data/lib/active_storage/transformers/transformer.rb +2 -0
- data/lib/active_storage/version.rb +1 -1
- data/lib/active_storage.rb +19 -3
- metadata +22 -31
- data/app/models/active_storage/blob/servable.rb +0 -22
|
@@ -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, customHeaders = {}) {
|
|
512
512
|
this.file = file;
|
|
513
513
|
this.attributes = {
|
|
514
514
|
filename: file.name,
|
|
@@ -522,6 +522,9 @@ class BlobRecord {
|
|
|
522
522
|
this.xhr.setRequestHeader("Content-Type", "application/json");
|
|
523
523
|
this.xhr.setRequestHeader("Accept", "application/json");
|
|
524
524
|
this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
|
|
525
|
+
Object.keys(customHeaders).forEach((headerKey => {
|
|
526
|
+
this.xhr.setRequestHeader(headerKey, customHeaders[headerKey]);
|
|
527
|
+
}));
|
|
525
528
|
const csrfToken = getMetaValue("csrf-token");
|
|
526
529
|
if (csrfToken != undefined) {
|
|
527
530
|
this.xhr.setRequestHeader("X-CSRF-Token", csrfToken);
|
|
@@ -604,11 +607,12 @@ class BlobUpload {
|
|
|
604
607
|
let id = 0;
|
|
605
608
|
|
|
606
609
|
class DirectUpload {
|
|
607
|
-
constructor(file, url, delegate) {
|
|
610
|
+
constructor(file, url, delegate, customHeaders = {}) {
|
|
608
611
|
this.id = ++id;
|
|
609
612
|
this.file = file;
|
|
610
613
|
this.url = url;
|
|
611
614
|
this.delegate = delegate;
|
|
615
|
+
this.customHeaders = customHeaders;
|
|
612
616
|
}
|
|
613
617
|
create(callback) {
|
|
614
618
|
FileChecksum.create(this.file, ((error, checksum) => {
|
|
@@ -616,7 +620,7 @@ class DirectUpload {
|
|
|
616
620
|
callback(error);
|
|
617
621
|
return;
|
|
618
622
|
}
|
|
619
|
-
const blob = new BlobRecord(this.file, checksum, this.url);
|
|
623
|
+
const blob = new BlobRecord(this.file, checksum, this.url, this.customHeaders);
|
|
620
624
|
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
|
|
621
625
|
blob.create((error => {
|
|
622
626
|
if (error) {
|
|
@@ -767,9 +771,9 @@ function start() {
|
|
|
767
771
|
}
|
|
768
772
|
|
|
769
773
|
function didClick(event) {
|
|
770
|
-
const
|
|
771
|
-
if (
|
|
772
|
-
submitButtonsByForm.set(
|
|
774
|
+
const {target: target} = event;
|
|
775
|
+
if ((target.tagName == "INPUT" || target.tagName == "BUTTON") && target.type == "submit" && target.form) {
|
|
776
|
+
submitButtonsByForm.set(target.form, target);
|
|
773
777
|
}
|
|
774
778
|
}
|
|
775
779
|
|
|
@@ -841,4 +845,4 @@ function autostart() {
|
|
|
841
845
|
|
|
842
846
|
setTimeout(autostart, 1);
|
|
843
847
|
|
|
844
|
-
export { DirectUpload, start };
|
|
848
|
+
export { DirectUpload, DirectUploadController, DirectUploadsController, start };
|
|
@@ -503,7 +503,7 @@
|
|
|
503
503
|
}
|
|
504
504
|
}
|
|
505
505
|
class BlobRecord {
|
|
506
|
-
constructor(file, checksum, url) {
|
|
506
|
+
constructor(file, checksum, url, customHeaders = {}) {
|
|
507
507
|
this.file = file;
|
|
508
508
|
this.attributes = {
|
|
509
509
|
filename: file.name,
|
|
@@ -517,6 +517,9 @@
|
|
|
517
517
|
this.xhr.setRequestHeader("Content-Type", "application/json");
|
|
518
518
|
this.xhr.setRequestHeader("Accept", "application/json");
|
|
519
519
|
this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
|
|
520
|
+
Object.keys(customHeaders).forEach((headerKey => {
|
|
521
|
+
this.xhr.setRequestHeader(headerKey, customHeaders[headerKey]);
|
|
522
|
+
}));
|
|
520
523
|
const csrfToken = getMetaValue("csrf-token");
|
|
521
524
|
if (csrfToken != undefined) {
|
|
522
525
|
this.xhr.setRequestHeader("X-CSRF-Token", csrfToken);
|
|
@@ -596,11 +599,12 @@
|
|
|
596
599
|
}
|
|
597
600
|
let id = 0;
|
|
598
601
|
class DirectUpload {
|
|
599
|
-
constructor(file, url, delegate) {
|
|
602
|
+
constructor(file, url, delegate, customHeaders = {}) {
|
|
600
603
|
this.id = ++id;
|
|
601
604
|
this.file = file;
|
|
602
605
|
this.url = url;
|
|
603
606
|
this.delegate = delegate;
|
|
607
|
+
this.customHeaders = customHeaders;
|
|
604
608
|
}
|
|
605
609
|
create(callback) {
|
|
606
610
|
FileChecksum.create(this.file, ((error, checksum) => {
|
|
@@ -608,7 +612,7 @@
|
|
|
608
612
|
callback(error);
|
|
609
613
|
return;
|
|
610
614
|
}
|
|
611
|
-
const blob = new BlobRecord(this.file, checksum, this.url);
|
|
615
|
+
const blob = new BlobRecord(this.file, checksum, this.url, this.customHeaders);
|
|
612
616
|
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
|
|
613
617
|
blob.create((error => {
|
|
614
618
|
if (error) {
|
|
@@ -750,9 +754,9 @@
|
|
|
750
754
|
}
|
|
751
755
|
}
|
|
752
756
|
function didClick(event) {
|
|
753
|
-
const
|
|
754
|
-
if (
|
|
755
|
-
submitButtonsByForm.set(
|
|
757
|
+
const {target: target} = event;
|
|
758
|
+
if ((target.tagName == "INPUT" || target.tagName == "BUTTON") && target.type == "submit" && target.form) {
|
|
759
|
+
submitButtonsByForm.set(target.form, target);
|
|
756
760
|
}
|
|
757
761
|
}
|
|
758
762
|
function didSubmitForm(event) {
|
|
@@ -816,6 +820,8 @@
|
|
|
816
820
|
}
|
|
817
821
|
setTimeout(autostart, 1);
|
|
818
822
|
exports.DirectUpload = DirectUpload;
|
|
823
|
+
exports.DirectUploadController = DirectUploadController;
|
|
824
|
+
exports.DirectUploadsController = DirectUploadsController;
|
|
819
825
|
exports.start = start;
|
|
820
826
|
Object.defineProperty(exports, "__esModule", {
|
|
821
827
|
value: true
|
|
@@ -42,11 +42,13 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def decode_verified_key
|
|
45
|
-
ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key)
|
|
45
|
+
key = ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key)
|
|
46
|
+
key&.deep_symbolize_keys
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
def decode_verified_token
|
|
49
|
-
ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token)
|
|
50
|
+
token = ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token)
|
|
51
|
+
token&.deep_symbolize_keys
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
def acceptable_content?(token)
|
|
@@ -12,7 +12,7 @@ class ActiveStorage::Representations::ProxyController < ActiveStorage::Represent
|
|
|
12
12
|
|
|
13
13
|
def show
|
|
14
14
|
http_cache_forever public: true do
|
|
15
|
-
send_blob_stream @representation, disposition: params[:disposition]
|
|
15
|
+
send_blob_stream @representation.image, disposition: params[:disposition]
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "active_support/core_ext/hash/except"
|
|
4
|
+
|
|
3
5
|
module ActiveStorage::FileServer # :nodoc:
|
|
4
6
|
private
|
|
5
7
|
def serve_file(path, content_type:, disposition:)
|
|
6
|
-
Rack::
|
|
8
|
+
::Rack::Files.new(nil).serving(request, path).tap do |(status, headers, body)|
|
|
7
9
|
self.status = status
|
|
8
10
|
self.response_body = body
|
|
9
11
|
|
|
@@ -11,6 +13,7 @@ module ActiveStorage::FileServer # :nodoc:
|
|
|
11
13
|
response.headers[name] = value
|
|
12
14
|
end
|
|
13
15
|
|
|
16
|
+
response.headers.except!("X-Cascade", "x-cascade") if status == 416
|
|
14
17
|
response.headers["Content-Type"] = content_type || DEFAULT_SEND_FILE_TYPE
|
|
15
18
|
response.headers["Content-Disposition"] = disposition || DEFAULT_SEND_FILE_DISPOSITION
|
|
16
19
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getMetaValue } from "./helpers"
|
|
2
2
|
|
|
3
3
|
export class BlobRecord {
|
|
4
|
-
constructor(file, checksum, url) {
|
|
4
|
+
constructor(file, checksum, url, customHeaders = {}) {
|
|
5
5
|
this.file = file
|
|
6
6
|
|
|
7
7
|
this.attributes = {
|
|
@@ -17,6 +17,9 @@ export class BlobRecord {
|
|
|
17
17
|
this.xhr.setRequestHeader("Content-Type", "application/json")
|
|
18
18
|
this.xhr.setRequestHeader("Accept", "application/json")
|
|
19
19
|
this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest")
|
|
20
|
+
Object.keys(customHeaders).forEach((headerKey) => {
|
|
21
|
+
this.xhr.setRequestHeader(headerKey, customHeaders[headerKey])
|
|
22
|
+
})
|
|
20
23
|
|
|
21
24
|
const csrfToken = getMetaValue("csrf-token")
|
|
22
25
|
if (csrfToken != undefined) {
|
|
@@ -5,11 +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, delegate, customHeaders = {}) {
|
|
9
9
|
this.id = ++id
|
|
10
10
|
this.file = file
|
|
11
11
|
this.url = url
|
|
12
12
|
this.delegate = delegate
|
|
13
|
+
this.customHeaders = customHeaders
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
create(callback) {
|
|
@@ -19,7 +20,7 @@ export class DirectUpload {
|
|
|
19
20
|
return
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
const blob = new BlobRecord(this.file, checksum, this.url)
|
|
23
|
+
const blob = new BlobRecord(this.file, checksum, this.url, this.customHeaders)
|
|
23
24
|
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr)
|
|
24
25
|
|
|
25
26
|
blob.create(error => {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { start } from "./ujs"
|
|
2
2
|
import { DirectUpload } from "./direct_upload"
|
|
3
|
-
|
|
3
|
+
import { DirectUploadController } from "./direct_upload_controller"
|
|
4
|
+
import { DirectUploadsController } from "./direct_uploads_controller"
|
|
5
|
+
export { start, DirectUpload, DirectUploadController, DirectUploadsController }
|
|
4
6
|
|
|
5
7
|
function autostart() {
|
|
6
8
|
if (window.ActiveStorage) {
|
|
@@ -15,9 +15,9 @@ export function start() {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function didClick(event) {
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
20
|
-
submitButtonsByForm.set(
|
|
18
|
+
const { target } = event
|
|
19
|
+
if ((target.tagName == "INPUT" || target.tagName == "BUTTON") && target.type == "submit" && target.form) {
|
|
20
|
+
submitButtonsByForm.set(target.form, target)
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ActiveStorage::TransformJob < ActiveStorage::BaseJob
|
|
4
|
+
queue_as { ActiveStorage.queues[:transform] }
|
|
5
|
+
|
|
6
|
+
discard_on ActiveRecord::RecordNotFound
|
|
7
|
+
retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :exponentially_longer
|
|
8
|
+
|
|
9
|
+
def perform(blob, transformations)
|
|
10
|
+
blob.variant(transformations).processed
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "active_support/core_ext/module/delegation"
|
|
4
4
|
|
|
5
|
+
# = Active Storage \Attachment
|
|
6
|
+
#
|
|
5
7
|
# Attachments associate records with blobs. Usually that's a one record-many blobs relationship,
|
|
6
8
|
# but it is possible to associate many different records with the same blob. A foreign-key constraint
|
|
7
9
|
# on the attachments table prevents blobs from being purged if they’re still attached to any records.
|
|
@@ -18,16 +20,31 @@ require "active_support/core_ext/module/delegation"
|
|
|
18
20
|
class ActiveStorage::Attachment < ActiveStorage::Record
|
|
19
21
|
self.table_name = "active_storage_attachments"
|
|
20
22
|
|
|
23
|
+
##
|
|
24
|
+
# :method:
|
|
25
|
+
#
|
|
26
|
+
# Returns the associated record.
|
|
21
27
|
belongs_to :record, polymorphic: true, touch: true
|
|
28
|
+
|
|
29
|
+
##
|
|
30
|
+
# :method:
|
|
31
|
+
#
|
|
32
|
+
# Returns the associated ActiveStorage::Blob.
|
|
22
33
|
belongs_to :blob, class_name: "ActiveStorage::Blob", autosave: true
|
|
23
34
|
|
|
24
35
|
delegate_missing_to :blob
|
|
25
36
|
delegate :signed_id, to: :blob
|
|
26
37
|
|
|
27
|
-
after_create_commit :mirror_blob_later, :analyze_blob_later
|
|
38
|
+
after_create_commit :mirror_blob_later, :analyze_blob_later, :transform_variants_later
|
|
28
39
|
after_destroy_commit :purge_dependent_blob_later
|
|
29
40
|
|
|
30
|
-
|
|
41
|
+
##
|
|
42
|
+
# :singleton-method:
|
|
43
|
+
#
|
|
44
|
+
# Eager load all variant records on an attachment at once.
|
|
45
|
+
#
|
|
46
|
+
# User.first.avatars.with_all_variant_records
|
|
47
|
+
scope :with_all_variant_records, -> { includes(blob: { variant_records: { image_attachment: :blob } }) }
|
|
31
48
|
|
|
32
49
|
# Synchronously deletes the attachment and {purges the blob}[rdoc-ref:ActiveStorage::Blob#purge].
|
|
33
50
|
def purge
|
|
@@ -49,23 +66,61 @@ class ActiveStorage::Attachment < ActiveStorage::Record
|
|
|
49
66
|
|
|
50
67
|
# Returns an ActiveStorage::Variant or ActiveStorage::VariantWithRecord
|
|
51
68
|
# instance for the attachment with the set of +transformations+ provided.
|
|
69
|
+
# Example:
|
|
70
|
+
#
|
|
71
|
+
# avatar.variant(resize_to_limit: [100, 100]).processed.url
|
|
72
|
+
#
|
|
73
|
+
# or if you are using pre-defined variants:
|
|
74
|
+
#
|
|
75
|
+
# avatar.variant(:thumb).processed.url
|
|
76
|
+
#
|
|
52
77
|
# See ActiveStorage::Blob::Representable#variant for more information.
|
|
53
78
|
#
|
|
54
79
|
# Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
|
|
55
80
|
# unknown pre-defined variant of the attachment.
|
|
56
81
|
def variant(transformations)
|
|
57
|
-
|
|
58
|
-
when Symbol
|
|
59
|
-
variant_name = transformations
|
|
60
|
-
transformations = variants.fetch(variant_name) do
|
|
61
|
-
record_model_name = record.to_model.model_name.name
|
|
62
|
-
raise ArgumentError, "Cannot find variant :#{variant_name} for #{record_model_name}##{name}"
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
82
|
+
transformations = transformations_by_name(transformations)
|
|
66
83
|
blob.variant(transformations)
|
|
67
84
|
end
|
|
68
85
|
|
|
86
|
+
# Returns an ActiveStorage::Preview instance for the attachment with the set
|
|
87
|
+
# of +transformations+ provided.
|
|
88
|
+
# Example:
|
|
89
|
+
#
|
|
90
|
+
# video.preview(resize_to_limit: [100, 100]).processed.url
|
|
91
|
+
#
|
|
92
|
+
# or if you are using pre-defined variants:
|
|
93
|
+
#
|
|
94
|
+
# video.preview(:thumb).processed.url
|
|
95
|
+
#
|
|
96
|
+
# See ActiveStorage::Blob::Representable#preview for more information.
|
|
97
|
+
#
|
|
98
|
+
# Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
|
|
99
|
+
# unknown pre-defined variant of the attachment.
|
|
100
|
+
def preview(transformations)
|
|
101
|
+
transformations = transformations_by_name(transformations)
|
|
102
|
+
blob.preview(transformations)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Returns an ActiveStorage::Preview or an ActiveStorage::Variant for the
|
|
106
|
+
# attachment with set of +transformations+ provided.
|
|
107
|
+
# Example:
|
|
108
|
+
#
|
|
109
|
+
# avatar.representation(resize_to_limit: [100, 100]).processed.url
|
|
110
|
+
#
|
|
111
|
+
# or if you are using pre-defined variants:
|
|
112
|
+
#
|
|
113
|
+
# avatar.representation(:thumb).processed.url
|
|
114
|
+
#
|
|
115
|
+
# See ActiveStorage::Blob::Representable#representation for more information.
|
|
116
|
+
#
|
|
117
|
+
# Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
|
|
118
|
+
# unknown pre-defined variant of the attachment.
|
|
119
|
+
def representation(transformations)
|
|
120
|
+
transformations = transformations_by_name(transformations)
|
|
121
|
+
blob.representation(transformations)
|
|
122
|
+
end
|
|
123
|
+
|
|
69
124
|
private
|
|
70
125
|
def analyze_blob_later
|
|
71
126
|
blob.analyze_later unless blob.analyzed?
|
|
@@ -75,6 +130,12 @@ class ActiveStorage::Attachment < ActiveStorage::Record
|
|
|
75
130
|
blob.mirror_later
|
|
76
131
|
end
|
|
77
132
|
|
|
133
|
+
def transform_variants_later
|
|
134
|
+
named_variants.each do |_name, named_variant|
|
|
135
|
+
blob.preprocessed(named_variant.transformations) if named_variant.preprocessed?(record)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
78
139
|
def purge_dependent_blob_later
|
|
79
140
|
blob&.purge_later if dependent == :purge_later
|
|
80
141
|
end
|
|
@@ -83,8 +144,21 @@ class ActiveStorage::Attachment < ActiveStorage::Record
|
|
|
83
144
|
record.attachment_reflections[name]&.options&.fetch(:dependent, nil)
|
|
84
145
|
end
|
|
85
146
|
|
|
86
|
-
def
|
|
87
|
-
record.attachment_reflections[name]&.
|
|
147
|
+
def named_variants
|
|
148
|
+
record.attachment_reflections[name]&.named_variants
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def transformations_by_name(transformations)
|
|
152
|
+
case transformations
|
|
153
|
+
when Symbol
|
|
154
|
+
variant_name = transformations
|
|
155
|
+
named_variants.fetch(variant_name) do
|
|
156
|
+
record_model_name = record.to_model.model_name.name
|
|
157
|
+
raise ArgumentError, "Cannot find variant :#{variant_name} for #{record_model_name}##{name}"
|
|
158
|
+
end.transformations
|
|
159
|
+
else
|
|
160
|
+
transformations
|
|
161
|
+
end
|
|
88
162
|
end
|
|
89
163
|
end
|
|
90
164
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "active_storage/analyzer/null_analyzer"
|
|
4
4
|
|
|
5
|
+
# = Active Storage \Blob \Analyzable
|
|
5
6
|
module ActiveStorage::Blob::Analyzable
|
|
6
7
|
# Extracts and stores metadata from the file associated with this blob using a relevant analyzer. Active Storage comes
|
|
7
8
|
# with built-in analyzers for images and videos. See ActiveStorage::Analyzer::ImageAnalyzer and
|
|
@@ -12,7 +13,7 @@ module ActiveStorage::Blob::Analyzable
|
|
|
12
13
|
# first analyzer for which +accept?+ returns true when given the blob. If no registered analyzer accepts the blob, no
|
|
13
14
|
# metadata is extracted from it.
|
|
14
15
|
#
|
|
15
|
-
# In a Rails application, add or remove analyzers by manipulating +Rails.application.config.active_storage.analyzers+
|
|
16
|
+
# In a \Rails application, add or remove analyzers by manipulating +Rails.application.config.active_storage.analyzers+
|
|
16
17
|
# in an initializer:
|
|
17
18
|
#
|
|
18
19
|
# # Add a custom analyzer for Microsoft Office documents:
|
|
@@ -21,9 +22,9 @@ module ActiveStorage::Blob::Analyzable
|
|
|
21
22
|
# # Remove the built-in video analyzer:
|
|
22
23
|
# Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer
|
|
23
24
|
#
|
|
24
|
-
# Outside of a Rails application, manipulate +ActiveStorage.analyzers+ instead.
|
|
25
|
+
# Outside of a \Rails application, manipulate +ActiveStorage.analyzers+ instead.
|
|
25
26
|
#
|
|
26
|
-
# You won't ordinarily need to call this method from a Rails application. New blobs are automatically and asynchronously
|
|
27
|
+
# You won't ordinarily need to call this method from a \Rails application. New blobs are automatically and asynchronously
|
|
27
28
|
# analyzed via #analyze_later when they're attached for the first time.
|
|
28
29
|
def analyze
|
|
29
30
|
update! metadata: metadata.merge(extract_metadata_via_analyzer)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "marcel"
|
|
4
4
|
|
|
5
5
|
module ActiveStorage::Blob::Representable
|
|
6
6
|
extend ActiveSupport::Concern
|
|
@@ -98,6 +98,10 @@ module ActiveStorage::Blob::Representable
|
|
|
98
98
|
variable? || previewable?
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
def preprocessed(transformations) # :nodoc:
|
|
102
|
+
ActiveStorage::TransformJob.perform_later(self, transformations)
|
|
103
|
+
end
|
|
104
|
+
|
|
101
105
|
private
|
|
102
106
|
def default_variant_transformations
|
|
103
107
|
{ format: default_variant_format }
|
|
@@ -112,10 +116,10 @@ module ActiveStorage::Blob::Representable
|
|
|
112
116
|
end
|
|
113
117
|
|
|
114
118
|
def format
|
|
115
|
-
if filename.extension.present? &&
|
|
119
|
+
if filename.extension.present? && Marcel::MimeType.for(extension: filename.extension) == content_type
|
|
116
120
|
filename.extension
|
|
117
121
|
else
|
|
118
|
-
|
|
122
|
+
Marcel::Magic.new(content_type.to_s).extensions.first
|
|
119
123
|
end
|
|
120
124
|
end
|
|
121
125
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# = Active Storage \Blob
|
|
4
|
+
#
|
|
3
5
|
# A blob is a record that contains the metadata about a file and a key for where that file resides on the service.
|
|
4
6
|
# Blobs can be created in two ways:
|
|
5
7
|
#
|
|
@@ -15,25 +17,9 @@
|
|
|
15
17
|
# update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file.
|
|
16
18
|
# If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one.
|
|
17
19
|
class ActiveStorage::Blob < ActiveStorage::Record
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
#
|
|
22
|
-
# include Analyzable
|
|
23
|
-
#
|
|
24
|
-
# would resolve to the top-level one, const_missing would not be triggered,
|
|
25
|
-
# and therefore ActiveStorage::Blob::Analyzable would not be autoloaded.
|
|
26
|
-
#
|
|
27
|
-
# By using qualified names, we ensure const_missing is invoked if needed.
|
|
28
|
-
# Please, note that Ruby 2.5 or newer is required, so Object is not checked
|
|
29
|
-
# when looking up the ancestors of ActiveStorage::Blob.
|
|
30
|
-
#
|
|
31
|
-
# Zeitwerk mode does not have this gotcha. If we ever drop classic mode, this
|
|
32
|
-
# can be simplified, bare constant names would just work.
|
|
33
|
-
include ActiveStorage::Blob::Analyzable
|
|
34
|
-
include ActiveStorage::Blob::Identifiable
|
|
35
|
-
include ActiveStorage::Blob::Representable
|
|
36
|
-
include ActiveStorage::Blob::Servable
|
|
20
|
+
include Analyzable
|
|
21
|
+
include Identifiable
|
|
22
|
+
include Representable
|
|
37
23
|
|
|
38
24
|
self.table_name = "active_storage_blobs"
|
|
39
25
|
|
|
@@ -45,14 +31,24 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
45
31
|
class_attribute :services, default: {}
|
|
46
32
|
class_attribute :service, instance_accessor: false
|
|
47
33
|
|
|
34
|
+
##
|
|
35
|
+
# :method:
|
|
36
|
+
#
|
|
37
|
+
# Returns the associated +ActiveStorage::Attachment+s.
|
|
48
38
|
has_many :attachments
|
|
49
39
|
|
|
40
|
+
##
|
|
41
|
+
# :singleton-method:
|
|
42
|
+
#
|
|
43
|
+
# Returns the blobs that aren't attached to any record.
|
|
50
44
|
scope :unattached, -> { where.missing(:attachments) }
|
|
51
45
|
|
|
52
46
|
after_initialize do
|
|
53
47
|
self.service_name ||= self.class.service&.name
|
|
54
48
|
end
|
|
55
49
|
|
|
50
|
+
after_update :touch_attachment_records
|
|
51
|
+
|
|
56
52
|
after_update_commit :update_service_metadata, if: -> { content_type_previously_changed? || metadata_previously_changed? }
|
|
57
53
|
|
|
58
54
|
before_destroy(prepend: true) do
|
|
@@ -142,10 +138,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
142
138
|
|
|
143
139
|
def scope_for_strict_loading # :nodoc:
|
|
144
140
|
if strict_loading_by_default? && ActiveStorage.track_variants
|
|
145
|
-
includes(
|
|
146
|
-
variant_records: { image_attachment: :blob },
|
|
147
|
-
preview_image_attachment: { blob: { variant_records: { image_attachment: :blob } } }
|
|
148
|
-
)
|
|
141
|
+
includes(variant_records: { image_attachment: :blob }, preview_image_attachment: :blob)
|
|
149
142
|
else
|
|
150
143
|
all
|
|
151
144
|
end
|
|
@@ -170,7 +163,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
170
163
|
end
|
|
171
164
|
|
|
172
165
|
# Returns the key pointing to the file on the service that's associated with this blob. The key is the
|
|
173
|
-
# secure-token format from Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd.
|
|
166
|
+
# secure-token format from \Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd.
|
|
174
167
|
# This key is not intended to be revealed directly to the user.
|
|
175
168
|
# Always refer to blobs using the signed_id or a verified form of the key.
|
|
176
169
|
def key
|
|
@@ -233,6 +226,16 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
233
226
|
service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum, custom_metadata: custom_metadata
|
|
234
227
|
end
|
|
235
228
|
|
|
229
|
+
def content_type_for_serving # :nodoc:
|
|
230
|
+
forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def forced_disposition_for_serving # :nodoc:
|
|
234
|
+
if forcibly_serve_as_binary? || !allowed_inline?
|
|
235
|
+
:attachment
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
236
239
|
|
|
237
240
|
# Uploads the +io+ to the service on the +key+ for this blob. Blobs are intended to be immutable, so you shouldn't be
|
|
238
241
|
# using this method after a file has already been uploaded to fit with a blob. If you want to create a derivative blob,
|
|
@@ -303,7 +306,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
303
306
|
end
|
|
304
307
|
|
|
305
308
|
def mirror_later # :nodoc:
|
|
306
|
-
|
|
309
|
+
service.mirror_later key, checksum: checksum if service.respond_to?(:mirror_later)
|
|
307
310
|
end
|
|
308
311
|
|
|
309
312
|
# Deletes the files on the service associated with the blob. This should only be done if the blob is going to be
|
|
@@ -334,33 +337,10 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
334
337
|
services.fetch(service_name)
|
|
335
338
|
end
|
|
336
339
|
|
|
337
|
-
def content_type=(value)
|
|
338
|
-
unless ActiveStorage.silence_invalid_content_types_warning
|
|
339
|
-
if INVALID_VARIABLE_CONTENT_TYPES_DEPRECATED_IN_RAILS_7.include?(value)
|
|
340
|
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
|
|
341
|
-
#{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.
|
|
342
|
-
If you want to keep supporting this content type past Rails 7.1, add it to `config.active_storage.variable_content_types`.
|
|
343
|
-
Dismiss this warning by setting `config.active_storage.silence_invalid_content_types_warning = true`.
|
|
344
|
-
MSG
|
|
345
|
-
end
|
|
346
|
-
|
|
347
|
-
if INVALID_VARIABLE_CONTENT_TYPES_TO_SERVE_AS_BINARY_DEPRECATED_IN_RAILS_7.include?(value)
|
|
348
|
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
|
|
349
|
-
#{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.
|
|
350
|
-
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`.
|
|
351
|
-
Dismiss this warning by setting `config.active_storage.silence_invalid_content_types_warning = true`.
|
|
352
|
-
MSG
|
|
353
|
-
end
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
super
|
|
357
|
-
end
|
|
358
|
-
|
|
359
|
-
INVALID_VARIABLE_CONTENT_TYPES_DEPRECATED_IN_RAILS_7 = ["image/jpg", "image/pjpeg", "image/bmp"]
|
|
360
|
-
INVALID_VARIABLE_CONTENT_TYPES_TO_SERVE_AS_BINARY_DEPRECATED_IN_RAILS_7 = ["text/javascript"]
|
|
361
|
-
|
|
362
340
|
private
|
|
363
341
|
def compute_checksum_in_chunks(io)
|
|
342
|
+
raise ArgumentError, "io must be rewindable" unless io.respond_to?(:rewind)
|
|
343
|
+
|
|
364
344
|
OpenSSL::Digest::MD5.new.tap do |checksum|
|
|
365
345
|
while chunk = io.read(5.megabytes)
|
|
366
346
|
checksum << chunk
|
|
@@ -374,6 +354,14 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
374
354
|
Marcel::MimeType.for io, name: filename.to_s, declared_type: content_type
|
|
375
355
|
end
|
|
376
356
|
|
|
357
|
+
def forcibly_serve_as_binary?
|
|
358
|
+
ActiveStorage.content_types_to_serve_as_binary.include?(content_type)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def allowed_inline?
|
|
362
|
+
ActiveStorage.content_types_allowed_inline.include?(content_type)
|
|
363
|
+
end
|
|
364
|
+
|
|
377
365
|
def web_image?
|
|
378
366
|
ActiveStorage.web_image_content_types.include?(content_type)
|
|
379
367
|
end
|
|
@@ -388,6 +376,12 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
388
376
|
end
|
|
389
377
|
end
|
|
390
378
|
|
|
379
|
+
def touch_attachment_records
|
|
380
|
+
attachments.includes(:record).each do |attachment|
|
|
381
|
+
attachment.touch
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
391
385
|
def update_service_metadata
|
|
392
386
|
service.update_metadata key, **service_metadata if service_metadata.any?
|
|
393
387
|
end
|
|
@@ -2,14 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
class ActiveStorage::Current < ActiveSupport::CurrentAttributes # :nodoc:
|
|
4
4
|
attribute :url_options
|
|
5
|
-
|
|
6
|
-
def host=(host)
|
|
7
|
-
ActiveSupport::Deprecation.warn("ActiveStorage::Current.host= is deprecated, instead use ActiveStorage::Current.url_options=")
|
|
8
|
-
self.url_options = { host: host }
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def host
|
|
12
|
-
ActiveSupport::Deprecation.warn("ActiveStorage::Current.host is deprecated, instead use ActiveStorage::Current.url_options")
|
|
13
|
-
self.url_options&.dig(:host)
|
|
14
|
-
end
|
|
15
5
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# = Active Storage \Filename
|
|
4
|
+
#
|
|
3
5
|
# Encapsulates a string representing a filename to provide convenient access to parts of it and sanitization.
|
|
4
6
|
# A Filename instance is returned by ActiveStorage::Blob#filename, and is comparable so it can be used for sorting.
|
|
5
7
|
class ActiveStorage::Filename
|