activestorage 7.0.8.7 → 7.2.2.1
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 +49 -390
- 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/preview_image_job.rb +16 -0
- 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 +101 -16
- 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 +15 -3
- data/app/models/active_storage/blob/servable.rb +22 -0
- data/app/models/active_storage/blob.rb +59 -72
- data/app/models/active_storage/current.rb +0 -10
- data/app/models/active_storage/filename.rb +2 -4
- data/app/models/active_storage/named_variant.rb +21 -0
- data/app/models/active_storage/preview.rb +23 -8
- 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 +2 -2
- data/lib/active_storage/analyzer/audio_analyzer.rb +16 -4
- data/lib/active_storage/analyzer/image_analyzer/vips.rb +5 -9
- data/lib/active_storage/analyzer/image_analyzer.rb +2 -0
- data/lib/active_storage/analyzer/video_analyzer.rb +9 -3
- 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 +51 -4
- data/lib/active_storage/attached/changes/create_one_of_many.rb +5 -1
- data/lib/active_storage/attached/many.rb +5 -4
- data/lib/active_storage/attached/model.rb +96 -60
- 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 +7 -9
- 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/mupdf_previewer.rb +6 -2
- data/lib/active_storage/previewer/poppler_pdf_previewer.rb +6 -2
- data/lib/active_storage/previewer/video_previewer.rb +1 -1
- 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 +1 -1
- data/lib/active_storage/transformers/transformer.rb +2 -0
- data/lib/active_storage/version.rb +1 -1
- data/lib/active_storage.rb +5 -4
- metadata +18 -27
@@ -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)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveStorage::PreviewImageJob < ActiveStorage::BaseJob
|
4
|
+
queue_as { ActiveStorage.queues[:preview_image] }
|
5
|
+
|
6
|
+
discard_on ActiveRecord::RecordNotFound, ActiveStorage::UnrepresentableError
|
7
|
+
retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :polynomially_longer
|
8
|
+
|
9
|
+
def perform(blob, variations)
|
10
|
+
blob.preview({}).processed
|
11
|
+
|
12
|
+
variations.each do |transformations|
|
13
|
+
blob.preprocessed(transformations)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -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
|
-
|
21
|
+
##
|
22
|
+
# :method:
|
23
|
+
#
|
24
|
+
# Returns the associated record.
|
25
|
+
belongs_to :record, polymorphic: true, touch: ActiveStorage.touch_attachment_records
|
20
26
|
|
21
|
-
|
22
|
-
|
27
|
+
##
|
28
|
+
# :method:
|
29
|
+
#
|
30
|
+
# Returns the associated ActiveStorage::Blob.
|
31
|
+
belongs_to :blob, class_name: "ActiveStorage::Blob", autosave: true, inverse_of: :attachments
|
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,22 @@ class ActiveStorage::Attachment < ActiveStorage::Record
|
|
75
131
|
blob.mirror_later
|
76
132
|
end
|
77
133
|
|
134
|
+
def transform_variants_later
|
135
|
+
preprocessed_variations = named_variants.filter_map { |_name, named_variant|
|
136
|
+
if named_variant.preprocessed?(record)
|
137
|
+
named_variant.transformations
|
138
|
+
end
|
139
|
+
}
|
140
|
+
|
141
|
+
if blob.preview_image_needed_before_processing_variants? && preprocessed_variations.any?
|
142
|
+
blob.create_preview_image_later(preprocessed_variations)
|
143
|
+
else
|
144
|
+
preprocessed_variations.each do |transformations|
|
145
|
+
blob.preprocessed(transformations)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
78
150
|
def purge_dependent_blob_later
|
79
151
|
blob&.purge_later if dependent == :purge_later
|
80
152
|
end
|
@@ -83,8 +155,21 @@ class ActiveStorage::Attachment < ActiveStorage::Record
|
|
83
155
|
record.attachment_reflections[name]&.options&.fetch(:dependent, nil)
|
84
156
|
end
|
85
157
|
|
86
|
-
def
|
87
|
-
record.attachment_reflections[name]&.
|
158
|
+
def named_variants
|
159
|
+
record.attachment_reflections[name]&.named_variants || {}
|
160
|
+
end
|
161
|
+
|
162
|
+
def transformations_by_name(transformations)
|
163
|
+
case transformations
|
164
|
+
when Symbol
|
165
|
+
variant_name = transformations
|
166
|
+
named_variants.fetch(variant_name) do
|
167
|
+
record_model_name = record.to_model.model_name.name
|
168
|
+
raise ArgumentError, "Cannot find variant :#{variant_name} for #{record_model_name}##{name}"
|
169
|
+
end.transformations
|
170
|
+
else
|
171
|
+
transformations
|
172
|
+
end
|
88
173
|
end
|
89
174
|
end
|
90
175
|
|
@@ -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,18 @@ module ActiveStorage::Blob::Representable
|
|
98
98
|
variable? || previewable?
|
99
99
|
end
|
100
100
|
|
101
|
+
def preview_image_needed_before_processing_variants? # :nodoc:
|
102
|
+
previewable? && !preview_image.attached?
|
103
|
+
end
|
104
|
+
|
105
|
+
def create_preview_image_later(variations) # :nodoc:
|
106
|
+
ActiveStorage::PreviewImageJob.perform_later(self, variations) if representable?
|
107
|
+
end
|
108
|
+
|
109
|
+
def preprocessed(transformations) # :nodoc:
|
110
|
+
ActiveStorage::TransformJob.perform_later(self, transformations) if representable?
|
111
|
+
end
|
112
|
+
|
101
113
|
private
|
102
114
|
def default_variant_transformations
|
103
115
|
{ format: default_variant_format }
|
@@ -112,10 +124,10 @@ module ActiveStorage::Blob::Representable
|
|
112
124
|
end
|
113
125
|
|
114
126
|
def format
|
115
|
-
if filename.extension.present? &&
|
127
|
+
if filename.extension.present? && Marcel::MimeType.for(extension: filename.extension) == content_type
|
116
128
|
filename.extension
|
117
129
|
else
|
118
|
-
|
130
|
+
Marcel::Magic.new(content_type.to_s).extensions.first
|
119
131
|
end
|
120
132
|
end
|
121
133
|
|
@@ -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
|
@@ -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,27 +17,6 @@
|
|
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
|
-
# We use constant paths in the following include calls to avoid a gotcha of
|
19
|
-
# classic mode: If the parent application defines a top-level Analyzable, for
|
20
|
-
# example, and ActiveStorage::Blob::Analyzable is not yet loaded, a bare
|
21
|
-
#
|
22
|
-
# include Analyzable
|
23
|
-
#
|
24
|
-
# would resolve to the top-level one, const_missing would not be triggered,
|
25
|
-
# and therefore ActiveStorage::Blob::Analyzable would not be autoloaded.
|
26
|
-
#
|
27
|
-
# By using qualified names, we ensure const_missing is invoked if needed.
|
28
|
-
# Please, note that Ruby 2.5 or newer is required, so Object is not checked
|
29
|
-
# when looking up the ancestors of ActiveStorage::Blob.
|
30
|
-
#
|
31
|
-
# Zeitwerk mode does not have this gotcha. If we ever drop classic mode, this
|
32
|
-
# can be simplified, bare constant names would just work.
|
33
|
-
include ActiveStorage::Blob::Analyzable
|
34
|
-
include ActiveStorage::Blob::Identifiable
|
35
|
-
include ActiveStorage::Blob::Representable
|
36
|
-
|
37
|
-
self.table_name = "active_storage_blobs"
|
38
|
-
|
39
20
|
MINIMUM_TOKEN_LENGTH = 28
|
40
21
|
|
41
22
|
has_secure_token :key, length: MINIMUM_TOKEN_LENGTH
|
@@ -44,14 +25,24 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
44
25
|
class_attribute :services, default: {}
|
45
26
|
class_attribute :service, instance_accessor: false
|
46
27
|
|
28
|
+
##
|
29
|
+
# :method:
|
30
|
+
#
|
31
|
+
# Returns the associated ActiveStorage::Attachment instances.
|
47
32
|
has_many :attachments
|
48
33
|
|
34
|
+
##
|
35
|
+
# :singleton-method:
|
36
|
+
#
|
37
|
+
# Returns the blobs that aren't attached to any record.
|
49
38
|
scope :unattached, -> { where.missing(:attachments) }
|
50
39
|
|
51
40
|
after_initialize do
|
52
41
|
self.service_name ||= self.class.service&.name
|
53
42
|
end
|
54
43
|
|
44
|
+
after_update :touch_attachments
|
45
|
+
|
55
46
|
after_update_commit :update_service_metadata, if: -> { content_type_previously_changed? || metadata_previously_changed? }
|
56
47
|
|
57
48
|
before_destroy(prepend: true) do
|
@@ -141,32 +132,56 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
141
132
|
|
142
133
|
def scope_for_strict_loading # :nodoc:
|
143
134
|
if strict_loading_by_default? && ActiveStorage.track_variants
|
144
|
-
includes(
|
135
|
+
includes(
|
136
|
+
variant_records: { image_attachment: :blob },
|
137
|
+
preview_image_attachment: { blob: { variant_records: { image_attachment: :blob } } }
|
138
|
+
)
|
145
139
|
else
|
146
140
|
all
|
147
141
|
end
|
148
142
|
end
|
149
143
|
|
150
144
|
# Concatenate multiple blobs into a single "composed" blob.
|
151
|
-
def compose(blobs, filename:, content_type: nil, metadata: nil)
|
145
|
+
def compose(blobs, key: nil, filename:, content_type: nil, metadata: nil)
|
152
146
|
raise ActiveRecord::RecordNotSaved, "All blobs must be persisted." if blobs.any?(&:new_record?)
|
153
147
|
|
154
148
|
content_type ||= blobs.pluck(:content_type).compact.first
|
155
149
|
|
156
|
-
new(filename: filename, content_type: content_type, metadata: metadata, byte_size: blobs.sum(&:byte_size)).tap do |combined_blob|
|
150
|
+
new(key: key, filename: filename, content_type: content_type, metadata: metadata, byte_size: blobs.sum(&:byte_size)).tap do |combined_blob|
|
157
151
|
combined_blob.compose(blobs.pluck(:key))
|
158
152
|
combined_blob.save!
|
159
153
|
end
|
160
154
|
end
|
155
|
+
|
156
|
+
def validate_service_configuration(service_name, model_class, association_name) # :nodoc:
|
157
|
+
if service_name
|
158
|
+
services.fetch(service_name) do
|
159
|
+
raise ArgumentError, "Cannot configure service #{service_name.inspect} for #{model_class}##{association_name}"
|
160
|
+
end
|
161
|
+
else
|
162
|
+
validate_global_service_configuration
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def validate_global_service_configuration # :nodoc:
|
167
|
+
if connected? && table_exists? && Rails.configuration.active_storage.service.nil?
|
168
|
+
raise RuntimeError, "Missing Active Storage service name. Specify Active Storage service name for config.active_storage.service in config/environments/#{Rails.env}.rb"
|
169
|
+
end
|
170
|
+
end
|
161
171
|
end
|
162
172
|
|
173
|
+
include Analyzable
|
174
|
+
include Identifiable
|
175
|
+
include Representable
|
176
|
+
include Servable
|
177
|
+
|
163
178
|
# 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)
|
179
|
+
def signed_id(purpose: :blob_id, expires_in: nil, expires_at: nil)
|
165
180
|
super
|
166
181
|
end
|
167
182
|
|
168
183
|
# 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.
|
184
|
+
# secure-token format from \Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd.
|
170
185
|
# This key is not intended to be revealed directly to the user.
|
171
186
|
# Always refer to blobs using the signed_id or a verified form of the key.
|
172
187
|
def key
|
@@ -229,16 +244,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
229
244
|
service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum, custom_metadata: custom_metadata
|
230
245
|
end
|
231
246
|
|
232
|
-
def content_type_for_serving # :nodoc:
|
233
|
-
forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
|
234
|
-
end
|
235
|
-
|
236
|
-
def forced_disposition_for_serving # :nodoc:
|
237
|
-
if forcibly_serve_as_binary? || !allowed_inline?
|
238
|
-
:attachment
|
239
|
-
end
|
240
|
-
end
|
241
|
-
|
242
247
|
|
243
248
|
# Uploads the +io+ to the service on the +key+ for this blob. Blobs are intended to be immutable, so you shouldn't be
|
244
249
|
# using this method after a file has already been uploaded to fit with a blob. If you want to create a derivative blob,
|
@@ -309,7 +314,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
309
314
|
end
|
310
315
|
|
311
316
|
def mirror_later # :nodoc:
|
312
|
-
|
317
|
+
service.mirror_later key, checksum: checksum if service.respond_to?(:mirror_later)
|
313
318
|
end
|
314
319
|
|
315
320
|
# Deletes the files on the service associated with the blob. This should only be done if the blob is going to be
|
@@ -340,36 +345,14 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
340
345
|
services.fetch(service_name)
|
341
346
|
end
|
342
347
|
|
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
348
|
private
|
369
349
|
def compute_checksum_in_chunks(io)
|
350
|
+
raise ArgumentError, "io must be rewindable" unless io.respond_to?(:rewind)
|
351
|
+
|
370
352
|
OpenSSL::Digest::MD5.new.tap do |checksum|
|
371
|
-
|
372
|
-
|
353
|
+
read_buffer = "".b
|
354
|
+
while io.read(5.megabytes, read_buffer)
|
355
|
+
checksum << read_buffer
|
373
356
|
end
|
374
357
|
|
375
358
|
io.rewind
|
@@ -380,14 +363,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
380
363
|
Marcel::MimeType.for io, name: filename.to_s, declared_type: content_type
|
381
364
|
end
|
382
365
|
|
383
|
-
def forcibly_serve_as_binary?
|
384
|
-
ActiveStorage.content_types_to_serve_as_binary.include?(content_type)
|
385
|
-
end
|
386
|
-
|
387
|
-
def allowed_inline?
|
388
|
-
ActiveStorage.content_types_allowed_inline.include?(content_type)
|
389
|
-
end
|
390
|
-
|
391
366
|
def web_image?
|
392
367
|
ActiveStorage.web_image_content_types.include?(content_type)
|
393
368
|
end
|
@@ -402,6 +377,18 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
402
377
|
end
|
403
378
|
end
|
404
379
|
|
380
|
+
def touch_attachments
|
381
|
+
attachments.then do |relation|
|
382
|
+
if ActiveStorage.touch_attachment_records
|
383
|
+
relation.includes(:record)
|
384
|
+
else
|
385
|
+
relation
|
386
|
+
end
|
387
|
+
end.each do |attachment|
|
388
|
+
attachment.touch
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
405
392
|
def update_service_metadata
|
406
393
|
service.update_metadata key, **service_metadata if service_metadata.any?
|
407
394
|
end
|