activestorage 7.0.8 → 7.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +156 -303
- data/MIT-LICENSE +1 -1
- data/README.md +6 -6
- data/app/assets/javascripts/activestorage.esm.js +8 -4
- data/app/assets/javascripts/activestorage.js +9 -3
- data/app/controllers/active_storage/blobs/proxy_controller.rb +1 -0
- data/app/controllers/active_storage/disk_controller.rb +4 -2
- data/app/controllers/active_storage/representations/proxy_controller.rb +1 -0
- data/app/controllers/concerns/active_storage/disable_session.rb +12 -0
- 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/jobs/active_storage/analyze_job.rb +1 -1
- data/app/jobs/active_storage/mirror_job.rb +1 -1
- data/app/jobs/active_storage/purge_job.rb +1 -1
- 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 +26 -46
- 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 +5 -3
- data/app/models/active_storage/variant.rb +8 -7
- data/app/models/active_storage/variant_with_record.rb +19 -7
- data/app/models/active_storage/variation.rb +5 -3
- data/config/routes.rb +6 -4
- 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 +45 -3
- data/lib/active_storage/attached/many.rb +5 -4
- data/lib/active_storage/attached/model.rb +66 -43
- 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 -0
- data/lib/active_storage/gem_version.rb +3 -3
- 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 +17 -27
@@ -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) {
|
@@ -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) {
|
@@ -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)
|
@@ -8,6 +8,7 @@
|
|
8
8
|
# {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
|
9
9
|
class ActiveStorage::Representations::ProxyController < ActiveStorage::Representations::BaseController
|
10
10
|
include ActiveStorage::Streaming
|
11
|
+
include ActiveStorage::DisableSession
|
11
12
|
|
12
13
|
def show
|
13
14
|
http_cache_forever public: true do
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This concern disables the session in order to allow caching by default in some CDNs as CloudFlare.
|
4
|
+
module ActiveStorage::DisableSession
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
before_action do
|
9
|
+
request.session_options[:skip] = true
|
10
|
+
end
|
11
|
+
end
|
12
|
+
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) {
|
@@ -5,7 +5,7 @@ class ActiveStorage::AnalyzeJob < ActiveStorage::BaseJob
|
|
5
5
|
queue_as { ActiveStorage.queues[:analysis] }
|
6
6
|
|
7
7
|
discard_on ActiveRecord::RecordNotFound
|
8
|
-
retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :
|
8
|
+
retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :polynomially_longer
|
9
9
|
|
10
10
|
def perform(blob)
|
11
11
|
blob.analyze
|
@@ -7,7 +7,7 @@ class ActiveStorage::MirrorJob < ActiveStorage::BaseJob
|
|
7
7
|
queue_as { ActiveStorage.queues[:mirror] }
|
8
8
|
|
9
9
|
discard_on ActiveStorage::FileNotFoundError
|
10
|
-
retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :
|
10
|
+
retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :polynomially_longer
|
11
11
|
|
12
12
|
def perform(key, checksum:)
|
13
13
|
ActiveStorage::Blob.service.try(:mirror, key, checksum: checksum)
|
@@ -5,7 +5,7 @@ class ActiveStorage::PurgeJob < ActiveStorage::BaseJob
|
|
5
5
|
queue_as { ActiveStorage.queues[:purge] }
|
6
6
|
|
7
7
|
discard_on ActiveRecord::RecordNotFound
|
8
|
-
retry_on ActiveRecord::Deadlocked, attempts: 10, wait: :
|
8
|
+
retry_on ActiveRecord::Deadlocked, attempts: 10, wait: :polynomially_longer
|
9
9
|
|
10
10
|
def perform(blob)
|
11
11
|
blob.purge
|
@@ -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: :polynomially_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,24 +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
|
20
|
+
include Analyzable
|
21
|
+
include Identifiable
|
22
|
+
include Representable
|
36
23
|
|
37
24
|
self.table_name = "active_storage_blobs"
|
38
25
|
|
@@ -44,14 +31,24 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
44
31
|
class_attribute :services, default: {}
|
45
32
|
class_attribute :service, instance_accessor: false
|
46
33
|
|
34
|
+
##
|
35
|
+
# :method:
|
36
|
+
#
|
37
|
+
# Returns the associated ActiveStorage::Attachment instances.
|
47
38
|
has_many :attachments
|
48
39
|
|
40
|
+
##
|
41
|
+
# :singleton-method:
|
42
|
+
#
|
43
|
+
# Returns the blobs that aren't attached to any record.
|
49
44
|
scope :unattached, -> { where.missing(:attachments) }
|
50
45
|
|
51
46
|
after_initialize do
|
52
47
|
self.service_name ||= self.class.service&.name
|
53
48
|
end
|
54
49
|
|
50
|
+
after_update :touch_attachment_records
|
51
|
+
|
55
52
|
after_update_commit :update_service_metadata, if: -> { content_type_previously_changed? || metadata_previously_changed? }
|
56
53
|
|
57
54
|
before_destroy(prepend: true) do
|
@@ -161,12 +158,12 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
161
158
|
end
|
162
159
|
|
163
160
|
# Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering.
|
164
|
-
def signed_id(purpose: :blob_id, expires_in: nil)
|
161
|
+
def signed_id(purpose: :blob_id, expires_in: nil, expires_at: nil)
|
165
162
|
super
|
166
163
|
end
|
167
164
|
|
168
165
|
# Returns the key pointing to the file on the service that's associated with this blob. The key is the
|
169
|
-
# 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.
|
170
167
|
# This key is not intended to be revealed directly to the user.
|
171
168
|
# Always refer to blobs using the signed_id or a verified form of the key.
|
172
169
|
def key
|
@@ -309,7 +306,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
309
306
|
end
|
310
307
|
|
311
308
|
def mirror_later # :nodoc:
|
312
|
-
|
309
|
+
service.mirror_later key, checksum: checksum if service.respond_to?(:mirror_later)
|
313
310
|
end
|
314
311
|
|
315
312
|
# Deletes the files on the service associated with the blob. This should only be done if the blob is going to be
|
@@ -340,33 +337,10 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
340
337
|
services.fetch(service_name)
|
341
338
|
end
|
342
339
|
|
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
|
-
|
368
340
|
private
|
369
341
|
def compute_checksum_in_chunks(io)
|
342
|
+
raise ArgumentError, "io must be rewindable" unless io.respond_to?(:rewind)
|
343
|
+
|
370
344
|
OpenSSL::Digest::MD5.new.tap do |checksum|
|
371
345
|
while chunk = io.read(5.megabytes)
|
372
346
|
checksum << chunk
|
@@ -402,6 +376,12 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
402
376
|
end
|
403
377
|
end
|
404
378
|
|
379
|
+
def touch_attachment_records
|
380
|
+
attachments.includes(:record).each do |attachment|
|
381
|
+
attachment.touch
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
405
385
|
def update_service_metadata
|
406
386
|
service.update_metadata key, **service_metadata if service_metadata.any?
|
407
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
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveStorage::NamedVariant # :nodoc:
|
4
|
+
attr_reader :transformations, :preprocessed
|
5
|
+
|
6
|
+
def initialize(transformations)
|
7
|
+
@preprocessed = transformations[:preprocessed]
|
8
|
+
@transformations = transformations.except(:preprocessed)
|
9
|
+
end
|
10
|
+
|
11
|
+
def preprocessed?(record)
|
12
|
+
case preprocessed
|
13
|
+
when Symbol
|
14
|
+
record.send(preprocessed)
|
15
|
+
when Proc
|
16
|
+
preprocessed.call(record)
|
17
|
+
else
|
18
|
+
preprocessed
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|