activestorage 7.0.8.4 → 7.1.3.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +190 -288
- 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/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/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 +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 +16 -26
data/README.md
CHANGED
@@ -6,11 +6,11 @@ Files can be uploaded from the server to the cloud or directly from the client t
|
|
6
6
|
|
7
7
|
Image files can furthermore be transformed using on-demand variants for quality, aspect ratio, size, or any other [MiniMagick](https://github.com/minimagick/minimagick) or [Vips](https://www.rubydoc.info/gems/ruby-vips/Vips/Image) supported transformation.
|
8
8
|
|
9
|
-
You can read more about Active Storage in the [Active Storage Overview](https://
|
9
|
+
You can read more about Active Storage in the [Active Storage Overview](https://guides.rubyonrails.org/active_storage_overview.html) guide.
|
10
10
|
|
11
11
|
## Compared to other storage solutions
|
12
12
|
|
13
|
-
A key difference to how Active Storage works compared to other attachment solutions in Rails is through the use of built-in [Blob](https://github.com/rails/rails/blob/main/activestorage/app/models/active_storage/blob.rb) and [Attachment](https://github.com/rails/rails/blob/main/activestorage/app/models/active_storage/attachment.rb) models (backed by Active Record). This means existing application models do not need to be modified with additional columns to associate with files. Active Storage uses polymorphic associations via the `Attachment` join model, which then connects to the actual `Blob`.
|
13
|
+
A key difference to how Active Storage works compared to other attachment solutions in \Rails is through the use of built-in [Blob](https://github.com/rails/rails/blob/main/activestorage/app/models/active_storage/blob.rb) and [Attachment](https://github.com/rails/rails/blob/main/activestorage/app/models/active_storage/attachment.rb) models (backed by Active Record). This means existing application models do not need to be modified with additional columns to associate with files. Active Storage uses polymorphic associations via the `Attachment` join model, which then connects to the actual `Blob`.
|
14
14
|
|
15
15
|
`Blob` models store attachment metadata (filename, content-type, etc.), and their identifier key in the storage service. Blob models do not store the actual binary data. They are intended to be immutable in spirit. One file, one blob. You can associate the same blob with multiple application models as well. And if you want to do transformations of a given `Blob`, the idea is that you'll simply create a new one, rather than attempt to mutate the existing one (though of course you can delete the previous version later if you don't need it).
|
16
16
|
|
@@ -144,11 +144,11 @@ Active Storage, with its included JavaScript library, supports uploading directl
|
|
144
144
|
|
145
145
|
1. Include the Active Storage JavaScript in your application's JavaScript bundle or reference it directly.
|
146
146
|
|
147
|
-
Requiring directly without bundling through the asset pipeline in the application
|
148
|
-
```
|
147
|
+
Requiring directly without bundling through the asset pipeline in the application HTML with autostart:
|
148
|
+
```erb
|
149
149
|
<%= javascript_include_tag "activestorage" %>
|
150
150
|
```
|
151
|
-
Requiring via importmap-rails without bundling through the asset pipeline in the application
|
151
|
+
Requiring via importmap-rails without bundling through the asset pipeline in the application HTML without autostart as ESM:
|
152
152
|
```ruby
|
153
153
|
# config/importmap.rb
|
154
154
|
pin "@rails/activestorage", to: "activestorage.esm.js"
|
@@ -170,7 +170,7 @@ Active Storage, with its included JavaScript library, supports uploading directl
|
|
170
170
|
```
|
171
171
|
2. Annotate file inputs with the direct upload URL.
|
172
172
|
|
173
|
-
```
|
173
|
+
```erb
|
174
174
|
<%= form.file_field :attachments, multiple: true, direct_upload: true %>
|
175
175
|
```
|
176
176
|
3. That's it! Uploads begin upon form submission.
|
@@ -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)
|
@@ -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
|
15
|
+
send_blob_stream @representation, 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 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
|