activestorage 7.0.8 → 7.1.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +188 -271
- data/MIT-LICENSE +1 -1
- data/README.md +6 -6
- data/app/assets/javascripts/activestorage.esm.js +11 -7
- data/app/assets/javascripts/activestorage.js +12 -6
- 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 +2 -1
- 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/javascript/activestorage/ujs.js +3 -3
- 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 +90 -15
- 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/servable.rb +22 -0
- data/app/models/active_storage/blob.rb +31 -67
- 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 +11 -4
- data/app/models/active_storage/variant.rb +10 -7
- data/app/models/active_storage/variant_record.rb +0 -2
- data/app/models/active_storage/variant_with_record.rb +21 -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 +72 -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 +7 -1
- 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 +19 -28
@@ -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 button = event.target.closest("button, input");
|
775
|
+
if (button && button.type === "submit" && button.form) {
|
776
|
+
submitButtonsByForm.set(button.form, button);
|
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 button = event.target.closest("button, input");
|
758
|
+
if (button && button.type === "submit" && button.form) {
|
759
|
+
submitButtonsByForm.set(button.form, button);
|
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)
|
@@ -8,10 +8,11 @@
|
|
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
|
14
|
-
send_blob_stream @representation
|
15
|
+
send_blob_stream @representation, disposition: params[:disposition]
|
15
16
|
end
|
16
17
|
end
|
17
18
|
end
|
@@ -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) {
|
@@ -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 button = event.target.closest("button, input")
|
19
|
+
if (button && button.type === "submit" && button.form) {
|
20
|
+
submitButtonsByForm.set(button.form, button)
|
21
21
|
}
|
22
22
|
}
|
23
23
|
|
@@ -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, ActiveStorage::UnrepresentableError
|
7
|
+
retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :polynomially_longer
|
8
|
+
|
9
|
+
def perform(blob, transformations)
|
10
|
+
blob.representation(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.
|
@@ -16,18 +18,34 @@ require "active_support/core_ext/module/delegation"
|
|
16
18
|
# # preloads blobs and variant records (if using `ActiveStorage.track_variants`)
|
17
19
|
# User.first.avatars.with_all_variant_records
|
18
20
|
class ActiveStorage::Attachment < ActiveStorage::Record
|
19
|
-
|
20
|
-
|
21
|
+
##
|
22
|
+
# :method:
|
23
|
+
#
|
24
|
+
# Returns the associated record.
|
21
25
|
belongs_to :record, polymorphic: true, touch: true
|
26
|
+
|
27
|
+
##
|
28
|
+
# :method:
|
29
|
+
#
|
30
|
+
# Returns the associated ActiveStorage::Blob.
|
22
31
|
belongs_to :blob, class_name: "ActiveStorage::Blob", autosave: true
|
23
32
|
|
24
33
|
delegate_missing_to :blob
|
25
34
|
delegate :signed_id, to: :blob
|
26
35
|
|
27
|
-
after_create_commit :mirror_blob_later, :analyze_blob_later
|
36
|
+
after_create_commit :mirror_blob_later, :analyze_blob_later, :transform_variants_later
|
28
37
|
after_destroy_commit :purge_dependent_blob_later
|
29
38
|
|
30
|
-
|
39
|
+
##
|
40
|
+
# :singleton-method:
|
41
|
+
#
|
42
|
+
# Eager load all variant records on an attachment at once.
|
43
|
+
#
|
44
|
+
# User.first.avatars.with_all_variant_records
|
45
|
+
scope :with_all_variant_records, -> { includes(blob: {
|
46
|
+
variant_records: { image_attachment: :blob },
|
47
|
+
preview_image_attachment: { blob: { variant_records: { image_attachment: :blob } } }
|
48
|
+
}) }
|
31
49
|
|
32
50
|
# Synchronously deletes the attachment and {purges the blob}[rdoc-ref:ActiveStorage::Blob#purge].
|
33
51
|
def purge
|
@@ -49,23 +67,61 @@ class ActiveStorage::Attachment < ActiveStorage::Record
|
|
49
67
|
|
50
68
|
# Returns an ActiveStorage::Variant or ActiveStorage::VariantWithRecord
|
51
69
|
# instance for the attachment with the set of +transformations+ provided.
|
70
|
+
# Example:
|
71
|
+
#
|
72
|
+
# avatar.variant(resize_to_limit: [100, 100]).processed.url
|
73
|
+
#
|
74
|
+
# or if you are using pre-defined variants:
|
75
|
+
#
|
76
|
+
# avatar.variant(:thumb).processed.url
|
77
|
+
#
|
52
78
|
# See ActiveStorage::Blob::Representable#variant for more information.
|
53
79
|
#
|
54
80
|
# Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
|
55
81
|
# unknown pre-defined variant of the attachment.
|
56
82
|
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
|
-
|
83
|
+
transformations = transformations_by_name(transformations)
|
66
84
|
blob.variant(transformations)
|
67
85
|
end
|
68
86
|
|
87
|
+
# Returns an ActiveStorage::Preview instance for the attachment with the set
|
88
|
+
# of +transformations+ provided.
|
89
|
+
# Example:
|
90
|
+
#
|
91
|
+
# video.preview(resize_to_limit: [100, 100]).processed.url
|
92
|
+
#
|
93
|
+
# or if you are using pre-defined variants:
|
94
|
+
#
|
95
|
+
# video.preview(:thumb).processed.url
|
96
|
+
#
|
97
|
+
# See ActiveStorage::Blob::Representable#preview for more information.
|
98
|
+
#
|
99
|
+
# Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
|
100
|
+
# unknown pre-defined variant of the attachment.
|
101
|
+
def preview(transformations)
|
102
|
+
transformations = transformations_by_name(transformations)
|
103
|
+
blob.preview(transformations)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns an ActiveStorage::Preview or an ActiveStorage::Variant for the
|
107
|
+
# attachment with set of +transformations+ provided.
|
108
|
+
# Example:
|
109
|
+
#
|
110
|
+
# avatar.representation(resize_to_limit: [100, 100]).processed.url
|
111
|
+
#
|
112
|
+
# or if you are using pre-defined variants:
|
113
|
+
#
|
114
|
+
# avatar.representation(:thumb).processed.url
|
115
|
+
#
|
116
|
+
# See ActiveStorage::Blob::Representable#representation for more information.
|
117
|
+
#
|
118
|
+
# Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
|
119
|
+
# unknown pre-defined variant of the attachment.
|
120
|
+
def representation(transformations)
|
121
|
+
transformations = transformations_by_name(transformations)
|
122
|
+
blob.representation(transformations)
|
123
|
+
end
|
124
|
+
|
69
125
|
private
|
70
126
|
def analyze_blob_later
|
71
127
|
blob.analyze_later unless blob.analyzed?
|
@@ -75,6 +131,12 @@ class ActiveStorage::Attachment < ActiveStorage::Record
|
|
75
131
|
blob.mirror_later
|
76
132
|
end
|
77
133
|
|
134
|
+
def transform_variants_later
|
135
|
+
named_variants.each do |_name, named_variant|
|
136
|
+
blob.preprocessed(named_variant.transformations) if named_variant.preprocessed?(record)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
78
140
|
def purge_dependent_blob_later
|
79
141
|
blob&.purge_later if dependent == :purge_later
|
80
142
|
end
|
@@ -83,8 +145,21 @@ class ActiveStorage::Attachment < ActiveStorage::Record
|
|
83
145
|
record.attachment_reflections[name]&.options&.fetch(:dependent, nil)
|
84
146
|
end
|
85
147
|
|
86
|
-
def
|
87
|
-
record.attachment_reflections[name]&.
|
148
|
+
def named_variants
|
149
|
+
record.attachment_reflections[name]&.named_variants
|
150
|
+
end
|
151
|
+
|
152
|
+
def transformations_by_name(transformations)
|
153
|
+
case transformations
|
154
|
+
when Symbol
|
155
|
+
variant_name = transformations
|
156
|
+
named_variants.fetch(variant_name) do
|
157
|
+
record_model_name = record.to_model.model_name.name
|
158
|
+
raise ArgumentError, "Cannot find variant :#{variant_name} for #{record_model_name}##{name}"
|
159
|
+
end.transformations
|
160
|
+
else
|
161
|
+
transformations
|
162
|
+
end
|
88
163
|
end
|
89
164
|
end
|
90
165
|
|
@@ -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) if representable?
|
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
|
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage::Blob::Servable # :nodoc:
|
4
|
+
def content_type_for_serving
|
5
|
+
forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
|
6
|
+
end
|
7
|
+
|
8
|
+
def forced_disposition_for_serving
|
9
|
+
if forcibly_serve_as_binary? || !allowed_inline?
|
10
|
+
:attachment
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
def forcibly_serve_as_binary?
|
16
|
+
ActiveStorage.content_types_to_serve_as_binary.include?(content_type)
|
17
|
+
end
|
18
|
+
|
19
|
+
def allowed_inline?
|
20
|
+
ActiveStorage.content_types_allowed_inline.include?(content_type)
|
21
|
+
end
|
22
|
+
end
|