activestorage 6.1.6.1 → 7.0.3.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activestorage might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +180 -212
- data/README.md +25 -11
- data/app/assets/javascripts/activestorage.esm.js +844 -0
- data/app/assets/javascripts/activestorage.js +257 -376
- data/app/controllers/active_storage/base_controller.rb +0 -9
- data/app/controllers/active_storage/blobs/proxy_controller.rb +15 -4
- data/app/controllers/active_storage/blobs/redirect_controller.rb +6 -4
- data/app/controllers/active_storage/disk_controller.rb +1 -0
- data/app/controllers/active_storage/representations/base_controller.rb +5 -1
- data/app/controllers/active_storage/representations/proxy_controller.rb +7 -3
- data/app/controllers/active_storage/representations/redirect_controller.rb +6 -4
- data/app/controllers/concerns/active_storage/set_blob.rb +6 -2
- data/app/controllers/concerns/active_storage/set_current.rb +3 -3
- data/app/controllers/concerns/active_storage/streaming.rb +65 -0
- data/app/javascript/activestorage/ujs.js +1 -1
- data/app/models/active_storage/attachment.rb +35 -2
- data/app/models/active_storage/blob/representable.rb +7 -5
- data/app/models/active_storage/blob.rb +92 -36
- data/app/models/active_storage/current.rb +12 -2
- data/app/models/active_storage/preview.rb +6 -4
- data/app/models/active_storage/record.rb +1 -1
- data/app/models/active_storage/variant.rb +3 -6
- data/app/models/active_storage/variant_record.rb +2 -0
- data/app/models/active_storage/variant_with_record.rb +9 -5
- data/app/models/active_storage/variation.rb +2 -2
- data/config/routes.rb +10 -10
- data/db/migrate/20170806125915_create_active_storage_tables.rb +32 -11
- 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 +17 -2
- data/db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb +7 -0
- data/lib/active_storage/analyzer/audio_analyzer.rb +65 -0
- data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +39 -0
- data/lib/active_storage/analyzer/image_analyzer/vips.rb +49 -0
- data/lib/active_storage/analyzer/image_analyzer.rb +2 -30
- data/lib/active_storage/analyzer/video_analyzer.rb +27 -12
- data/lib/active_storage/analyzer.rb +8 -4
- data/lib/active_storage/attached/changes/create_many.rb +7 -3
- data/lib/active_storage/attached/changes/create_one.rb +1 -1
- data/lib/active_storage/attached/changes/create_one_of_many.rb +1 -1
- data/lib/active_storage/attached/changes/delete_many.rb +1 -1
- data/lib/active_storage/attached/changes/delete_one.rb +1 -1
- data/lib/active_storage/attached/changes/detach_many.rb +18 -0
- data/lib/active_storage/attached/changes/detach_one.rb +24 -0
- data/lib/active_storage/attached/changes/purge_many.rb +27 -0
- data/lib/active_storage/attached/changes/purge_one.rb +27 -0
- data/lib/active_storage/attached/changes.rb +7 -1
- data/lib/active_storage/attached/many.rb +27 -15
- data/lib/active_storage/attached/model.rb +35 -7
- data/lib/active_storage/attached/one.rb +32 -27
- data/lib/active_storage/downloader.rb +4 -4
- data/lib/active_storage/engine.rb +45 -1
- data/lib/active_storage/fixture_set.rb +76 -0
- data/lib/active_storage/gem_version.rb +4 -4
- data/lib/active_storage/previewer.rb +4 -4
- data/lib/active_storage/reflection.rb +12 -2
- data/lib/active_storage/service/azure_storage_service.rb +28 -6
- data/lib/active_storage/service/configurator.rb +1 -1
- data/lib/active_storage/service/disk_service.rb +24 -19
- data/lib/active_storage/service/gcs_service.rb +109 -11
- data/lib/active_storage/service/mirror_service.rb +2 -2
- data/lib/active_storage/service/registry.rb +1 -1
- data/lib/active_storage/service/s3_service.rb +37 -15
- data/lib/active_storage/service.rb +13 -5
- data/lib/active_storage/transformers/image_processing_transformer.rb +1 -1
- data/lib/active_storage/transformers/transformer.rb +1 -1
- data/lib/active_storage/version.rb +1 -1
- data/lib/active_storage.rb +4 -0
- metadata +24 -14
- data/app/controllers/concerns/active_storage/set_headers.rb +0 -12
@@ -7,13 +7,4 @@ class ActiveStorage::BaseController < ActionController::Base
|
|
7
7
|
protect_from_forgery with: :exception
|
8
8
|
|
9
9
|
self.etag_with_template_digest = false
|
10
|
-
|
11
|
-
private
|
12
|
-
def stream(blob)
|
13
|
-
blob.download do |chunk|
|
14
|
-
response.stream.write chunk
|
15
|
-
end
|
16
|
-
ensure
|
17
|
-
response.stream.close
|
18
|
-
end
|
19
10
|
end
|
@@ -1,14 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Proxy files through application. This avoids having a redirect and makes files easier to cache.
|
4
|
+
#
|
5
|
+
# WARNING: All Active Storage controllers are publicly accessible by default. The
|
6
|
+
# generated URLs are hard to guess, but permanent by design. If your files
|
7
|
+
# require a higher level of protection consider implementing
|
8
|
+
# {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
|
4
9
|
class ActiveStorage::Blobs::ProxyController < ActiveStorage::BaseController
|
5
10
|
include ActiveStorage::SetBlob
|
6
|
-
include ActiveStorage::
|
11
|
+
include ActiveStorage::Streaming
|
7
12
|
|
8
13
|
def show
|
9
|
-
|
10
|
-
|
11
|
-
|
14
|
+
if request.headers["Range"].present?
|
15
|
+
send_blob_byte_range_data @blob, request.headers["Range"]
|
16
|
+
else
|
17
|
+
http_cache_forever public: true do
|
18
|
+
response.headers["Accept-Ranges"] = "bytes"
|
19
|
+
response.headers["Content-Length"] = @blob.byte_size.to_s
|
20
|
+
|
21
|
+
send_blob_stream @blob, disposition: params[:disposition]
|
22
|
+
end
|
12
23
|
end
|
13
24
|
end
|
14
25
|
end
|
@@ -1,14 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Take a signed permanent reference for a blob and turn it into an expiring service URL for download.
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
4
|
+
#
|
5
|
+
# WARNING: All Active Storage controllers are publicly accessible by default. The
|
6
|
+
# generated URLs are hard to guess, but permanent by design. If your files
|
7
|
+
# require a higher level of protection consider implementing
|
8
|
+
# {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
|
7
9
|
class ActiveStorage::Blobs::RedirectController < ActiveStorage::BaseController
|
8
10
|
include ActiveStorage::SetBlob
|
9
11
|
|
10
12
|
def show
|
11
13
|
expires_in ActiveStorage.service_urls_expire_in
|
12
|
-
redirect_to @blob.url(disposition: params[:disposition])
|
14
|
+
redirect_to @blob.url(disposition: params[:disposition]), allow_other_host: true
|
13
15
|
end
|
14
16
|
end
|
@@ -23,6 +23,7 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
|
|
23
23
|
if token = decode_verified_token
|
24
24
|
if acceptable_content?(token)
|
25
25
|
named_disk_service(token[:service_name]).upload token[:key], request.body, checksum: token[:checksum]
|
26
|
+
head :no_content
|
26
27
|
else
|
27
28
|
head :unprocessable_entity
|
28
29
|
end
|
@@ -1,11 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class ActiveStorage::Representations::BaseController < ActiveStorage::BaseController
|
3
|
+
class ActiveStorage::Representations::BaseController < ActiveStorage::BaseController # :nodoc:
|
4
4
|
include ActiveStorage::SetBlob
|
5
5
|
|
6
6
|
before_action :set_representation
|
7
7
|
|
8
8
|
private
|
9
|
+
def blob_scope
|
10
|
+
ActiveStorage::Blob.scope_for_strict_loading
|
11
|
+
end
|
12
|
+
|
9
13
|
def set_representation
|
10
14
|
@representation = @blob.representation(params[:variation_key]).processed
|
11
15
|
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
@@ -1,13 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Proxy files through application. This avoids having a redirect and makes files easier to cache.
|
4
|
+
#
|
5
|
+
# WARNING: All Active Storage controllers are publicly accessible by default. The
|
6
|
+
# generated URLs are hard to guess, but permanent by design. If your files
|
7
|
+
# require a higher level of protection consider implementing
|
8
|
+
# {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
|
4
9
|
class ActiveStorage::Representations::ProxyController < ActiveStorage::Representations::BaseController
|
5
|
-
include ActiveStorage::
|
10
|
+
include ActiveStorage::Streaming
|
6
11
|
|
7
12
|
def show
|
8
13
|
http_cache_forever public: true do
|
9
|
-
|
10
|
-
stream @representation
|
14
|
+
send_blob_stream @representation.image, disposition: params[:disposition]
|
11
15
|
end
|
12
16
|
end
|
13
17
|
end
|
@@ -1,12 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Take a signed permanent reference for a blob representation and turn it into an expiring service URL for download.
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
4
|
+
#
|
5
|
+
# WARNING: All Active Storage controllers are publicly accessible by default. The
|
6
|
+
# generated URLs are hard to guess, but permanent by design. If your files
|
7
|
+
# require a higher level of protection consider implementing
|
8
|
+
# {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
|
7
9
|
class ActiveStorage::Representations::RedirectController < ActiveStorage::Representations::BaseController
|
8
10
|
def show
|
9
11
|
expires_in ActiveStorage.service_urls_expire_in
|
10
|
-
redirect_to @representation.url(disposition: params[:disposition])
|
12
|
+
redirect_to @representation.url(disposition: params[:disposition]), allow_other_host: true
|
11
13
|
end
|
12
14
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module ActiveStorage::SetBlob
|
3
|
+
module ActiveStorage::SetBlob # :nodoc:
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
6
|
included do
|
@@ -9,8 +9,12 @@ module ActiveStorage::SetBlob #:nodoc:
|
|
9
9
|
|
10
10
|
private
|
11
11
|
def set_blob
|
12
|
-
@blob =
|
12
|
+
@blob = blob_scope.find_signed!(params[:signed_blob_id] || params[:signed_id])
|
13
13
|
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
14
14
|
head :not_found
|
15
15
|
end
|
16
|
+
|
17
|
+
def blob_scope
|
18
|
+
ActiveStorage::Blob
|
19
|
+
end
|
16
20
|
end
|
@@ -1,15 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Sets the <tt>ActiveStorage::Current.
|
3
|
+
# Sets the <tt>ActiveStorage::Current.url_options</tt> attribute, which the disk service uses to generate URLs.
|
4
4
|
# Include this concern in custom controllers that call ActiveStorage::Blob#url,
|
5
5
|
# ActiveStorage::Variant#url, or ActiveStorage::Preview#url so the disk service can
|
6
|
-
# generate URLs using the same host, protocol, and
|
6
|
+
# generate URLs using the same host, protocol, and port as the current request.
|
7
7
|
module ActiveStorage::SetCurrent
|
8
8
|
extend ActiveSupport::Concern
|
9
9
|
|
10
10
|
included do
|
11
11
|
before_action do
|
12
|
-
ActiveStorage::Current.
|
12
|
+
ActiveStorage::Current.url_options = { protocol: request.protocol, host: request.host, port: request.port }
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
module ActiveStorage::Streaming
|
6
|
+
DEFAULT_BLOB_STREAMING_DISPOSITION = "inline"
|
7
|
+
|
8
|
+
include ActionController::DataStreaming
|
9
|
+
include ActionController::Live
|
10
|
+
|
11
|
+
private
|
12
|
+
# Stream the blob in byte ranges specified through the header
|
13
|
+
def send_blob_byte_range_data(blob, range_header, disposition: nil)
|
14
|
+
ranges = Rack::Utils.get_byte_ranges(range_header, blob.byte_size)
|
15
|
+
|
16
|
+
return head(:range_not_satisfiable) if ranges.blank? || ranges.all?(&:blank?)
|
17
|
+
|
18
|
+
if ranges.length == 1
|
19
|
+
range = ranges.first
|
20
|
+
content_type = blob.content_type_for_serving
|
21
|
+
data = blob.download_chunk(range)
|
22
|
+
|
23
|
+
response.headers["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{blob.byte_size}"
|
24
|
+
else
|
25
|
+
boundary = SecureRandom.hex
|
26
|
+
content_type = "multipart/byteranges; boundary=#{boundary}"
|
27
|
+
data = +""
|
28
|
+
|
29
|
+
ranges.compact.each do |range|
|
30
|
+
chunk = blob.download_chunk(range)
|
31
|
+
|
32
|
+
data << "\r\n--#{boundary}\r\n"
|
33
|
+
data << "Content-Type: #{blob.content_type_for_serving}\r\n"
|
34
|
+
data << "Content-Range: bytes #{range.begin}-#{range.end}/#{blob.byte_size}\r\n\r\n"
|
35
|
+
data << chunk
|
36
|
+
end
|
37
|
+
|
38
|
+
data << "\r\n--#{boundary}--\r\n"
|
39
|
+
end
|
40
|
+
|
41
|
+
response.headers["Accept-Ranges"] = "bytes"
|
42
|
+
response.headers["Content-Length"] = data.length.to_s
|
43
|
+
|
44
|
+
send_data(
|
45
|
+
data,
|
46
|
+
disposition: blob.forced_disposition_for_serving || disposition || DEFAULT_BLOB_STREAMING_DISPOSITION,
|
47
|
+
filename: blob.filename.sanitized,
|
48
|
+
status: :partial_content,
|
49
|
+
type: content_type
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Stream the blob from storage directly to the response. The disposition can be controlled by setting +disposition+.
|
54
|
+
# The content type and filename is set directly from the +blob+.
|
55
|
+
def send_blob_stream(blob, disposition: nil) # :doc:
|
56
|
+
send_stream(
|
57
|
+
filename: blob.filename.sanitized,
|
58
|
+
disposition: blob.forced_disposition_for_serving || disposition || DEFAULT_BLOB_STREAMING_DISPOSITION,
|
59
|
+
type: blob.content_type_for_serving) do |stream|
|
60
|
+
blob.download do |chunk|
|
61
|
+
stream.write chunk
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -9,7 +9,7 @@ export function start() {
|
|
9
9
|
if (!started) {
|
10
10
|
started = true
|
11
11
|
document.addEventListener("click", didClick, true)
|
12
|
-
document.addEventListener("submit", didSubmitForm)
|
12
|
+
document.addEventListener("submit", didSubmitForm, true)
|
13
13
|
document.addEventListener("ajax:before", didSubmitRemoteElement)
|
14
14
|
}
|
15
15
|
}
|
@@ -7,6 +7,14 @@ require "active_support/core_ext/module/delegation"
|
|
7
7
|
# on the attachments table prevents blobs from being purged if they’re still attached to any records.
|
8
8
|
#
|
9
9
|
# Attachments also have access to all methods from {ActiveStorage::Blob}[rdoc-ref:ActiveStorage::Blob].
|
10
|
+
#
|
11
|
+
# If you wish to preload attachments or blobs, you can use these scopes:
|
12
|
+
#
|
13
|
+
# # preloads attachments, their corresponding blobs, and variant records (if using `ActiveStorage.track_variants`)
|
14
|
+
# User.all.with_attached_avatars
|
15
|
+
#
|
16
|
+
# # preloads blobs and variant records (if using `ActiveStorage.track_variants`)
|
17
|
+
# User.first.avatars.with_all_variant_records
|
10
18
|
class ActiveStorage::Attachment < ActiveStorage::Record
|
11
19
|
self.table_name = "active_storage_attachments"
|
12
20
|
|
@@ -19,11 +27,13 @@ class ActiveStorage::Attachment < ActiveStorage::Record
|
|
19
27
|
after_create_commit :mirror_blob_later, :analyze_blob_later
|
20
28
|
after_destroy_commit :purge_dependent_blob_later
|
21
29
|
|
30
|
+
scope :with_all_variant_records, -> { includes(blob: :variant_records) }
|
31
|
+
|
22
32
|
# Synchronously deletes the attachment and {purges the blob}[rdoc-ref:ActiveStorage::Blob#purge].
|
23
33
|
def purge
|
24
34
|
transaction do
|
25
35
|
delete
|
26
|
-
record&.
|
36
|
+
record.touch if record&.persisted?
|
27
37
|
end
|
28
38
|
blob&.purge
|
29
39
|
end
|
@@ -32,11 +42,30 @@ class ActiveStorage::Attachment < ActiveStorage::Record
|
|
32
42
|
def purge_later
|
33
43
|
transaction do
|
34
44
|
delete
|
35
|
-
record&.
|
45
|
+
record.touch if record&.persisted?
|
36
46
|
end
|
37
47
|
blob&.purge_later
|
38
48
|
end
|
39
49
|
|
50
|
+
# Returns an ActiveStorage::Variant or ActiveStorage::VariantWithRecord
|
51
|
+
# instance for the attachment with the set of +transformations+ provided.
|
52
|
+
# See ActiveStorage::Blob::Representable#variant for more information.
|
53
|
+
#
|
54
|
+
# Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
|
55
|
+
# unknown pre-defined variant of the attachment.
|
56
|
+
def variant(transformations)
|
57
|
+
case transformations
|
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
|
+
|
66
|
+
blob.variant(transformations)
|
67
|
+
end
|
68
|
+
|
40
69
|
private
|
41
70
|
def analyze_blob_later
|
42
71
|
blob.analyze_later unless blob.analyzed?
|
@@ -53,6 +82,10 @@ class ActiveStorage::Attachment < ActiveStorage::Record
|
|
53
82
|
def dependent
|
54
83
|
record.attachment_reflections[name]&.options&.fetch(:dependent, nil)
|
55
84
|
end
|
85
|
+
|
86
|
+
def variants
|
87
|
+
record.attachment_reflections[name]&.variants
|
88
|
+
end
|
56
89
|
end
|
57
90
|
|
58
91
|
ActiveSupport.run_load_hooks :active_storage_attachment, ActiveStorage::Attachment
|
@@ -12,8 +12,8 @@ module ActiveStorage::Blob::Representable
|
|
12
12
|
has_one_attached :preview_image
|
13
13
|
end
|
14
14
|
|
15
|
-
# Returns an ActiveStorage::Variant instance with the set of +transformations+ provided.
|
16
|
-
# files, and it allows any image to be transformed for size, colors, and the like. Example:
|
15
|
+
# Returns an ActiveStorage::Variant or ActiveStorage::VariantWithRecord instance with the set of +transformations+ provided.
|
16
|
+
# This is only relevant for image files, and it allows any image to be transformed for size, colors, and the like. Example:
|
17
17
|
#
|
18
18
|
# avatar.variant(resize_to_limit: [100, 100]).processed.url
|
19
19
|
#
|
@@ -28,8 +28,9 @@ module ActiveStorage::Blob::Representable
|
|
28
28
|
# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
|
29
29
|
# can then produce on-demand.
|
30
30
|
#
|
31
|
-
# Raises ActiveStorage::InvariableError if
|
32
|
-
# variable, call
|
31
|
+
# Raises ActiveStorage::InvariableError if the variant processor cannot
|
32
|
+
# transform the blob. To determine whether a blob is variable, call
|
33
|
+
# ActiveStorage::Blob#variable?.
|
33
34
|
def variant(transformations)
|
34
35
|
if variable?
|
35
36
|
variant_class.new(self, ActiveStorage::Variation.wrap(transformations).default_to(default_variant_transformations))
|
@@ -38,7 +39,8 @@ module ActiveStorage::Blob::Representable
|
|
38
39
|
end
|
39
40
|
end
|
40
41
|
|
41
|
-
# Returns true if
|
42
|
+
# Returns true if the variant processor can transform the blob (its content
|
43
|
+
# type is in +ActiveStorage.variable_content_types+).
|
42
44
|
def variable?
|
43
45
|
ActiveStorage.variable_content_types.include?(content_type)
|
44
46
|
end
|
@@ -39,7 +39,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
39
39
|
MINIMUM_TOKEN_LENGTH = 28
|
40
40
|
|
41
41
|
has_secure_token :key, length: MINIMUM_TOKEN_LENGTH
|
42
|
-
store :metadata, accessors: [ :analyzed, :identified ], coder: ActiveRecord::Coders::JSON
|
42
|
+
store :metadata, accessors: [ :analyzed, :identified, :composed ], coder: ActiveRecord::Coders::JSON
|
43
43
|
|
44
44
|
class_attribute :services, default: {}
|
45
45
|
class_attribute :service, instance_accessor: false
|
@@ -52,13 +52,14 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
52
52
|
self.service_name ||= self.class.service&.name
|
53
53
|
end
|
54
54
|
|
55
|
-
after_update_commit :update_service_metadata, if:
|
55
|
+
after_update_commit :update_service_metadata, if: -> { content_type_previously_changed? || metadata_previously_changed? }
|
56
56
|
|
57
57
|
before_destroy(prepend: true) do
|
58
58
|
raise ActiveRecord::InvalidForeignKey if attachments.exists?
|
59
59
|
end
|
60
60
|
|
61
61
|
validates :service_name, presence: true
|
62
|
+
validates :checksum, presence: true, unless: :composed
|
62
63
|
|
63
64
|
validate do
|
64
65
|
if service_name_changed? && service_name.present?
|
@@ -86,21 +87,13 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
86
87
|
super(id, purpose: purpose)
|
87
88
|
end
|
88
89
|
|
89
|
-
def
|
90
|
-
new(filename: filename, content_type: content_type, metadata: metadata, service_name: service_name).tap do |blob|
|
91
|
-
blob.upload(io, identify: identify)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
deprecate :build_after_upload
|
96
|
-
|
97
|
-
def build_after_unfurling(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) #:nodoc:
|
90
|
+
def build_after_unfurling(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) # :nodoc:
|
98
91
|
new(key: key, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name).tap do |blob|
|
99
92
|
blob.unfurl(io, identify: identify)
|
100
93
|
end
|
101
94
|
end
|
102
95
|
|
103
|
-
def create_after_unfurling!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil)
|
96
|
+
def create_after_unfurling!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) # :nodoc:
|
104
97
|
build_after_unfurling(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify).tap(&:save!)
|
105
98
|
end
|
106
99
|
|
@@ -115,9 +108,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
115
108
|
end
|
116
109
|
end
|
117
110
|
|
118
|
-
alias_method :create_after_upload!, :create_and_upload!
|
119
|
-
deprecate create_after_upload!: :create_and_upload!
|
120
|
-
|
121
111
|
# Returns a saved blob _without_ uploading a file to the service. This blob will point to a key where there is
|
122
112
|
# no file yet. It's intended to be used together with a client-side upload, which will first create the blob
|
123
113
|
# in order to produce the signed URL for uploading. This signed URL points to the key generated by the blob.
|
@@ -137,7 +127,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
137
127
|
end
|
138
128
|
|
139
129
|
# Customize signed ID purposes for backwards compatibility.
|
140
|
-
def combine_signed_id_purposes(purpose)
|
130
|
+
def combine_signed_id_purposes(purpose) # :nodoc:
|
141
131
|
purpose.to_s
|
142
132
|
end
|
143
133
|
|
@@ -145,14 +135,34 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
145
135
|
#
|
146
136
|
# We override the reader (.signed_id_verifier) instead of just calling the writer (.signed_id_verifier=)
|
147
137
|
# to guard against the case where ActiveStorage.verifier isn't yet initialized at load time.
|
148
|
-
def signed_id_verifier
|
138
|
+
def signed_id_verifier # :nodoc:
|
149
139
|
@signed_id_verifier ||= ActiveStorage.verifier
|
150
140
|
end
|
141
|
+
|
142
|
+
def scope_for_strict_loading # :nodoc:
|
143
|
+
if strict_loading_by_default? && ActiveStorage.track_variants
|
144
|
+
includes(variant_records: { image_attachment: :blob }, preview_image_attachment: :blob)
|
145
|
+
else
|
146
|
+
all
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Concatenate multiple blobs into a single "composed" blob.
|
151
|
+
def compose(blobs, filename:, content_type: nil, metadata: nil)
|
152
|
+
raise ActiveRecord::RecordNotSaved, "All blobs must be persisted." if blobs.any?(&:new_record?)
|
153
|
+
|
154
|
+
content_type ||= blobs.pluck(:content_type).compact.first
|
155
|
+
|
156
|
+
new(filename: filename, content_type: content_type, metadata: metadata, byte_size: blobs.sum(&:byte_size)).tap do |combined_blob|
|
157
|
+
combined_blob.compose(blobs.pluck(:key))
|
158
|
+
combined_blob.save!
|
159
|
+
end
|
160
|
+
end
|
151
161
|
end
|
152
162
|
|
153
163
|
# Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering.
|
154
|
-
def signed_id
|
155
|
-
super
|
164
|
+
def signed_id(purpose: :blob_id, expires_in: nil)
|
165
|
+
super
|
156
166
|
end
|
157
167
|
|
158
168
|
# Returns the key pointing to the file on the service that's associated with this blob. The key is the
|
@@ -171,6 +181,14 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
171
181
|
ActiveStorage::Filename.new(self[:filename])
|
172
182
|
end
|
173
183
|
|
184
|
+
def custom_metadata
|
185
|
+
self[:metadata][:custom] || {}
|
186
|
+
end
|
187
|
+
|
188
|
+
def custom_metadata=(metadata)
|
189
|
+
self[:metadata] = self[:metadata].merge(custom: metadata)
|
190
|
+
end
|
191
|
+
|
174
192
|
# Returns true if the content_type of this blob is in the image range, like image/png.
|
175
193
|
def image?
|
176
194
|
content_type.start_with?("image")
|
@@ -200,25 +218,22 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
200
218
|
content_type: content_type_for_serving, disposition: forced_disposition_for_serving || disposition, **options
|
201
219
|
end
|
202
220
|
|
203
|
-
alias_method :service_url, :url
|
204
|
-
deprecate service_url: :url
|
205
|
-
|
206
221
|
# Returns a URL that can be used to directly upload a file for this blob on the service. This URL is intended to be
|
207
222
|
# short-lived for security and only generated on-demand by the client-side JavaScript responsible for doing the uploading.
|
208
223
|
def service_url_for_direct_upload(expires_in: ActiveStorage.service_urls_expire_in)
|
209
|
-
service.url_for_direct_upload key, expires_in: expires_in, content_type: content_type, content_length: byte_size, checksum: checksum
|
224
|
+
service.url_for_direct_upload key, expires_in: expires_in, content_type: content_type, content_length: byte_size, checksum: checksum, custom_metadata: custom_metadata
|
210
225
|
end
|
211
226
|
|
212
227
|
# Returns a Hash of headers for +service_url_for_direct_upload+ requests.
|
213
228
|
def service_headers_for_direct_upload
|
214
|
-
service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum
|
229
|
+
service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum, custom_metadata: custom_metadata
|
215
230
|
end
|
216
231
|
|
217
|
-
def content_type_for_serving
|
232
|
+
def content_type_for_serving # :nodoc:
|
218
233
|
forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
|
219
234
|
end
|
220
235
|
|
221
|
-
def forced_disposition_for_serving
|
236
|
+
def forced_disposition_for_serving # :nodoc:
|
222
237
|
if forcibly_serve_as_binary? || !allowed_inline?
|
223
238
|
:attachment
|
224
239
|
end
|
@@ -242,23 +257,33 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
242
257
|
upload_without_unfurling io
|
243
258
|
end
|
244
259
|
|
245
|
-
def unfurl(io, identify: true)
|
260
|
+
def unfurl(io, identify: true) # :nodoc:
|
246
261
|
self.checksum = compute_checksum_in_chunks(io)
|
247
262
|
self.content_type = extract_content_type(io) if content_type.nil? || identify
|
248
263
|
self.byte_size = io.size
|
249
264
|
self.identified = true
|
250
265
|
end
|
251
266
|
|
252
|
-
def upload_without_unfurling(io)
|
267
|
+
def upload_without_unfurling(io) # :nodoc:
|
253
268
|
service.upload key, io, checksum: checksum, **service_metadata
|
254
269
|
end
|
255
270
|
|
271
|
+
def compose(keys) # :nodoc:
|
272
|
+
self.composed = true
|
273
|
+
service.compose(keys, key, **service_metadata)
|
274
|
+
end
|
275
|
+
|
256
276
|
# Downloads the file associated with this blob. If no block is given, the entire file is read into memory and returned.
|
257
277
|
# That'll use a lot of RAM for very large files. If a block is given, then the download is streamed and yielded in chunks.
|
258
278
|
def download(&block)
|
259
279
|
service.download key, &block
|
260
280
|
end
|
261
281
|
|
282
|
+
# Downloads a part of the file associated with this blob.
|
283
|
+
def download_chunk(range)
|
284
|
+
service.download_chunk key, range
|
285
|
+
end
|
286
|
+
|
262
287
|
# Downloads the blob to a tempfile on disk. Yields the tempfile.
|
263
288
|
#
|
264
289
|
# The tempfile's name is prefixed with +ActiveStorage-+ and the blob's ID. Its extension matches that of the blob.
|
@@ -273,11 +298,17 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
273
298
|
#
|
274
299
|
# Raises ActiveStorage::IntegrityError if the downloaded data does not match the blob's checksum.
|
275
300
|
def open(tmpdir: nil, &block)
|
276
|
-
service.open
|
277
|
-
|
301
|
+
service.open(
|
302
|
+
key,
|
303
|
+
checksum: checksum,
|
304
|
+
verify: !composed,
|
305
|
+
name: [ "ActiveStorage-#{id}-", filename.extension_with_delimiter ],
|
306
|
+
tmpdir: tmpdir,
|
307
|
+
&block
|
308
|
+
)
|
278
309
|
end
|
279
310
|
|
280
|
-
def mirror_later
|
311
|
+
def mirror_later # :nodoc:
|
281
312
|
ActiveStorage::MirrorJob.perform_later(key, checksum: checksum) if service.respond_to?(:mirror)
|
282
313
|
end
|
283
314
|
|
@@ -294,7 +325,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
294
325
|
# be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use #purge_later instead.
|
295
326
|
def purge
|
296
327
|
destroy
|
297
|
-
delete
|
328
|
+
delete if previously_persisted?
|
298
329
|
rescue ActiveRecord::InvalidForeignKey
|
299
330
|
end
|
300
331
|
|
@@ -309,9 +340,34 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
309
340
|
services.fetch(service_name)
|
310
341
|
end
|
311
342
|
|
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
|
+
|
312
368
|
private
|
313
369
|
def compute_checksum_in_chunks(io)
|
314
|
-
Digest::MD5.new.tap do |checksum|
|
370
|
+
OpenSSL::Digest::MD5.new.tap do |checksum|
|
315
371
|
while chunk = io.read(5.megabytes)
|
316
372
|
checksum << chunk
|
317
373
|
end
|
@@ -338,11 +394,11 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
338
394
|
|
339
395
|
def service_metadata
|
340
396
|
if forcibly_serve_as_binary?
|
341
|
-
{ content_type: ActiveStorage.binary_content_type, disposition: :attachment, filename: filename }
|
397
|
+
{ content_type: ActiveStorage.binary_content_type, disposition: :attachment, filename: filename, custom_metadata: custom_metadata }
|
342
398
|
elsif !allowed_inline?
|
343
|
-
{ content_type: content_type, disposition: :attachment, filename: filename }
|
399
|
+
{ content_type: content_type, disposition: :attachment, filename: filename, custom_metadata: custom_metadata }
|
344
400
|
else
|
345
|
-
{ content_type: content_type }
|
401
|
+
{ content_type: content_type, custom_metadata: custom_metadata }
|
346
402
|
end
|
347
403
|
end
|
348
404
|
|
@@ -1,5 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class ActiveStorage::Current < ActiveSupport::CurrentAttributes
|
4
|
-
attribute :
|
3
|
+
class ActiveStorage::Current < ActiveSupport::CurrentAttributes # :nodoc:
|
4
|
+
attribute :url_options
|
5
|
+
|
6
|
+
def host=(host)
|
7
|
+
ActiveSupport::Deprecation.warn("ActiveStorage::Current.host= is deprecated, instead use ActiveStorage::Current.url_options=")
|
8
|
+
self.url_options = { host: host }
|
9
|
+
end
|
10
|
+
|
11
|
+
def host
|
12
|
+
ActiveSupport::Deprecation.warn("ActiveStorage::Current.host is deprecated, instead use ActiveStorage::Current.url_options")
|
13
|
+
self.url_options&.dig(:host)
|
14
|
+
end
|
5
15
|
end
|