aboutsummaryrefslogtreecommitdiffstats
path: root/activestorage/lib
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/lib
parentf7b4be40410432b77e0c5d114b0ce73480ff984d (diff)
downloadrails-d30586211b41e018869a1a3f4e3af778a31591db.tar.gz
rails-d30586211b41e018869a1a3f4e3af778a31591db.tar.bz2
rails-d30586211b41e018869a1a3f4e3af778a31591db.zip
Preview PDFs and videos
Diffstat (limited to 'activestorage/lib')
-rw-r--r--activestorage/lib/active_storage.rb2
-rw-r--r--activestorage/lib/active_storage/engine.rb10
-rw-r--r--activestorage/lib/active_storage/previewer.rb69
-rw-r--r--activestorage/lib/active_storage/previewer/pdf_previewer.rb17
-rw-r--r--activestorage/lib/active_storage/previewer/video_previewer.rb17
-rw-r--r--activestorage/lib/active_storage/service.rb7
-rw-r--r--activestorage/lib/active_storage/service/azure_storage_service.rb2
-rw-r--r--activestorage/lib/active_storage/service/disk_service.rb5
-rw-r--r--activestorage/lib/active_storage/service/gcs_service.rb11
-rw-r--r--activestorage/lib/active_storage/service/s3_service.rb2
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