diff options
Diffstat (limited to 'activestorage/app')
14 files changed, 81 insertions, 30 deletions
diff --git a/activestorage/app/assets/javascripts/activestorage.js b/activestorage/app/assets/javascripts/activestorage.js index 375eb6b533..e2bcb520b9 100644 --- a/activestorage/app/assets/javascripts/activestorage.js +++ b/activestorage/app/assets/javascripts/activestorage.js @@ -560,7 +560,10 @@ this.xhr.setRequestHeader("Content-Type", "application/json"); this.xhr.setRequestHeader("Accept", "application/json"); this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); - this.xhr.setRequestHeader("X-CSRF-Token", getMetaValue("csrf-token")); + var csrfToken = getMetaValue("csrf-token"); + if (csrfToken != undefined) { + this.xhr.setRequestHeader("X-CSRF-Token", csrfToken); + } this.xhr.addEventListener("load", function(event) { return _this.requestDidLoad(event); }); @@ -867,7 +870,7 @@ } function didClick(event) { var target = event.target; - if (target.tagName == "INPUT" && target.type == "submit" && target.form) { + if ((target.tagName == "INPUT" || target.tagName == "BUTTON") && target.type == "submit" && target.form) { submitButtonsByForm.set(target.form, target); } } @@ -902,7 +905,7 @@ } } function submitForm(form) { - var button = submitButtonsByForm.get(form) || findElement(form, "input[type=submit]"); + var button = submitButtonsByForm.get(form) || findElement(form, "input[type=submit], button[type=submit]"); if (button) { var _button = button, disabled = _button.disabled; button.disabled = false; diff --git a/activestorage/app/controllers/active_storage/disk_controller.rb b/activestorage/app/controllers/active_storage/disk_controller.rb index 99982202dd..df8d73cc91 100644 --- a/activestorage/app/controllers/active_storage/disk_controller.rb +++ b/activestorage/app/controllers/active_storage/disk_controller.rb @@ -9,7 +9,7 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController def show if key = decode_verified_key - serve_file disk_service.path_for(key), content_type: params[:content_type], disposition: params[:disposition] + serve_file disk_service.path_for(key[:key]), content_type: key[:content_type], disposition: key[:disposition] else head :not_found end diff --git a/activestorage/app/javascript/activestorage/blob_record.js b/activestorage/app/javascript/activestorage/blob_record.js index ff847892b2..7fbe315f76 100644 --- a/activestorage/app/javascript/activestorage/blob_record.js +++ b/activestorage/app/javascript/activestorage/blob_record.js @@ -17,7 +17,12 @@ export class BlobRecord { this.xhr.setRequestHeader("Content-Type", "application/json") this.xhr.setRequestHeader("Accept", "application/json") this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest") - this.xhr.setRequestHeader("X-CSRF-Token", getMetaValue("csrf-token")) + + const csrfToken = getMetaValue("csrf-token") + if (csrfToken != undefined) { + this.xhr.setRequestHeader("X-CSRF-Token", csrfToken) + } + this.xhr.addEventListener("load", event => this.requestDidLoad(event)) this.xhr.addEventListener("error", event => this.requestDidError(event)) } diff --git a/activestorage/app/javascript/activestorage/ujs.js b/activestorage/app/javascript/activestorage/ujs.js index f5353389ef..98fcba60fa 100644 --- a/activestorage/app/javascript/activestorage/ujs.js +++ b/activestorage/app/javascript/activestorage/ujs.js @@ -16,7 +16,7 @@ export function start() { function didClick(event) { const { target } = event - if (target.tagName == "INPUT" && target.type == "submit" && target.form) { + if ((target.tagName == "INPUT" || target.tagName == "BUTTON") && target.type == "submit" && target.form) { submitButtonsByForm.set(target.form, target) } } @@ -58,7 +58,7 @@ function handleFormSubmissionEvent(event) { } function submitForm(form) { - let button = submitButtonsByForm.get(form) || findElement(form, "input[type=submit]") + let button = submitButtonsByForm.get(form) || findElement(form, "input[type=submit], button[type=submit]") if (button) { const { disabled } = button diff --git a/activestorage/app/jobs/active_storage/analyze_job.rb b/activestorage/app/jobs/active_storage/analyze_job.rb index 804ee4557a..35d043d508 100644 --- a/activestorage/app/jobs/active_storage/analyze_job.rb +++ b/activestorage/app/jobs/active_storage/analyze_job.rb @@ -2,6 +2,8 @@ # Provides asynchronous analysis of ActiveStorage::Blob records via ActiveStorage::Blob#analyze_later. class ActiveStorage::AnalyzeJob < ActiveStorage::BaseJob + queue_as { ActiveStorage.queues[:analysis] } + retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :exponentially_longer def perform(blob) diff --git a/activestorage/app/jobs/active_storage/base_job.rb b/activestorage/app/jobs/active_storage/base_job.rb index 6caab42a2d..7bc2064dc5 100644 --- a/activestorage/app/jobs/active_storage/base_job.rb +++ b/activestorage/app/jobs/active_storage/base_job.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true class ActiveStorage::BaseJob < ActiveJob::Base - queue_as { ActiveStorage.queue } end diff --git a/activestorage/app/jobs/active_storage/purge_job.rb b/activestorage/app/jobs/active_storage/purge_job.rb index 2604977bf1..5ceb222005 100644 --- a/activestorage/app/jobs/active_storage/purge_job.rb +++ b/activestorage/app/jobs/active_storage/purge_job.rb @@ -2,6 +2,8 @@ # Provides asynchronous purging of ActiveStorage::Blob records via ActiveStorage::Blob#purge_later. class ActiveStorage::PurgeJob < ActiveStorage::BaseJob + queue_as { ActiveStorage.queues[:purge] } + discard_on ActiveRecord::RecordNotFound retry_on ActiveRecord::Deadlocked, attempts: 10, wait: :exponentially_longer diff --git a/activestorage/app/models/active_storage/attachment.rb b/activestorage/app/models/active_storage/attachment.rb index 4bdd1c0224..13758d9179 100644 --- a/activestorage/app/models/active_storage/attachment.rb +++ b/activestorage/app/models/active_storage/attachment.rb @@ -3,9 +3,8 @@ require "active_support/core_ext/module/delegation" # Attachments associate records with blobs. Usually that's a one record-many blobs relationship, -# but it is possible to associate many different records with the same blob. If you're doing that, -# you'll want to declare with <tt>has_one/many_attached :thingy, dependent: false</tt>, so that destroying -# any one record won't destroy the blob as well. (Then you'll need to do your own garbage collecting, though). +# but it is possible to associate many different records with the same blob. A foreign-key constraint +# on the attachments table prevents blobs from being purged if they’re still attached to any records. class ActiveStorage::Attachment < ActiveRecord::Base self.table_name = "active_storage_attachments" diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb index 53aa9f0237..6ca7d49bc1 100644 --- a/activestorage/app/models/active_storage/blob.rb +++ b/activestorage/app/models/active_storage/blob.rb @@ -40,7 +40,7 @@ class ActiveStorage::Blob < ActiveRecord::Base 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. # @@ -79,6 +79,15 @@ class ActiveStorage::Blob < ActiveRecord::Base def create_before_direct_upload!(filename:, byte_size:, checksum:, content_type: nil, metadata: nil) create! filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata end + + # To prevent problems with case-insensitive filesystems, especially in combination + # with databases which treat indices as case-sensitive, all blob keys generated are going + # to only contain the base-36 character alphabet and will therefore be lowercase. To maintain + # the same or higher amount of entropy as in the base-58 encoding used by `has_secure_token` + # the number of bytes used is increased to 28 from the standard 24 + def generate_unique_secure_token + SecureRandom.base36(28) + end end # Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering. @@ -87,9 +96,10 @@ class ActiveStorage::Blob < ActiveRecord::Base ActiveStorage.verifier.generate(id, purpose: :blob_id) end - # Returns the key pointing to the file on the service that's associated with this blob. The key is in the - # standard secure-token format from Rails. So it'll look like: XTAPjJCJiuDrLk3TmwyJGpUo. This key is not intended - # to be revealed directly to the user. Always refer to blobs using the signed_id or a verified form of the key. + # Returns the key pointing to the file on the service that's associated with this blob. The key is the + # secure-token format from Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd. + # This key is not intended to be revealed directly to the user. + # Always refer to blobs using the signed_id or a verified form of the key. def key # We can't wait until the record is first saved to have a key for it self[:key] ||= self.class.generate_unique_secure_token @@ -130,8 +140,8 @@ class ActiveStorage::Blob < ActiveRecord::Base 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 @@ -170,7 +180,7 @@ class ActiveStorage::Blob < ActiveRecord::Base end def upload_without_unfurling(io) #:nodoc: - service.upload key, io, checksum: checksum + 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. @@ -239,5 +249,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 diff --git a/activestorage/app/models/active_storage/blob/identifiable.rb b/activestorage/app/models/active_storage/blob/identifiable.rb index 2c17ddc25f..924bd06131 100644 --- a/activestorage/app/models/active_storage/blob/identifiable.rb +++ b/activestorage/app/models/active_storage/blob/identifiable.rb @@ -2,7 +2,10 @@ module ActiveStorage::Blob::Identifiable def identify - update! content_type: identify_content_type, identified: true unless identified? + unless identified? + update! content_type: identify_content_type, identified: true + update_service_metadata + end end def identified? @@ -21,4 +24,8 @@ module ActiveStorage::Blob::Identifiable "" end end + + def update_service_metadata + service.update_metadata key, service_metadata if service_metadata.any? + end end diff --git a/activestorage/app/models/active_storage/blob/representable.rb b/activestorage/app/models/active_storage/blob/representable.rb index 03d5511481..32e8fcefdf 100644 --- a/activestorage/app/models/active_storage/blob/representable.rb +++ b/activestorage/app/models/active_storage/blob/representable.rb @@ -10,7 +10,7 @@ module ActiveStorage::Blob::Representable # Returns an ActiveStorage::Variant instance with the set of +transformations+ provided. This is only relevant for image # files, and it allows any image to be transformed for size, colors, and the like. Example: # - # avatar.variant(resize_to_fit: [100, 100]).processed.service_url + # avatar.variant(resize_to_limit: [100, 100]).processed.service_url # # This will create and process a variant of the avatar blob that's constrained to a height and width of 100px. # Then it'll upload said variant to the service according to a derivative key of the blob and the transformations. @@ -18,7 +18,7 @@ module ActiveStorage::Blob::Representable # Frequently, though, you don't actually want to transform the variant right away. But rather simply refer to a # specific variant that can be created by a controller on-demand. Like so: # - # <%= image_tag Current.user.avatar.variant(resize_to_fit: [100, 100]) %> + # <%= image_tag Current.user.avatar.variant(resize_to_limit: [100, 100]) %> # # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController # can then produce on-demand. @@ -43,13 +43,13 @@ module ActiveStorage::Blob::Representable # from a non-image blob. Active Storage comes with built-in previewers for videos and PDF documents. The video previewer # extracts the first frame from a video and the PDF previewer extracts the first page from a PDF document. # - # blob.preview(resize_to_fit: [100, 100]).processed.service_url + # blob.preview(resize_to_limit: [100, 100]).processed.service_url # # Avoid processing previews synchronously in views. Instead, link to a controller action that processes them on demand. # Active Storage provides one, but you may want to create your own (for example, if you need authentication). Here’s # how to use the built-in version: # - # <%= image_tag video.preview(resize_to_fit: [100, 100]) %> + # <%= image_tag video.preview(resize_to_limit: [100, 100]) %> # # This method raises ActiveStorage::UnpreviewableError if no previewer accepts the receiving blob. To determine # whether a blob is accepted by any previewer, call ActiveStorage::Blob#previewable?. @@ -69,7 +69,7 @@ module ActiveStorage::Blob::Representable # Returns an ActiveStorage::Preview for a previewable blob or an ActiveStorage::Variant for a variable image blob. # - # blob.representation(resize_to_fit: [100, 100]).processed.service_url + # blob.representation(resize_to_limit: [100, 100]).processed.service_url # # Raises ActiveStorage::UnrepresentableError if the receiving blob is neither variable nor previewable. Call # ActiveStorage::Blob#representable? to determine whether a blob is representable. diff --git a/activestorage/app/models/active_storage/preview.rb b/activestorage/app/models/active_storage/preview.rb index dd50494799..bb9d960443 100644 --- a/activestorage/app/models/active_storage/preview.rb +++ b/activestorage/app/models/active_storage/preview.rb @@ -38,7 +38,7 @@ class ActiveStorage::Preview # Processes the preview if it has not been processed yet. Returns the receiving Preview instance for convenience: # - # blob.preview(resize_to_fit: [100, 100]).processed.service_url + # blob.preview(resize_to_limit: [100, 100]).processed.service_url # # Processing a preview generates an image from its blob and attaches the preview image to the blob. Because the preview # image is stored with the blob, it is only generated once. diff --git a/activestorage/app/models/active_storage/variant.rb b/activestorage/app/models/active_storage/variant.rb index ea57fa5f78..bc0058967a 100644 --- a/activestorage/app/models/active_storage/variant.rb +++ b/activestorage/app/models/active_storage/variant.rb @@ -27,7 +27,7 @@ require "ostruct" # To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided # by Active Storage like so: # -# <%= image_tag Current.user.avatar.variant(resize_to_fit: [100, 100]) %> +# <%= image_tag Current.user.avatar.variant(resize_to_limit: [100, 100]) %> # # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController # can then produce on-demand. @@ -36,15 +36,15 @@ require "ostruct" # has already been processed and uploaded to the service, and, if so, just return that. Otherwise it will perform # the transformations, upload the variant to the service, and return itself again. Example: # -# avatar.variant(resize_to_fit: [100, 100]).processed.service_url +# avatar.variant(resize_to_limit: [100, 100]).processed.service_url # # This will create and process a variant of the avatar blob that's constrained to a height and width of 100. # Then it'll upload said variant to the service according to a derivative key of the blob and the transformations. # # You can combine any number of ImageMagick/libvips operations into a variant, as well as any macros provided by the -# ImageProcessing gem (such as +resize_to_fit+): +# ImageProcessing gem (such as +resize_to_limit+): # -# avatar.variant(resize_to_fit: [800, 800], monochrome: true, rotate: "-90") +# avatar.variant(resize_to_limit: [800, 800], monochrome: true, rotate: "-90") # # Visit the following links for a list of available ImageProcessing commands and ImageMagick/libvips operations: # diff --git a/activestorage/app/models/active_storage/variation.rb b/activestorage/app/models/active_storage/variation.rb index 3adc2407e5..67568772da 100644 --- a/activestorage/app/models/active_storage/variation.rb +++ b/activestorage/app/models/active_storage/variation.rb @@ -6,7 +6,7 @@ # In case you do need to use this directly, it's instantiated using a hash of transformations where # the key is the command and the value is the arguments. Example: # -# ActiveStorage::Variation.new(resize_to_fit: [100, 100], monochrome: true, trim: true, rotate: "-90") +# ActiveStorage::Variation.new(resize_to_limit: [100, 100], monochrome: true, trim: true, rotate: "-90") # # The options map directly to {ImageProcessing}[https://github.com/janko-m/image_processing] commands. class ActiveStorage::Variation |