activestorage 7.0.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +158 -178
- data/MIT-LICENSE +1 -1
- data/README.md +7 -7
- data/app/assets/javascripts/activestorage.esm.js +10 -18
- data/app/assets/javascripts/activestorage.js +11 -17
- data/app/controllers/active_storage/base_controller.rb +1 -1
- data/app/controllers/active_storage/blobs/proxy_controller.rb +2 -0
- data/app/controllers/active_storage/direct_uploads_controller.rb +1 -7
- data/app/controllers/active_storage/disk_controller.rb +4 -2
- data/app/controllers/active_storage/representations/proxy_controller.rb +3 -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/controllers/concerns/active_storage/streaming.rb +1 -0
- data/app/javascript/activestorage/blob_record.js +6 -10
- data/app/javascript/activestorage/direct_upload.js +3 -4
- data/app/javascript/activestorage/direct_upload_controller.js +1 -9
- 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 +88 -14
- 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 +27 -47
- 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 +11 -10
- data/app/models/active_storage/variant_with_record.rb +20 -8
- data/app/models/active_storage/variation.rb +6 -4
- data/config/routes.rb +6 -4
- data/db/migrate/20170806125915_create_active_storage_tables.rb +1 -1
- data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +4 -0
- data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +2 -0
- data/db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb +2 -0
- data/lib/active_storage/analyzer/audio_analyzer.rb +17 -5
- data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +9 -7
- data/lib/active_storage/analyzer/image_analyzer/vips.rb +9 -7
- data/lib/active_storage/analyzer/image_analyzer.rb +2 -0
- data/lib/active_storage/analyzer/video_analyzer.rb +15 -6
- 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 +31 -9
- data/lib/active_storage/errors.rb +0 -3
- data/lib/active_storage/fixture_set.rb +7 -8
- data/lib/active_storage/gem_version.rb +2 -2
- data/lib/active_storage/log_subscriber.rb +12 -0
- data/lib/active_storage/previewer/video_previewer.rb +2 -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/image_processing_transformer.rb +65 -0
- data/lib/active_storage/transformers/transformer.rb +2 -0
- data/lib/active_storage/version.rb +1 -1
- data/lib/active_storage.rb +310 -4
- metadata +21 -32
- data/lib/active_storage/direct_upload_token.rb +0 -59
@@ -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,
|
@@ -511,14 +511,15 @@
|
|
511
511
|
byte_size: file.size,
|
512
512
|
checksum: checksum
|
513
513
|
};
|
514
|
-
this.directUploadToken = directUploadToken;
|
515
|
-
this.attachmentName = attachmentName;
|
516
514
|
this.xhr = new XMLHttpRequest;
|
517
515
|
this.xhr.open("POST", url, true);
|
518
516
|
this.xhr.responseType = "json";
|
519
517
|
this.xhr.setRequestHeader("Content-Type", "application/json");
|
520
518
|
this.xhr.setRequestHeader("Accept", "application/json");
|
521
519
|
this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
|
520
|
+
Object.keys(customHeaders).forEach((headerKey => {
|
521
|
+
this.xhr.setRequestHeader(headerKey, customHeaders[headerKey]);
|
522
|
+
}));
|
522
523
|
const csrfToken = getMetaValue("csrf-token");
|
523
524
|
if (csrfToken != undefined) {
|
524
525
|
this.xhr.setRequestHeader("X-CSRF-Token", csrfToken);
|
@@ -540,9 +541,7 @@
|
|
540
541
|
create(callback) {
|
541
542
|
this.callback = callback;
|
542
543
|
this.xhr.send(JSON.stringify({
|
543
|
-
blob: this.attributes
|
544
|
-
direct_upload_token: this.directUploadToken,
|
545
|
-
attachment_name: this.attachmentName
|
544
|
+
blob: this.attributes
|
546
545
|
}));
|
547
546
|
}
|
548
547
|
requestDidLoad(event) {
|
@@ -600,13 +599,12 @@
|
|
600
599
|
}
|
601
600
|
let id = 0;
|
602
601
|
class DirectUpload {
|
603
|
-
constructor(file, url,
|
602
|
+
constructor(file, url, delegate, customHeaders = {}) {
|
604
603
|
this.id = ++id;
|
605
604
|
this.file = file;
|
606
605
|
this.url = url;
|
607
|
-
this.serviceName = serviceName;
|
608
|
-
this.attachmentName = attachmentName;
|
609
606
|
this.delegate = delegate;
|
607
|
+
this.customHeaders = customHeaders;
|
610
608
|
}
|
611
609
|
create(callback) {
|
612
610
|
FileChecksum.create(this.file, ((error, checksum) => {
|
@@ -614,7 +612,7 @@
|
|
614
612
|
callback(error);
|
615
613
|
return;
|
616
614
|
}
|
617
|
-
const blob = new BlobRecord(this.file, checksum, this.url, this.
|
615
|
+
const blob = new BlobRecord(this.file, checksum, this.url, this.customHeaders);
|
618
616
|
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
|
619
617
|
blob.create((error => {
|
620
618
|
if (error) {
|
@@ -643,7 +641,7 @@
|
|
643
641
|
constructor(input, file) {
|
644
642
|
this.input = input;
|
645
643
|
this.file = file;
|
646
|
-
this.directUpload = new DirectUpload(this.file, this.url, this
|
644
|
+
this.directUpload = new DirectUpload(this.file, this.url, this);
|
647
645
|
this.dispatch("initialize");
|
648
646
|
}
|
649
647
|
start(callback) {
|
@@ -674,12 +672,6 @@
|
|
674
672
|
get url() {
|
675
673
|
return this.input.getAttribute("data-direct-upload-url");
|
676
674
|
}
|
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
|
-
}
|
683
675
|
dispatch(name, detail = {}) {
|
684
676
|
detail.file = this.file;
|
685
677
|
detail.id = this.directUpload.id;
|
@@ -828,6 +820,8 @@
|
|
828
820
|
}
|
829
821
|
setTimeout(autostart, 1);
|
830
822
|
exports.DirectUpload = DirectUpload;
|
823
|
+
exports.DirectUploadController = DirectUploadController;
|
824
|
+
exports.DirectUploadsController = DirectUploadsController;
|
831
825
|
exports.start = start;
|
832
826
|
Object.defineProperty(exports, "__esModule", {
|
833
827
|
value: true
|
@@ -8,6 +8,8 @@
|
|
8
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
|
+
include ActiveStorage::Streaming
|
12
|
+
include ActiveStorage::DisableSession
|
11
13
|
|
12
14
|
def show
|
13
15
|
if request.headers["Range"].present?
|
@@ -4,10 +4,8 @@
|
|
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
|
-
|
9
7
|
def create
|
10
|
-
blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_args
|
8
|
+
blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_args)
|
11
9
|
render json: direct_upload_json(blob)
|
12
10
|
end
|
13
11
|
|
@@ -16,10 +14,6 @@ class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
|
|
16
14
|
params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, metadata: {}).to_h.symbolize_keys
|
17
15
|
end
|
18
16
|
|
19
|
-
def verified_service_name
|
20
|
-
ActiveStorage::DirectUploadToken.verify_direct_upload_token(params[:direct_upload_token], params[:attachment_name], session)
|
21
|
-
end
|
22
|
-
|
23
17
|
def direct_upload_json(blob)
|
24
18
|
blob.as_json(root: false, methods: :signed_id).merge(direct_upload: {
|
25
19
|
url: blob.service_url_for_direct_upload,
|
@@ -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)
|
@@ -7,6 +7,9 @@
|
|
7
7
|
# require a higher level of protection consider implementing
|
8
8
|
# {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
|
9
9
|
class ActiveStorage::Representations::ProxyController < ActiveStorage::Representations::BaseController
|
10
|
+
include ActiveStorage::Streaming
|
11
|
+
include ActiveStorage::DisableSession
|
12
|
+
|
10
13
|
def show
|
11
14
|
http_cache_forever public: true do
|
12
15
|
send_blob_stream @representation.image, disposition: params[:disposition]
|
@@ -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,25 +1,25 @@
|
|
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 = {
|
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
|
-
|
17
14
|
this.xhr = new XMLHttpRequest
|
18
15
|
this.xhr.open("POST", url, true)
|
19
16
|
this.xhr.responseType = "json"
|
20
17
|
this.xhr.setRequestHeader("Content-Type", "application/json")
|
21
18
|
this.xhr.setRequestHeader("Accept", "application/json")
|
22
19
|
this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest")
|
20
|
+
Object.keys(customHeaders).forEach((headerKey) => {
|
21
|
+
this.xhr.setRequestHeader(headerKey, customHeaders[headerKey])
|
22
|
+
})
|
23
23
|
|
24
24
|
const csrfToken = getMetaValue("csrf-token")
|
25
25
|
if (csrfToken != undefined) {
|
@@ -46,11 +46,7 @@ export class BlobRecord {
|
|
46
46
|
|
47
47
|
create(callback) {
|
48
48
|
this.callback = callback
|
49
|
-
this.xhr.send(JSON.stringify({
|
50
|
-
blob: this.attributes,
|
51
|
-
direct_upload_token: this.directUploadToken,
|
52
|
-
attachment_name: this.attachmentName
|
53
|
-
}))
|
49
|
+
this.xhr.send(JSON.stringify({ blob: this.attributes }))
|
54
50
|
}
|
55
51
|
|
56
52
|
requestDidLoad(event) {
|
@@ -5,13 +5,12 @@ import { BlobUpload } from "./blob_upload"
|
|
5
5
|
let id = 0
|
6
6
|
|
7
7
|
export class DirectUpload {
|
8
|
-
constructor(file, url,
|
8
|
+
constructor(file, url, delegate, customHeaders = {}) {
|
9
9
|
this.id = ++id
|
10
10
|
this.file = file
|
11
11
|
this.url = url
|
12
|
-
this.serviceName = serviceName
|
13
|
-
this.attachmentName = attachmentName
|
14
12
|
this.delegate = delegate
|
13
|
+
this.customHeaders = customHeaders
|
15
14
|
}
|
16
15
|
|
17
16
|
create(callback) {
|
@@ -21,7 +20,7 @@ export class DirectUpload {
|
|
21
20
|
return
|
22
21
|
}
|
23
22
|
|
24
|
-
const blob = new BlobRecord(this.file, checksum, this.url, this.
|
23
|
+
const blob = new BlobRecord(this.file, checksum, this.url, this.customHeaders)
|
25
24
|
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr)
|
26
25
|
|
27
26
|
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)
|
9
9
|
this.dispatch("initialize")
|
10
10
|
}
|
11
11
|
|
@@ -41,14 +41,6 @@ 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
|
-
|
52
44
|
dispatch(name, detail = {}) {
|
53
45
|
detail.file = this.file
|
54
46
|
detail.id = this.directUpload.id
|
@@ -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,11 +2,13 @@
|
|
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.
|
8
10
|
#
|
9
|
-
# Attachments also have access to all methods from
|
11
|
+
# Attachments also have access to all methods from ActiveStorage::Blob.
|
10
12
|
#
|
11
13
|
# If you wish to preload attachments or blobs, you can use these scopes:
|
12
14
|
#
|
@@ -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
|
|