aboutsummaryrefslogtreecommitdiffstats
path: root/activestorage/app/models/active_storage
diff options
context:
space:
mode:
authorGeorge Claghorn <george.claghorn@gmail.com>2017-09-28 16:43:37 -0400
committerGitHub <noreply@github.com>2017-09-28 16:43:37 -0400
commitd30586211b41e018869a1a3f4e3af778a31591db (patch)
tree664028edf779b3a6d55e08c2503cfe91e2309eff /activestorage/app/models/active_storage
parentf7b4be40410432b77e0c5d114b0ce73480ff984d (diff)
downloadrails-d30586211b41e018869a1a3f4e3af778a31591db.tar.gz
rails-d30586211b41e018869a1a3f4e3af778a31591db.tar.bz2
rails-d30586211b41e018869a1a3f4e3af778a31591db.zip
Preview PDFs and videos
Diffstat (limited to 'activestorage/app/models/active_storage')
-rw-r--r--activestorage/app/models/active_storage/blob.rb47
-rw-r--r--activestorage/app/models/active_storage/preview.rb90
-rw-r--r--activestorage/app/models/active_storage/variant.rb6
-rw-r--r--activestorage/app/models/active_storage/variation.rb9
4 files changed, 141 insertions, 11 deletions
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)