diff options
author | George Claghorn <george.claghorn@gmail.com> | 2017-09-28 16:43:37 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-09-28 16:43:37 -0400 |
commit | d30586211b41e018869a1a3f4e3af778a31591db (patch) | |
tree | 664028edf779b3a6d55e08c2503cfe91e2309eff /activestorage/app | |
parent | f7b4be40410432b77e0c5d114b0ce73480ff984d (diff) | |
download | rails-d30586211b41e018869a1a3f4e3af778a31591db.tar.gz rails-d30586211b41e018869a1a3f4e3af778a31591db.tar.bz2 rails-d30586211b41e018869a1a3f4e3af778a31591db.zip |
Preview PDFs and videos
Diffstat (limited to 'activestorage/app')
8 files changed, 160 insertions, 44 deletions
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 <tt>url_for(variant)</tt> (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) |