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/lib | |
parent | f7b4be40410432b77e0c5d114b0ce73480ff984d (diff) | |
download | rails-d30586211b41e018869a1a3f4e3af778a31591db.tar.gz rails-d30586211b41e018869a1a3f4e3af778a31591db.tar.bz2 rails-d30586211b41e018869a1a3f4e3af778a31591db.zip |
Preview PDFs and videos
Diffstat (limited to 'activestorage/lib')
10 files changed, 135 insertions, 7 deletions
diff --git a/activestorage/lib/active_storage.rb b/activestorage/lib/active_storage.rb index ccc1d4a163..44d9c25504 100644 --- a/activestorage/lib/active_storage.rb +++ b/activestorage/lib/active_storage.rb @@ -33,6 +33,8 @@ module ActiveStorage autoload :Attached autoload :Service + autoload :Previewer mattr_accessor :verifier + mattr_accessor :previewers, default: [] end diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb index 590a36a30a..335eae8dd8 100644 --- a/activestorage/lib/active_storage/engine.rb +++ b/activestorage/lib/active_storage/engine.rb @@ -3,11 +3,15 @@ require "rails" require "active_storage" +require "active_storage/previewer/pdf_previewer" +require "active_storage/previewer/video_previewer" + module ActiveStorage class Engine < Rails::Engine # :nodoc: isolate_namespace ActiveStorage config.active_storage = ActiveSupport::OrderedOptions.new + config.active_storage.previewers = [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ] config.eager_load_namespaces << ActiveStorage @@ -59,5 +63,11 @@ module ActiveStorage end end end + + initializer "active_storage.previewers" do + config.after_initialize do |app| + ActiveStorage.previewers = app.config.active_storage.previewers || [] + end + end end end diff --git a/activestorage/lib/active_storage/previewer.rb b/activestorage/lib/active_storage/previewer.rb new file mode 100644 index 0000000000..c91f64ac65 --- /dev/null +++ b/activestorage/lib/active_storage/previewer.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module ActiveStorage + # This is an abstract base class for previewers, which generate images from blobs. See + # ActiveStorage::Previewer::PDFPreviewer and ActiveStorage::Previewer::VideoPreviewer for examples of + # concrete subclasses. + class Previewer + attr_reader :blob + + # Implement this method in a concrete subclass. Have it return true when given a blob from which + # the previewer can generate an image. + def self.accept?(blob) + false + end + + def initialize(blob) + @blob = blob + end + + # Override this method in a concrete subclass. Have it yield an attachable preview image (i.e. + # anything accepted by ActiveStorage::Attached::One#attach). + def preview + raise NotImplementedError + end + + private + # Downloads the blob to a new tempfile. Yields the tempfile. + # + # Use this method to get a tempfile that you can provide to a drawing command. + def open # :doc: + Tempfile.open("input") do |file| + download_blob_to file + yield file + end + end + + def download_blob_to(file) + file.binmode + blob.download { |chunk| file.write(chunk) } + file.rewind + end + + + # Executes a system command, capturing its binary output in a tempfile. Yields the tempfile. + # + # Use this method to shell out to system libraries (e.g. mupdf or ffmpeg) for preview image + # generation. The resulting tempfile can be used as the +:io+ value in an attachable Hash: + # + # def preview + # open do |input| + # draw "my-drawing-command", input.path, "--format", "png", "-" do |output| + # yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png" + # end + # end + # end + def draw(*argv) # :doc: + Tempfile.open("output") do |file| + capture *argv, to: file + yield file + end + end + + def capture(*argv, to:) + to.binmode + IO.popen(argv) { |out| IO.copy_stream(out, to) } + to.rewind + end + end +end diff --git a/activestorage/lib/active_storage/previewer/pdf_previewer.rb b/activestorage/lib/active_storage/previewer/pdf_previewer.rb new file mode 100644 index 0000000000..31a2a8f120 --- /dev/null +++ b/activestorage/lib/active_storage/previewer/pdf_previewer.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module ActiveStorage + class Previewer::PDFPreviewer < Previewer + def self.accept?(blob) + blob.content_type == "application/pdf" + end + + def preview + open do |input| + draw "mutool", "draw", "-F", "png", "-o", "-", input.path, "1" do |output| + yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png" + end + end + end + end +end diff --git a/activestorage/lib/active_storage/previewer/video_previewer.rb b/activestorage/lib/active_storage/previewer/video_previewer.rb new file mode 100644 index 0000000000..840d87f100 --- /dev/null +++ b/activestorage/lib/active_storage/previewer/video_previewer.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module ActiveStorage + class Previewer::VideoPreviewer < Previewer + def self.accept?(blob) + blob.video? + end + + def preview + open do |input| + draw "ffmpeg", "-i", input.path, "-y", "-vcodec", "png", "-vf", "thumbnail", "-vframes", "1", "-f", "image2", "-" do |output| + yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png" + end + end + end + end +end diff --git a/activestorage/lib/active_storage/service.rb b/activestorage/lib/active_storage/service.rb index b80fdea1ab..1f012da1e7 100644 --- a/activestorage/lib/active_storage/service.rb +++ b/activestorage/lib/active_storage/service.rb @@ -4,6 +4,7 @@ require "active_storage/log_subscriber" module ActiveStorage class IntegrityError < StandardError; end + # Abstract class serving as an interface for concrete services. # # The available services are: @@ -42,6 +43,8 @@ module ActiveStorage class_attribute :logger + class_attribute :url_expires_in, default: 5.minutes + class << self # Configure an Active Storage service by name from a set of configurations, # typically loaded from a YAML file. The Active Storage engine uses this @@ -113,5 +116,9 @@ module ActiveStorage # ActiveStorage::Service::DiskService => Disk self.class.name.split("::").third.remove("Service") end + + def content_disposition_with(type: "inline", filename:) + (type.to_s.presence_in(%w( attachment inline )) || "inline") + "; #{filename.parameters}" + end end end diff --git a/activestorage/lib/active_storage/service/azure_storage_service.rb b/activestorage/lib/active_storage/service/azure_storage_service.rb index 895cc9c2f1..27dd192ce6 100644 --- a/activestorage/lib/active_storage/service/azure_storage_service.rb +++ b/activestorage/lib/active_storage/service/azure_storage_service.rb @@ -66,7 +66,7 @@ module ActiveStorage URI(base_url), false, permissions: "r", expiry: format_expiry(expires_in), - content_disposition: disposition, + content_disposition: content_disposition_with(type: disposition, filename: filename), content_type: content_type ).to_s diff --git a/activestorage/lib/active_storage/service/disk_service.rb b/activestorage/lib/active_storage/service/disk_service.rb index f600753a08..52eaba4e7b 100644 --- a/activestorage/lib/active_storage/service/disk_service.rb +++ b/activestorage/lib/active_storage/service/disk_service.rb @@ -64,9 +64,10 @@ module ActiveStorage if defined?(Rails.application) Rails.application.routes.url_helpers.rails_disk_service_path \ verified_key_with_expiration, - filename: filename, disposition: disposition, content_type: content_type + filename: filename, disposition: content_disposition_with(type: disposition, filename: filename), content_type: content_type else - "/rails/active_storage/disk/#{verified_key_with_expiration}/#{filename}?content_type=#{content_type}&disposition=#{disposition}" + "/rails/active_storage/disk/#{verified_key_with_expiration}/#{filename}?content_type=#{content_type}" \ + "&disposition=#{content_disposition_with(type: disposition, filename: filename)}" end payload[:url] = generated_url diff --git a/activestorage/lib/active_storage/service/gcs_service.rb b/activestorage/lib/active_storage/service/gcs_service.rb index 685dd61a0a..b4ffeeeb8a 100644 --- a/activestorage/lib/active_storage/service/gcs_service.rb +++ b/activestorage/lib/active_storage/service/gcs_service.rb @@ -24,12 +24,17 @@ module ActiveStorage end end - # FIXME: Add streaming when given a block + # FIXME: Download in chunks when given a block. def download(key) instrument :download, key do io = file_for(key).download io.rewind - io.read + + if block_given? + yield io.read + else + io.read + end end end @@ -54,7 +59,7 @@ module ActiveStorage def url(key, expires_in:, filename:, content_type:, disposition:) instrument :url, key do |payload| generated_url = file_for(key).signed_url expires: expires_in, query: { - "response-content-disposition" => disposition, + "response-content-disposition" => content_disposition_with(type: disposition, filename: filename), "response-content-type" => content_type } diff --git a/activestorage/lib/active_storage/service/s3_service.rb b/activestorage/lib/active_storage/service/s3_service.rb index e074269353..3e93cdd072 100644 --- a/activestorage/lib/active_storage/service/s3_service.rb +++ b/activestorage/lib/active_storage/service/s3_service.rb @@ -55,7 +55,7 @@ module ActiveStorage def url(key, expires_in:, filename:, disposition:, content_type:) instrument :url, key do |payload| generated_url = object_for(key).presigned_url :get, expires_in: expires_in.to_i, - response_content_disposition: disposition, + response_content_disposition: content_disposition_with(type: disposition, filename: filename), response_content_type: content_type payload[:url] = generated_url |