aboutsummaryrefslogtreecommitdiffstats
path: root/activestorage/app/models/active_storage/blob.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activestorage/app/models/active_storage/blob.rb')
-rw-r--r--activestorage/app/models/active_storage/blob.rb91
1 files changed, 74 insertions, 17 deletions
diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb
index b8b3b62f22..04f9dbff9f 100644
--- a/activestorage/app/models/active_storage/blob.rb
+++ b/activestorage/app/models/active_storage/blob.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_storage/downloader"
+
# A blob is a record that contains the metadata about a file and a key for where that file resides on the service.
# Blobs can be created in two ways:
#
@@ -33,8 +35,12 @@ class ActiveStorage::Blob < ActiveRecord::Base
scope :unattached, -> { left_joins(:attachments).where(ActiveStorage::Attachment.table_name => { blob_id: nil }) }
+ before_destroy(prepend: true) do
+ raise ActiveRecord::InvalidForeignKey if attachments.exists?
+ end
+
class << self
- # You can used the signed ID of a blob to refer to it on the client side without fear of tampering.
+ # You can use the signed ID of a blob to refer to it on the client side without fear of tampering.
# This is particularly helpful for direct uploads where the client-side needs to refer to the blob
# that was created ahead of the upload itself on form submission.
#
@@ -46,15 +52,17 @@ class ActiveStorage::Blob < ActiveRecord::Base
# Returns a new, unsaved blob instance after the +io+ has been uploaded to the service.
# When providing a content type, pass <tt>identify: false</tt> to bypass automatic content type inference.
def build_after_upload(io:, filename:, content_type: nil, metadata: nil, identify: true)
- new.tap do |blob|
- blob.filename = filename
- blob.content_type = content_type
- blob.metadata = metadata
-
+ new(filename: filename, content_type: content_type, metadata: metadata).tap do |blob|
blob.upload(io, identify: identify)
end
end
+ def build_after_unfurling(io:, filename:, content_type: nil, metadata: nil, identify: true) #:nodoc:
+ new(filename: filename, content_type: content_type, metadata: metadata).tap do |blob|
+ blob.unfurl(io, identify: identify)
+ end
+ end
+
# Returns a saved blob instance after the +io+ has been uploaded to the service. Note, the blob is first built,
# then the +io+ is uploaded, then the blob is saved. This is done this way to avoid uploading (which may take
# time), while having an open database transaction.
@@ -119,16 +127,16 @@ class ActiveStorage::Blob < ActiveRecord::Base
# with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL.
# Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And
# it allows permanent URLs that redirect to the +service_url+ to be cached in the view.
- def service_url(expires_in: service.url_expires_in, disposition: :inline, filename: nil, **options)
+ def service_url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline, filename: nil, **options)
filename = ActiveStorage::Filename.wrap(filename || self.filename)
- service.url key, expires_in: expires_in, filename: filename, content_type: content_type,
- disposition: forcibly_serve_as_binary? ? :attachment : disposition, **options
+ service.url key, expires_in: expires_in, filename: filename, content_type: content_type_for_service_url,
+ disposition: forced_disposition_for_service_url || disposition, **options
end
# Returns a URL that can be used to directly upload a file for this blob on the service. This URL is intended to be
# short-lived for security and only generated on-demand by the client-side JavaScript responsible for doing the uploading.
- def service_url_for_direct_upload(expires_in: service.url_expires_in)
+ def service_url_for_direct_upload(expires_in: ActiveStorage.service_urls_expire_in)
service.url_for_direct_upload key, expires_in: expires_in, content_type: content_type, content_length: byte_size, checksum: checksum
end
@@ -150,12 +158,19 @@ class ActiveStorage::Blob < ActiveRecord::Base
# Normally, you do not have to call this method directly at all. Use the factory class methods of +build_after_upload+
# and +create_after_upload!+.
def upload(io, identify: true)
+ unfurl io, identify: identify
+ upload_without_unfurling io
+ end
+
+ def unfurl(io, identify: true) #:nodoc:
self.checksum = compute_checksum_in_chunks(io)
self.content_type = extract_content_type(io) if content_type.nil? || identify
self.byte_size = io.size
self.identified = true
+ end
- service.upload(key, io, checksum: checksum)
+ def upload_without_unfurling(io) #:nodoc:
+ service.upload key, io, checksum: checksum, **service_metadata
end
# Downloads the file associated with this blob. If no block is given, the entire file is read into memory and returned.
@@ -164,9 +179,26 @@ class ActiveStorage::Blob < ActiveRecord::Base
service.download key, &block
end
+ # Downloads the blob to a tempfile on disk. Yields the tempfile.
+ #
+ # The tempfile's name is prefixed with +ActiveStorage-+ and the blob's ID. Its extension matches that of the blob.
+ #
+ # By default, the tempfile is created in <tt>Dir.tmpdir</tt>. Pass +tempdir:+ to create it in a different directory:
+ #
+ # blob.open(tempdir: "/path/to/tmp") do |file|
+ # # ...
+ # end
+ #
+ # The tempfile is automatically closed and unlinked after the given block is executed.
+ #
+ # Raises ActiveStorage::IntegrityError if the downloaded data does not match the blob's checksum.
+ def open(tempdir: nil, &block)
+ ActiveStorage::Downloader.new(self, tempdir: tempdir).download_blob_to_tempfile(&block)
+ end
+
- # Deletes the file on the service that's associated with this blob. This should only be done if the blob is going to be
- # deleted as well or you will essentially have a dead reference. It's recommended to use the +#purge+ and +#purge_later+
+ # Deletes the files on the service associated with the blob. This should only be done if the blob is going to be
+ # deleted as well or you will essentially have a dead reference. It's recommended to use #purge and #purge_later
# methods in most circumstances.
def delete
service.delete(key)
@@ -175,14 +207,15 @@ class ActiveStorage::Blob < ActiveRecord::Base
# Deletes the file on the service and then destroys the blob record. This is the recommended way to dispose of unwanted
# blobs. Note, though, that deleting the file off the service will initiate a HTTP connection to the service, which may
- # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use +#purge_later+ instead.
+ # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use #purge_later instead.
def purge
- delete
destroy
+ delete
+ rescue ActiveRecord::InvalidForeignKey
end
- # Enqueues an ActiveStorage::PurgeJob job that'll call +purge+. This is the recommended way to purge blobs when the call
- # needs to be made from a transaction, a callback, or any other real-time scenario.
+ # Enqueues an ActiveStorage::PurgeJob to call #purge. This is the recommended way to purge blobs from a transaction,
+ # an Active Record callback, or in any other real-time scenario.
def purge_later
ActiveStorage::PurgeJob.perform_later(self)
end
@@ -206,5 +239,29 @@ class ActiveStorage::Blob < ActiveRecord::Base
ActiveStorage.content_types_to_serve_as_binary.include?(content_type)
end
+ def allowed_inline?
+ ActiveStorage.content_types_allowed_inline.include?(content_type)
+ end
+
+ def content_type_for_service_url
+ forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
+ end
+
+ def forced_disposition_for_service_url
+ if forcibly_serve_as_binary? || !allowed_inline?
+ :attachment
+ end
+ end
+
+ def service_metadata
+ if forcibly_serve_as_binary?
+ { content_type: ActiveStorage.binary_content_type, disposition: :attachment, filename: filename }
+ elsif !allowed_inline?
+ { content_type: content_type, disposition: :attachment, filename: filename }
+ else
+ { content_type: content_type }
+ end
+ end
+
ActiveSupport.run_load_hooks(:active_storage_blob, self)
end