From 0993cbe3e0564c8dfa8b258e3a88059d311a352b Mon Sep 17 00:00:00 2001 From: Yoshiyuki Hirano Date: Mon, 18 Sep 2017 09:13:00 +0900 Subject: Remove unused require in ActiveStorage::Variation --- activestorage/app/models/active_storage/variation.rb | 2 -- 1 file changed, 2 deletions(-) (limited to 'activestorage/app') diff --git a/activestorage/app/models/active_storage/variation.rb b/activestorage/app/models/active_storage/variation.rb index bf269e2a8f..cf04a879eb 100644 --- a/activestorage/app/models/active_storage/variation.rb +++ b/activestorage/app/models/active_storage/variation.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/object/inclusion" - # A set of transformations that can be applied to a blob to create a variant. This class is exposed via # the ActiveStorage::Blob#variant method and should rarely be used directly. # -- cgit v1.2.3 From 91edf754c4ccdb55bfebb2fcb1458ca0a4d769d9 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 20 Sep 2017 15:34:04 -0400 Subject: Flesh out ActiveStorage::Filename docs --- activestorage/app/models/active_storage/filename.rb | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) (limited to 'activestorage/app') diff --git a/activestorage/app/models/active_storage/filename.rb b/activestorage/app/models/active_storage/filename.rb index dead6b6d33..8e3cd488a4 100644 --- a/activestorage/app/models/active_storage/filename.rb +++ b/activestorage/app/models/active_storage/filename.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -# Encapsulates a string representing a filename to provide convenience access to parts of it and a sanitized version. -# This is what's returned by ActiveStorage::Blob#filename. A Filename instance is comparable so it can be used for sorting. +# Encapsulates a string representing a filename to provide convenient access to parts of it sanitization. +# A Filename instance is returned by ActiveStorage::Blob#filename, and is comparable so it can be used for sorting. class ActiveStorage::Filename include Comparable @@ -9,23 +9,31 @@ class ActiveStorage::Filename @filename = filename end - # Returns the basename of the filename. + # Returns the part of the filename preceding any extension. # # ActiveStorage::Filename.new("racecar.jpg").base # => "racecar" + # ActiveStorage::Filename.new("racecar").base # => "racecar" + # ActiveStorage::Filename.new(".gitignore").base # => ".gitignore" def base File.basename @filename, extension_with_delimiter end - # Returns the extension with delimiter of the filename. + # Returns the extension of the filename (i.e. the substring following the last dot, excluding a dot at the + # beginning) with the dot that precedes it. If the filename has no extension, an empty string is returned. # # ActiveStorage::Filename.new("racecar.jpg").extension_with_delimiter # => ".jpg" + # ActiveStorage::Filename.new("racecar").extension_with_delimiter # => "" + # ActiveStorage::Filename.new(".gitignore").extension_with_delimiter # => "" def extension_with_delimiter File.extname @filename end - # Returns the extension without delimiter of the filename. + # Returns the extension of the filename (i.e. the substring following the last dot, excluding a dot at + # the beginning). If the filename has no extension, an empty string is returned. # # ActiveStorage::Filename.new("racecar.jpg").extension_without_delimiter # => "jpg" + # ActiveStorage::Filename.new("racecar").extension_without_delimiter # => "" + # ActiveStorage::Filename.new(".gitignore").extension_without_delimiter # => "" def extension_without_delimiter extension_with_delimiter.from(1).to_s end @@ -37,7 +45,7 @@ class ActiveStorage::Filename # ActiveStorage::Filename.new("foo:bar.jpg").sanitized # => "foo-bar.jpg" # ActiveStorage::Filename.new("foo/bar.jpg").sanitized # => "foo-bar.jpg" # - # ...and any other character unsafe for URLs or storage is converted or stripped. + # Characters considered unsafe for storage (e.g. \, $, and the RTL override character) are replaced with a dash. def sanitized @filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-") end -- cgit v1.2.3 From 4e68525e338685c1c77b76a488eb06af021f0e05 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Wed, 20 Sep 2017 15:35:01 -0400 Subject: Add missing word [ci skip] --- activestorage/app/models/active_storage/filename.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activestorage/app') diff --git a/activestorage/app/models/active_storage/filename.rb b/activestorage/app/models/active_storage/filename.rb index 8e3cd488a4..79d55dc889 100644 --- a/activestorage/app/models/active_storage/filename.rb +++ b/activestorage/app/models/active_storage/filename.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Encapsulates a string representing a filename to provide convenient access to parts of it sanitization. +# Encapsulates a string representing a filename to provide convenient access to parts of it and sanitization. # A Filename instance is returned by ActiveStorage::Blob#filename, and is comparable so it can be used for sorting. class ActiveStorage::Filename include Comparable -- cgit v1.2.3 From d30586211b41e018869a1a3f4e3af778a31591db Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Thu, 28 Sep 2017 16:43:37 -0400 Subject: Preview PDFs and videos --- .../controllers/active_storage/blobs_controller.rb | 15 +--- .../controllers/active_storage/disk_controller.rb | 6 +- .../active_storage/previews_controller.rb | 12 +++ .../active_storage/variants_controller.rb | 19 +---- activestorage/app/models/active_storage/blob.rb | 47 +++++++++-- activestorage/app/models/active_storage/preview.rb | 90 ++++++++++++++++++++++ activestorage/app/models/active_storage/variant.rb | 6 +- .../app/models/active_storage/variation.rb | 9 +++ 8 files changed, 160 insertions(+), 44 deletions(-) create mode 100644 activestorage/app/controllers/active_storage/previews_controller.rb create mode 100644 activestorage/app/models/active_storage/preview.rb (limited to 'activestorage/app') diff --git a/activestorage/app/controllers/active_storage/blobs_controller.rb b/activestorage/app/controllers/active_storage/blobs_controller.rb index 00aa8567c8..a17e3852f9 100644 --- a/activestorage/app/controllers/active_storage/blobs_controller.rb +++ b/activestorage/app/controllers/active_storage/blobs_controller.rb @@ -6,20 +6,11 @@ # authenticated redirection controller. class ActiveStorage::BlobsController < ActionController::Base def show - if blob = find_signed_blob - expires_in 5.minutes # service_url defaults to 5 minutes - redirect_to blob.service_url(disposition: disposition_param) + if blob = ActiveStorage::Blob.find_signed(params[:signed_id]) + expires_in ActiveStorage::Blob.service.url_expires_in + redirect_to blob.service_url(disposition: params[:disposition]) else head :not_found end end - - private - def find_signed_blob - ActiveStorage::Blob.find_signed(params[:signed_id]) - end - - def disposition_param - params[:disposition].presence_in(%w( inline attachment )) || "inline" - end end diff --git a/activestorage/app/controllers/active_storage/disk_controller.rb b/activestorage/app/controllers/active_storage/disk_controller.rb index 41e6d61bff..a4fd427cb2 100644 --- a/activestorage/app/controllers/active_storage/disk_controller.rb +++ b/activestorage/app/controllers/active_storage/disk_controller.rb @@ -8,7 +8,7 @@ class ActiveStorage::DiskController < ActionController::Base def show if key = decode_verified_key send_data disk_service.download(key), - disposition: disposition_param, content_type: params[:content_type] + disposition: params[:disposition], content_type: params[:content_type] else head :not_found end @@ -38,10 +38,6 @@ class ActiveStorage::DiskController < ActionController::Base ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key) end - def disposition_param - params[:disposition].presence || "inline" - end - def decode_verified_token ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token) diff --git a/activestorage/app/controllers/active_storage/previews_controller.rb b/activestorage/app/controllers/active_storage/previews_controller.rb new file mode 100644 index 0000000000..9e8cf27b6e --- /dev/null +++ b/activestorage/app/controllers/active_storage/previews_controller.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class ActiveStorage::PreviewsController < ActionController::Base + def show + if blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id]) + expires_in ActiveStorage::Blob.service.url_expires_in + redirect_to ActiveStorage::Preview.new(blob, params[:variation_key]).processed.service_url(disposition: params[:disposition]) + else + head :not_found + end + end +end diff --git a/activestorage/app/controllers/active_storage/variants_controller.rb b/activestorage/app/controllers/active_storage/variants_controller.rb index 02e3010626..dc5e78ecc0 100644 --- a/activestorage/app/controllers/active_storage/variants_controller.rb +++ b/activestorage/app/controllers/active_storage/variants_controller.rb @@ -6,24 +6,11 @@ # authenticated redirection controller. class ActiveStorage::VariantsController < ActionController::Base def show - if blob = find_signed_blob - expires_in 5.minutes # service_url defaults to 5 minutes - redirect_to ActiveStorage::Variant.new(blob, decoded_variation).processed.service_url(disposition: disposition_param) + if blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id]) + expires_in ActiveStorage::Blob.service.url_expires_in + redirect_to ActiveStorage::Variant.new(blob, params[:variation_key]).processed.service_url(disposition: params[:disposition]) else head :not_found end end - - private - def find_signed_blob - ActiveStorage::Blob.find_signed(params[:signed_blob_id]) - end - - def decoded_variation - ActiveStorage::Variation.decode(params[:variation_key]) - end - - def disposition_param - params[:disposition].presence_in(%w( inline attachment )) || "inline" - end end diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb index e6cf08ce83..7477b09d09 100644 --- a/activestorage/app/models/active_storage/blob.rb +++ b/activestorage/app/models/active_storage/blob.rb @@ -14,6 +14,8 @@ # update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file. # If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one. class ActiveStorage::Blob < ActiveRecord::Base + class UnpreviewableError < StandardError; end + self.table_name = "active_storage_blobs" has_secure_token :key @@ -21,6 +23,8 @@ class ActiveStorage::Blob < ActiveRecord::Base class_attribute :service + has_one_attached :preview_image + class << self # You can used 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 @@ -101,19 +105,18 @@ class ActiveStorage::Blob < ActiveRecord::Base content_type.start_with?("text") end - # Returns an ActiveStorage::Variant instance with the set of +transformations+ - # passed in. This is only relevant for image files, and it allows any image to - # be transformed for size, colors, and the like. Example: + # 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: "100x100").processed.service_url # - # This will create and process a variant of the avatar blob that's constrained to a height and width of 100. + # 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. # # 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 url_for(Current.user.avatar.variant(resize: "100x100")) %> + # <%= image_tag Current.user.avatar.variant(resize: "100x100") %> # # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController # can then produce on-demand. @@ -122,17 +125,45 @@ class ActiveStorage::Blob < ActiveRecord::Base end + # Returns an ActiveStorage::Preview instance with the set of +transformations+ provided. A preview is an image generated + # 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: "100x100").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: "100x100") %> + # + # This method raises ActiveStorage::Blob::UnpreviewableError if no previewer accepts the receiving blob. To determine + # whether a blob is accepted by any previewer, call ActiveStorage::Blob#previewable?. + def preview(transformations) + if previewable? + ActiveStorage::Preview.new(self, ActiveStorage::Variation.new(transformations)) + else + raise UnpreviewableError + end + end + + # Returns true if any registered previewer accepts the blob. By default, this will return true for videos and PDF documents. + def previewable? + ActiveStorage.previewers.any? { |klass| klass.accept?(self) } + end + + # Returns the URL of the blob on the service. This URL is intended to be short-lived for security and not used directly # 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: 5.minutes, disposition: :inline) - service.url key, expires_in: expires_in, disposition: "#{disposition}; #{filename.parameters}", filename: filename, content_type: content_type + def service_url(expires_in: service.url_expires_in, disposition: "inline") + service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type 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: 5.minutes) + def service_url_for_direct_upload(expires_in: service.url_expires_in) service.url_for_direct_upload key, expires_in: expires_in, content_type: content_type, content_length: byte_size, checksum: checksum end diff --git a/activestorage/app/models/active_storage/preview.rb b/activestorage/app/models/active_storage/preview.rb new file mode 100644 index 0000000000..42c4bbc5a4 --- /dev/null +++ b/activestorage/app/models/active_storage/preview.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +# Some non-image blobs can be previewed: that is, they can be presented as images. A video blob can be previewed by +# extracting its first frame, and a PDF blob can be previewed by extracting its first page. +# +# A previewer extracts a preview image from a blob. Active Storage provides previewers for videos and PDFs: +# ActiveStorage::Previewer::VideoPreviewer and ActiveStorage::Previewer::PDFPreviewer. Build custom previewers by +# subclassing ActiveStorage::Previewer and implementing the requisite methods. Consult the ActiveStorage::Previewer +# documentation for more details on what's required of previewers. +# +# To choose the previewer for a blob, Active Storage calls +accept?+ on each registered previewer in order. It uses the +# first previewer for which +accept?+ returns true when given the blob. In a Rails application, add or remove previewers +# by manipulating +Rails.application.config.active_storage.previewers+ in an initializer: +# +# Rails.application.config.active_storage.previewers +# # => [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ] +# +# # Add a custom previewer for Microsoft Office documents: +# Rails.application.config.active_storage.previewers << DOCXPreviewer +# # => [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer, DOCXPreviewer ] +# +# Outside of a Rails application, modify +ActiveStorage.previewers+ instead. +# +# The built-in previewers rely on third-party system libraries: +# +# * {ffmpeg}[https://www.ffmpeg.org] +# * {mupdf}[https://mupdf.com] +# +# These libraries are not provided by Rails. You must install them yourself to use the built-in previewers. Before you +# install and use third-party software, make sure you understand the licensing implications of doing so. +class ActiveStorage::Preview + class UnprocessedError < StandardError; end + + attr_reader :blob, :variation + + def initialize(blob, variation_or_variation_key) + @blob, @variation = blob, ActiveStorage::Variation.wrap(variation_or_variation_key) + end + + # Processes the preview if it has not been processed yet. Returns the receiving Preview instance for convenience: + # + # blob.preview(resize: "100x100").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. + def processed + process unless processed? + self + end + + # Returns the blob's attached preview image. + def image + blob.preview_image + end + + # Returns the URL of the preview's variant on the service. Raises ActiveStorage::Preview::UnprocessedError if the + # preview has not been processed yet. + # + # This method synchronously processes a variant of the preview image, so do not call it in views. Instead, generate + # a stable URL that redirects to the short-lived URL returned by this method. + def service_url(**options) + if processed? + variant.service_url(options) + else + raise UnprocessedError + end + end + + private + def processed? + image.attached? + end + + def process + previewer.preview { |attachable| image.attach(attachable) } + end + + def variant + ActiveStorage::Variant.new(image, variation).processed + end + + + def previewer + previewer_class.new(blob) + end + + def previewer_class + ActiveStorage.previewers.detect { |klass| klass.accept?(blob) } + end +end diff --git a/activestorage/app/models/active_storage/variant.rb b/activestorage/app/models/active_storage/variant.rb index 02bf32b352..54685b4c0e 100644 --- a/activestorage/app/models/active_storage/variant.rb +++ b/activestorage/app/models/active_storage/variant.rb @@ -38,8 +38,8 @@ class ActiveStorage::Variant attr_reader :blob, :variation delegate :service, to: :blob - def initialize(blob, variation) - @blob, @variation = blob, variation + def initialize(blob, variation_or_variation_key) + @blob, @variation = blob, ActiveStorage::Variation.wrap(variation_or_variation_key) end # Returns the variant instance itself after it's been processed or an existing processing has been found on the service. @@ -61,7 +61,7 @@ class ActiveStorage::Variant # Use url_for(variant) (or the implied form, like +link_to variant+ or +redirect_to variant+) to get the stable URL # for a variant that points to the ActiveStorage::VariantsController, which in turn will use this +service_call+ method # for its redirection. - def service_url(expires_in: 5.minutes, disposition: :inline) + def service_url(expires_in: service.url_expires_in, disposition: :inline) service.url key, expires_in: expires_in, disposition: disposition, filename: blob.filename, content_type: blob.content_type end diff --git a/activestorage/app/models/active_storage/variation.rb b/activestorage/app/models/active_storage/variation.rb index cf04a879eb..df2643442a 100644 --- a/activestorage/app/models/active_storage/variation.rb +++ b/activestorage/app/models/active_storage/variation.rb @@ -13,6 +13,15 @@ class ActiveStorage::Variation attr_reader :transformations class << self + def wrap(variation_or_key) + case variation_or_key + when self + variation_or_key + else + decode variation_or_key + end + end + # Returns a variation instance with the transformations that were encoded by +encode+. def decode(key) new ActiveStorage.verifier.verify(key, purpose: :variation) -- cgit v1.2.3 From 45ed61ac4731eb91f39d3762889dce0da899af45 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Tue, 3 Oct 2017 08:27:21 -0500 Subject: Associate blobs with their attachments --- activestorage/app/models/active_storage/blob.rb | 2 ++ 1 file changed, 2 insertions(+) (limited to 'activestorage/app') diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb index 7477b09d09..ff785d4f61 100644 --- a/activestorage/app/models/active_storage/blob.rb +++ b/activestorage/app/models/active_storage/blob.rb @@ -23,6 +23,8 @@ class ActiveStorage::Blob < ActiveRecord::Base class_attribute :service + has_many :attachments + has_one_attached :preview_image class << self -- cgit v1.2.3 From 53c16188924c7f4179555cdc6ae6911e44743d60 Mon Sep 17 00:00:00 2001 From: Yoshiyuki Hirano Date: Wed, 4 Oct 2017 03:36:21 +0900 Subject: Fix third-party system libraries list in ActiveStorage::Preview [ci skip] --- activestorage/app/models/active_storage/preview.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'activestorage/app') diff --git a/activestorage/app/models/active_storage/preview.rb b/activestorage/app/models/active_storage/preview.rb index 42c4bbc5a4..be5053edae 100644 --- a/activestorage/app/models/active_storage/preview.rb +++ b/activestorage/app/models/active_storage/preview.rb @@ -23,8 +23,8 @@ # # The built-in previewers rely on third-party system libraries: # -# * {ffmpeg}[https://www.ffmpeg.org] -# * {mupdf}[https://mupdf.com] +# * {ffmpeg}[https://www.ffmpeg.org] +# * {mupdf}[https://mupdf.com] # # These libraries are not provided by Rails. You must install them yourself to use the built-in previewers. Before you # install and use third-party software, make sure you understand the licensing implications of doing so. -- cgit v1.2.3 From ead60686e810df4b49bf19f4f113b48f16ae560f Mon Sep 17 00:00:00 2001 From: khall Date: Wed, 4 Oct 2017 12:26:04 -0700 Subject: Replace variation key use with SHA256 of key to prevent long filenames If a variant has a large set of options associated with it, the generated filename will be too long, causing Errno::ENAMETOOLONG to be raised. This change replaces those potentially long filenames with a much more compact SHA256 hash. Fixes #30662. --- activestorage/app/models/active_storage/variant.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activestorage/app') diff --git a/activestorage/app/models/active_storage/variant.rb b/activestorage/app/models/active_storage/variant.rb index 54685b4c0e..90a3605331 100644 --- a/activestorage/app/models/active_storage/variant.rb +++ b/activestorage/app/models/active_storage/variant.rb @@ -50,7 +50,7 @@ class ActiveStorage::Variant # Returns a combination key of the blob and the variation that together identifies a specific variant. def key - "variants/#{blob.key}/#{variation.key}" + "variants/#{blob.key}/#{Digest::SHA256.hexdigest(variation.key)}" end # Returns the URL of the variant on the service. This URL is intended to be short-lived for security and not used directly -- cgit v1.2.3 From 445c682a8465b1a42f1335ae2cf7d20b9a112fcd Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Thu, 12 Oct 2017 11:47:21 -0400 Subject: Introduce ActiveStorage::Blob#representation --- activestorage/app/models/active_storage/blob.rb | 26 ++++++++++++++++++++++ activestorage/app/models/active_storage/variant.rb | 4 ++++ 2 files changed, 30 insertions(+) (limited to 'activestorage/app') diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb index ff785d4f61..d43049e32c 100644 --- a/activestorage/app/models/active_storage/blob.rb +++ b/activestorage/app/models/active_storage/blob.rb @@ -15,6 +15,7 @@ # If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one. class ActiveStorage::Blob < ActiveRecord::Base class UnpreviewableError < StandardError; end + class UnrepresentableError < StandardError; end self.table_name = "active_storage_blobs" @@ -155,6 +156,31 @@ class ActiveStorage::Blob < ActiveRecord::Base end + # Returns an ActiveStorage::Preview instance for a previewable blob or an ActiveStorage::Variant instance for an image blob. + # + # blob.representation(resize: "100x100").processed.service_url + # + # Raises ActiveStorage::Blob::UnrepresentableError if the receiving blob is neither an image nor previewable. Call + # ActiveStorage::Blob#representable? to determine whether a blob is representable. + # + # See ActiveStorage::Blob#preview and ActiveStorage::Blob#variant for more information. + def representation(transformations) + case + when previewable? + preview transformations + when image? + variant transformations + else + raise UnrepresentableError + end + end + + # Returns true if the blob is an image or is previewable. + def representable? + image? || previewable? + end + + # Returns the URL of the blob on the service. This URL is intended to be short-lived for security and not used directly # 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 diff --git a/activestorage/app/models/active_storage/variant.rb b/activestorage/app/models/active_storage/variant.rb index 90a3605331..915b78162c 100644 --- a/activestorage/app/models/active_storage/variant.rb +++ b/activestorage/app/models/active_storage/variant.rb @@ -65,6 +65,10 @@ class ActiveStorage::Variant service.url key, expires_in: expires_in, disposition: disposition, filename: blob.filename, content_type: blob.content_type end + # Returns the receiving variant. Allows ActiveStorage::Variant and ActiveStorage::Preview instances to be duck-typed. + def image + self + end private def processed? -- cgit v1.2.3 From 62ff514d33d3a3b0930956a4b4866e6b228c278c Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Thu, 12 Oct 2017 13:40:25 -0400 Subject: Accept variation keys in #preview and #variant --- activestorage/app/models/active_storage/blob.rb | 4 ++-- activestorage/app/models/active_storage/variation.rb | 15 ++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) (limited to 'activestorage/app') diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb index d43049e32c..84b8f3827b 100644 --- a/activestorage/app/models/active_storage/blob.rb +++ b/activestorage/app/models/active_storage/blob.rb @@ -124,7 +124,7 @@ class ActiveStorage::Blob < ActiveRecord::Base # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController # can then produce on-demand. def variant(transformations) - ActiveStorage::Variant.new(self, ActiveStorage::Variation.new(transformations)) + ActiveStorage::Variant.new(self, ActiveStorage::Variation.wrap(transformations)) end @@ -144,7 +144,7 @@ class ActiveStorage::Blob < ActiveRecord::Base # whether a blob is accepted by any previewer, call ActiveStorage::Blob#previewable?. def preview(transformations) if previewable? - ActiveStorage::Preview.new(self, ActiveStorage::Variation.new(transformations)) + ActiveStorage::Preview.new(self, ActiveStorage::Variation.wrap(transformations)) else raise UnpreviewableError end diff --git a/activestorage/app/models/active_storage/variation.rb b/activestorage/app/models/active_storage/variation.rb index df2643442a..13bad87cac 100644 --- a/activestorage/app/models/active_storage/variation.rb +++ b/activestorage/app/models/active_storage/variation.rb @@ -13,16 +13,21 @@ class ActiveStorage::Variation attr_reader :transformations class << self - def wrap(variation_or_key) - case variation_or_key + # Returns a Variation instance based on the given variator. If the variator is a Variation, it is + # returned unmodified. If it is a String, it is passed to ActiveStorage::Variation.decode. Otherwise, + # it is assumed to be a transformations Hash and is passed directly to the constructor. + def wrap(variator) + case variator when self - variation_or_key + variator + when String + decode variator else - decode variation_or_key + new variator end end - # Returns a variation instance with the transformations that were encoded by +encode+. + # Returns a Variation instance with the transformations that were encoded by +encode+. def decode(key) new ActiveStorage.verifier.verify(key, purpose: :variation) end -- cgit v1.2.3 From 29da7d1ff510a9f376fc6c780273dfa89298ff51 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Fri, 13 Oct 2017 07:52:39 -0400 Subject: Clarify comment [ci skip] --- activestorage/app/models/active_storage/variant.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activestorage/app') diff --git a/activestorage/app/models/active_storage/variant.rb b/activestorage/app/models/active_storage/variant.rb index 915b78162c..fa5aa69bd3 100644 --- a/activestorage/app/models/active_storage/variant.rb +++ b/activestorage/app/models/active_storage/variant.rb @@ -65,7 +65,7 @@ class ActiveStorage::Variant service.url key, expires_in: expires_in, disposition: disposition, filename: blob.filename, content_type: blob.content_type end - # Returns the receiving variant. Allows ActiveStorage::Variant and ActiveStorage::Preview instances to be duck-typed. + # Returns the receiving variant. Allows ActiveStorage::Variant and ActiveStorage::Preview instances to be used interchangeably. def image self end -- cgit v1.2.3 From 605484079d297d1ba6835628465be81f03c052ee Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Sun, 22 Oct 2017 13:16:59 -0400 Subject: Extract metadata from images and videos --- .../app/jobs/active_storage/analyze_job.rb | 8 +++ activestorage/app/jobs/active_storage/purge_job.rb | 2 +- .../app/models/active_storage/attachment.rb | 7 +++ activestorage/app/models/active_storage/blob.rb | 57 +++++++++++++++++++++- 4 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 activestorage/app/jobs/active_storage/analyze_job.rb (limited to 'activestorage/app') diff --git a/activestorage/app/jobs/active_storage/analyze_job.rb b/activestorage/app/jobs/active_storage/analyze_job.rb new file mode 100644 index 0000000000..a11a73d030 --- /dev/null +++ b/activestorage/app/jobs/active_storage/analyze_job.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# Provides asynchronous analysis of ActiveStorage::Blob records via ActiveStorage::Blob#analyze_later. +class ActiveStorage::AnalyzeJob < ActiveJob::Base + def perform(blob) + blob.analyze + end +end diff --git a/activestorage/app/jobs/active_storage/purge_job.rb b/activestorage/app/jobs/active_storage/purge_job.rb index 990ab27c9f..188840f702 100644 --- a/activestorage/app/jobs/active_storage/purge_job.rb +++ b/activestorage/app/jobs/active_storage/purge_job.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Provides delayed purging of ActiveStorage::Blob records via ActiveStorage::Blob#purge_later. +# Provides asynchronous purging of ActiveStorage::Blob records via ActiveStorage::Blob#purge_later. class ActiveStorage::PurgeJob < ActiveJob::Base # FIXME: Limit this to a custom ActiveStorage error retry_on StandardError diff --git a/activestorage/app/models/active_storage/attachment.rb b/activestorage/app/models/active_storage/attachment.rb index 29226e8ee9..9f61a5dbf3 100644 --- a/activestorage/app/models/active_storage/attachment.rb +++ b/activestorage/app/models/active_storage/attachment.rb @@ -14,6 +14,8 @@ class ActiveStorage::Attachment < ActiveRecord::Base delegate_missing_to :blob + after_create_commit :analyze_blob_later + # Synchronously purges the blob (deletes it from the configured service) and destroys the attachment. def purge blob.purge @@ -25,4 +27,9 @@ class ActiveStorage::Attachment < ActiveRecord::Base blob.purge_later destroy end + + private + def analyze_blob_later + blob.analyze_later unless blob.analyzed? + end end diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb index 84b8f3827b..99823e14c6 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/analyzer/null_analyzer" + # 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: # @@ -20,7 +22,7 @@ class ActiveStorage::Blob < ActiveRecord::Base self.table_name = "active_storage_blobs" has_secure_token :key - store :metadata, coder: JSON + store :metadata, accessors: [ :analyzed ], coder: JSON class_attribute :service @@ -224,6 +226,46 @@ class ActiveStorage::Blob < ActiveRecord::Base end + # Extracts and stores metadata from the file associated with this blob using a relevant analyzer. Active Storage comes + # with built-in analyzers for images and videos. See ActiveStorage::Analyzer::ImageAnalyzer and + # ActiveStorage::Analyzer::VideoAnalyzer for information about the specific attributes they extract and the third-party + # libraries they require. + # + # To choose the analyzer for a blob, Active Storage calls +accept?+ on each registered analyzer in order. It uses the + # first analyzer for which +accept?+ returns true when given the blob. If no registered analyzer accepts the blob, no + # metadata is extracted from it. + # + # In a Rails application, add or remove analyzers by manipulating +Rails.application.config.active_storage.analyzers+ + # in an initializer: + # + # # Add a custom analyzer for Microsoft Office documents: + # Rails.application.config.active_storage.analyzers.append DOCXAnalyzer + # + # # Remove the built-in video analyzer: + # Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer + # + # Outside of a Rails application, manipulate +ActiveStorage.analyzers+ instead. + # + # You won't ordinarily need to call this method from a Rails application. New blobs are automatically and asynchronously + # analyzed via #analyze_later when they're attached for the first time. + def analyze + update! metadata: extract_metadata_via_analyzer + end + + # Enqueues an ActiveStorage::AnalyzeJob which calls #analyze. + # + # This method is automatically called for a blob when it's attached for the first time. You can call it to analyze a blob + # again (e.g. if you add a new analyzer or modify an existing one). + def analyze_later + ActiveStorage::AnalyzeJob.perform_later(self) + end + + # Returns true if the blob has been analyzed. + def analyzed? + analyzed + 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+ # methods in most circumstances. @@ -255,4 +297,17 @@ class ActiveStorage::Blob < ActiveRecord::Base io.rewind end.base64digest end + + + def extract_metadata_via_analyzer + analyzer.metadata.merge(analyzed: true) + end + + def analyzer + analyzer_class.new(self) + end + + def analyzer_class + ActiveStorage.analyzers.detect { |klass| klass.accept?(self) } || ActiveStorage::Analyzer::NullAnalyzer + end end -- cgit v1.2.3 From 9ec67362054e874ed905310a79b670941fa397af Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Fri, 3 Nov 2017 11:29:21 -0400 Subject: Permit configuring Active Storage's job queue --- activestorage/app/jobs/active_storage/analyze_job.rb | 2 +- activestorage/app/jobs/active_storage/base_job.rb | 5 +++++ activestorage/app/jobs/active_storage/purge_job.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 activestorage/app/jobs/active_storage/base_job.rb (limited to 'activestorage/app') diff --git a/activestorage/app/jobs/active_storage/analyze_job.rb b/activestorage/app/jobs/active_storage/analyze_job.rb index a11a73d030..2a952f9f74 100644 --- a/activestorage/app/jobs/active_storage/analyze_job.rb +++ b/activestorage/app/jobs/active_storage/analyze_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Provides asynchronous analysis of ActiveStorage::Blob records via ActiveStorage::Blob#analyze_later. -class ActiveStorage::AnalyzeJob < ActiveJob::Base +class ActiveStorage::AnalyzeJob < ActiveStorage::BaseJob def perform(blob) blob.analyze end diff --git a/activestorage/app/jobs/active_storage/base_job.rb b/activestorage/app/jobs/active_storage/base_job.rb new file mode 100644 index 0000000000..6caab42a2d --- /dev/null +++ b/activestorage/app/jobs/active_storage/base_job.rb @@ -0,0 +1,5 @@ +# 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 188840f702..98874d2250 100644 --- a/activestorage/app/jobs/active_storage/purge_job.rb +++ b/activestorage/app/jobs/active_storage/purge_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Provides asynchronous purging of ActiveStorage::Blob records via ActiveStorage::Blob#purge_later. -class ActiveStorage::PurgeJob < ActiveJob::Base +class ActiveStorage::PurgeJob < ActiveStorage::BaseJob # FIXME: Limit this to a custom ActiveStorage error retry_on StandardError -- cgit v1.2.3 From 704a7e425ca99af1b778c764a86e5388647631dd Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Mon, 13 Nov 2017 16:36:39 -0500 Subject: Preserve existing metadata when analyzing a blob Closes #31138. --- activestorage/app/models/active_storage/blob.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activestorage/app') diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb index 99823e14c6..2aa05d665e 100644 --- a/activestorage/app/models/active_storage/blob.rb +++ b/activestorage/app/models/active_storage/blob.rb @@ -249,7 +249,7 @@ class ActiveStorage::Blob < ActiveRecord::Base # You won't ordinarily need to call this method from a Rails application. New blobs are automatically and asynchronously # analyzed via #analyze_later when they're attached for the first time. def analyze - update! metadata: extract_metadata_via_analyzer + update! metadata: metadata.merge(extract_metadata_via_analyzer) end # Enqueues an ActiveStorage::AnalyzeJob which calls #analyze. -- cgit v1.2.3 From 2d20a7696a761b1840bc2fbe09a2fd4bff2a779f Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Mon, 20 Nov 2017 10:52:54 -0500 Subject: Fix direct uploads to local service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Disable CSRF protection for ActiveStorage::DiskController#update. The local disk service is intended to imitate a third-party service like S3 or GCS, so we don't care where direct uploads originate: they’re authorized by signed tokens. Closes #30290. [Shinichi Maeshima & George Claghorn] --- activestorage/app/controllers/active_storage/disk_controller.rb | 2 ++ 1 file changed, 2 insertions(+) (limited to 'activestorage/app') diff --git a/activestorage/app/controllers/active_storage/disk_controller.rb b/activestorage/app/controllers/active_storage/disk_controller.rb index a4fd427cb2..8caecfff49 100644 --- a/activestorage/app/controllers/active_storage/disk_controller.rb +++ b/activestorage/app/controllers/active_storage/disk_controller.rb @@ -5,6 +5,8 @@ # Always go through the BlobsController, or your own authenticated controller, rather than directly # to the service url. class ActiveStorage::DiskController < ActionController::Base + skip_forgery_protection + def show if key = decode_verified_key send_data disk_service.download(key), -- cgit v1.2.3 From 4d5f0bb30b5ac76407c9864b83b69b8a83ac3dd6 Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Tue, 21 Nov 2017 14:59:30 -0500 Subject: Fix loading ActiveStorage::DiskController when CSRF protection is disabled by default --- activestorage/app/controllers/active_storage/disk_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activestorage/app') diff --git a/activestorage/app/controllers/active_storage/disk_controller.rb b/activestorage/app/controllers/active_storage/disk_controller.rb index 8caecfff49..a7e10c0696 100644 --- a/activestorage/app/controllers/active_storage/disk_controller.rb +++ b/activestorage/app/controllers/active_storage/disk_controller.rb @@ -5,7 +5,7 @@ # Always go through the BlobsController, or your own authenticated controller, rather than directly # to the service url. class ActiveStorage::DiskController < ActionController::Base - skip_forgery_protection + skip_forgery_protection if default_protect_from_forgery def show if key = decode_verified_key -- cgit v1.2.3 From 8c5a7fbefd3cad403e7594d0b6a5488d80d4c98e Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Sat, 2 Dec 2017 22:43:28 -0500 Subject: Purge variants with their blobs --- activestorage/app/models/active_storage/blob.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'activestorage/app') diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb index 2aa05d665e..acaf22fac1 100644 --- a/activestorage/app/models/active_storage/blob.rb +++ b/activestorage/app/models/active_storage/blob.rb @@ -270,7 +270,8 @@ class ActiveStorage::Blob < ActiveRecord::Base # deleted as well or you will essentially have a dead reference. It's recommended to use the +#purge+ and +#purge_later+ # methods in most circumstances. def delete - service.delete key + service.delete(key) + service.delete_prefixed("variants/#{key}/") if image? end # Deletes the file on the service and then destroys the blob record. This is the recommended way to dispose of unwanted -- cgit v1.2.3