diff options
Diffstat (limited to 'activestorage')
35 files changed, 436 insertions, 68 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) diff --git a/activestorage/config/routes.rb b/activestorage/config/routes.rb index c3194887be..c659e079fd 100644 --- a/activestorage/config/routes.rb +++ b/activestorage/config/routes.rb @@ -24,6 +24,19 @@ Rails.application.routes.draw do resolve("ActiveStorage::Variant") { |variant| route_for(:rails_variant, variant) } + get "/rails/active_storage/previews/:signed_blob_id/:variation_key/*filename" => "active_storage/previews#show", as: :rails_blob_preview, internal: true + + direct :rails_preview do |preview| + signed_blob_id = preview.blob.signed_id + variation_key = preview.variation.key + filename = preview.blob.filename + + route_for(:rails_blob_preview, signed_blob_id, variation_key, filename) + end + + resolve("ActiveStorage::Preview") { |preview| route_for(:rails_preview, preview) } + + get "/rails/active_storage/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service, internal: true put "/rails/active_storage/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service, internal: true post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads, internal: true 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 diff --git a/activestorage/test/controllers/blobs_controller_test.rb b/activestorage/test/controllers/blobs_controller_test.rb index c37b9c8a10..97177e64c2 100644 --- a/activestorage/test/controllers/blobs_controller_test.rb +++ b/activestorage/test/controllers/blobs_controller_test.rb @@ -5,7 +5,7 @@ require "database/setup" class ActiveStorage::BlobsControllerTest < ActionDispatch::IntegrationTest setup do - @blob = create_image_blob filename: "racecar.jpg" + @blob = create_file_blob filename: "racecar.jpg" end test "showing blob utilizes browser caching" do diff --git a/activestorage/test/controllers/previews_controller_test.rb b/activestorage/test/controllers/previews_controller_test.rb new file mode 100644 index 0000000000..c3151a710e --- /dev/null +++ b/activestorage/test/controllers/previews_controller_test.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "test_helper" +require "database/setup" + +class ActiveStorage::PreviewsControllerTest < ActionDispatch::IntegrationTest + setup do + @blob = create_file_blob filename: "report.pdf", content_type: "application/pdf" + end + + test "showing preview inline" do + get rails_blob_preview_url( + filename: @blob.filename, + signed_blob_id: @blob.signed_id, + variation_key: ActiveStorage::Variation.encode(resize: "100x100")) + + assert @blob.preview_image.attached? + assert_redirected_to(/report\.png\?.*disposition=inline/) + + image = read_image(@blob.preview_image.variant(resize: "100x100")) + assert_equal 77, image.width + assert_equal 100, image.height + end +end diff --git a/activestorage/test/controllers/variants_controller_test.rb b/activestorage/test/controllers/variants_controller_test.rb index 0a049f3bc4..6c70d73786 100644 --- a/activestorage/test/controllers/variants_controller_test.rb +++ b/activestorage/test/controllers/variants_controller_test.rb @@ -5,7 +5,7 @@ require "database/setup" class ActiveStorage::VariantsControllerTest < ActionDispatch::IntegrationTest setup do - @blob = create_image_blob filename: "racecar.jpg" + @blob = create_file_blob filename: "racecar.jpg" end test "showing variant inline" do @@ -16,7 +16,7 @@ class ActiveStorage::VariantsControllerTest < ActionDispatch::IntegrationTest assert_redirected_to(/racecar\.jpg\?.*disposition=inline/) - image = read_image_variant(@blob.variant(resize: "100x100")) + image = read_image(@blob.variant(resize: "100x100")) assert_equal 100, image.width assert_equal 67, image.height end diff --git a/activestorage/test/fixtures/files/report.pdf b/activestorage/test/fixtures/files/report.pdf Binary files differnew file mode 100644 index 0000000000..cccb9b5d64 --- /dev/null +++ b/activestorage/test/fixtures/files/report.pdf diff --git a/activestorage/test/fixtures/files/video.mp4 b/activestorage/test/fixtures/files/video.mp4 Binary files differnew file mode 100644 index 0000000000..8fb1c5b24d --- /dev/null +++ b/activestorage/test/fixtures/files/video.mp4 diff --git a/activestorage/test/models/preview_test.rb b/activestorage/test/models/preview_test.rb new file mode 100644 index 0000000000..317a2d5c58 --- /dev/null +++ b/activestorage/test/models/preview_test.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require "test_helper" +require "database/setup" + +class ActiveStorage::PreviewTest < ActiveSupport::TestCase + test "previewing a PDF" do + blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf") + preview = blob.preview(resize: "640x280").processed + assert preview.image.attached? + assert_equal "report.png", preview.image.filename.to_s + assert_equal "image/png", preview.image.content_type + + image = read_image(preview.image) + assert_equal 612, image.width + assert_equal 792, image.height + end + + test "previewing an MP4 video" do + blob = create_file_blob(filename: "video.mp4", content_type: "video/mp4") + preview = blob.preview(resize: "640x280").processed + assert preview.image.attached? + assert_equal "video.png", preview.image.filename.to_s + assert_equal "image/png", preview.image.content_type + + image = read_image(preview.image) + assert_equal 640, image.width + assert_equal 480, image.height + end + + test "previewing an unpreviewable blob" do + blob = create_file_blob + + assert_raises ActiveStorage::Blob::UnpreviewableError do + blob.preview resize: "640x280" + end + end +end diff --git a/activestorage/test/models/variant_test.rb b/activestorage/test/models/variant_test.rb index ca112ab907..d7cbef4e55 100644 --- a/activestorage/test/models/variant_test.rb +++ b/activestorage/test/models/variant_test.rb @@ -5,14 +5,14 @@ require "database/setup" class ActiveStorage::VariantTest < ActiveSupport::TestCase setup do - @blob = create_image_blob filename: "racecar.jpg" + @blob = create_file_blob filename: "racecar.jpg" end test "resized variation" do variant = @blob.variant(resize: "100x100").processed assert_match(/racecar\.jpg/, variant.service_url) - image = read_image_variant(variant) + image = read_image(variant) assert_equal 100, image.width assert_equal 67, image.height end @@ -21,7 +21,7 @@ class ActiveStorage::VariantTest < ActiveSupport::TestCase variant = @blob.variant(resize: "100x100", monochrome: true).processed assert_match(/racecar\.jpg/, variant.service_url) - image = read_image_variant(variant) + image = read_image(variant) assert_equal 100, image.width assert_equal 67, image.height assert_match(/Gray/, image.colorspace) diff --git a/activestorage/test/previewer/pdf_previewer_test.rb b/activestorage/test/previewer/pdf_previewer_test.rb new file mode 100644 index 0000000000..60f075d1b2 --- /dev/null +++ b/activestorage/test/previewer/pdf_previewer_test.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require "active_storage/previewer/pdf_previewer" + +class ActiveStorage::Previewer::PDFPreviewerTest < ActiveSupport::TestCase + setup do + @blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf") + end + + test "previewing a PDF document" do + ActiveStorage::Previewer::PDFPreviewer.new(@blob).preview do |attachable| + assert_equal "image/png", attachable[:content_type] + assert_equal "report.png", attachable[:filename] + + image = MiniMagick::Image.read(attachable[:io]) + assert_equal 612, image.width + assert_equal 792, image.height + end + end +end diff --git a/activestorage/test/previewer/video_previewer_test.rb b/activestorage/test/previewer/video_previewer_test.rb new file mode 100644 index 0000000000..967d5d5ba9 --- /dev/null +++ b/activestorage/test/previewer/video_previewer_test.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require "active_storage/previewer/video_previewer" + +class ActiveStorage::Previewer::VideoPreviewerTest < ActiveSupport::TestCase + setup do + @blob = create_file_blob(filename: "video.mp4", content_type: "video/mp4") + end + + test "previewing an MP4 video" do + ActiveStorage::Previewer::VideoPreviewer.new(@blob).preview do |attachable| + assert_equal "image/png", attachable[:content_type] + assert_equal "video.png", attachable[:filename] + + image = MiniMagick::Image.read(attachable[:io]) + assert_equal 640, image.width + assert_equal 480, image.height + end + end +end diff --git a/activestorage/test/service/azure_storage_service_test.rb b/activestorage/test/service/azure_storage_service_test.rb index 4729bdfbc5..4b7e70b8b1 100644 --- a/activestorage/test/service/azure_storage_service_test.rb +++ b/activestorage/test/service/azure_storage_service_test.rb @@ -11,7 +11,7 @@ if SERVICE_CONFIGURATIONS[:azure] test "signed URL generation" do url = @service.url(FIXTURE_KEY, expires_in: 5.minutes, - disposition: "inline; filename=\"avatar.png\"", filename: "avatar.png", content_type: "image/png") + disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png") assert_match(/(\S+)&rscd=inline%3B\+filename%3D%22avatar.png%22&rsct=image%2Fpng/, url) assert_match SERVICE_CONFIGURATIONS[:azure][:container], url diff --git a/activestorage/test/service/disk_service_test.rb b/activestorage/test/service/disk_service_test.rb index e07d1d88bc..4a6361b920 100644 --- a/activestorage/test/service/disk_service_test.rb +++ b/activestorage/test/service/disk_service_test.rb @@ -9,6 +9,6 @@ class ActiveStorage::Service::DiskServiceTest < ActiveSupport::TestCase test "url generation" do assert_match(/rails\/active_storage\/disk\/.*\/avatar\.png\?content_type=image%2Fpng&disposition=inline/, - @service.url(FIXTURE_KEY, expires_in: 5.minutes, disposition: "inline; filename=\"avatar.png\"", filename: "avatar.png", content_type: "image/png")) + @service.url(FIXTURE_KEY, expires_in: 5.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png")) end end diff --git a/activestorage/test/service/gcs_service_test.rb b/activestorage/test/service/gcs_service_test.rb index f664cee90b..5566c664a9 100644 --- a/activestorage/test/service/gcs_service_test.rb +++ b/activestorage/test/service/gcs_service_test.rb @@ -34,10 +34,10 @@ if SERVICE_CONFIGURATIONS[:gcs] test "signed URL generation" do freeze_time do url = SERVICE.bucket.signed_url(FIXTURE_KEY, expires: 120) + - "&response-content-disposition=inline%3B+filename%3D%22test.txt%22" + + "&response-content-disposition=inline%3B+filename%3D%22test.txt%22%3B+filename%2A%3DUTF-8%27%27test.txt" + "&response-content-type=text%2Fplain" - assert_equal url, @service.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: "inline; filename=\"test.txt\"", filename: "test.txt", content_type: "text/plain") + assert_equal url, @service.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("test.txt"), content_type: "text/plain") end end end diff --git a/activestorage/test/service/mirror_service_test.rb b/activestorage/test/service/mirror_service_test.rb index 93e86eff70..92101b1282 100644 --- a/activestorage/test/service/mirror_service_test.rb +++ b/activestorage/test/service/mirror_service_test.rb @@ -47,9 +47,11 @@ class ActiveStorage::Service::MirrorServiceTest < ActiveSupport::TestCase end test "URL generation in primary service" do + filename = ActiveStorage::Filename.new("test.txt") + freeze_time do - assert_equal SERVICE.primary.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: :inline, filename: "test.txt", content_type: "text/plain"), - @service.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: :inline, filename: "test.txt", content_type: "text/plain") + assert_equal SERVICE.primary.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: :inline, filename: filename, content_type: "text/plain"), + @service.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: :inline, filename: filename, content_type: "text/plain") end end diff --git a/activestorage/test/service/s3_service_test.rb b/activestorage/test/service/s3_service_test.rb index c07d6396b1..c3818422aa 100644 --- a/activestorage/test/service/s3_service_test.rb +++ b/activestorage/test/service/s3_service_test.rb @@ -33,7 +33,7 @@ if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].pr test "signed URL generation" do url = @service.url(FIXTURE_KEY, expires_in: 5.minutes, - disposition: "inline; filename=\"avatar.png\"", filename: "avatar.png", content_type: "image/png") + disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png") assert_match(/s3\.(\S+)?amazonaws.com.*response-content-disposition=inline.*avatar\.png.*response-content-type=image%2Fpng/, url) assert_match SERVICE_CONFIGURATIONS[:s3][:bucket], url diff --git a/activestorage/test/template/image_tag_test.rb b/activestorage/test/template/image_tag_test.rb index dedc58452e..80f183e0e3 100644 --- a/activestorage/test/template/image_tag_test.rb +++ b/activestorage/test/template/image_tag_test.rb @@ -7,7 +7,7 @@ class ActiveStorage::ImageTagTest < ActionView::TestCase tests ActionView::Helpers::AssetTagHelper setup do - @blob = create_image_blob filename: "racecar.jpg" + @blob = create_file_blob filename: "racecar.jpg" end test "blob" do @@ -19,6 +19,12 @@ class ActiveStorage::ImageTagTest < ActionView::TestCase assert_dom_equal %(<img src="#{polymorphic_url variant}" />), image_tag(variant) end + test "preview" do + blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf") + preview = blob.preview(resize: "100x100") + assert_dom_equal %(<img src="#{polymorphic_url preview}" />), image_tag(preview) + end + test "attachment" do attachment = ActiveStorage::Attachment.new(blob: @blob) assert_dom_equal %(<img src="#{polymorphic_url attachment}" />), image_tag(attachment) diff --git a/activestorage/test/test_helper.rb b/activestorage/test/test_helper.rb index 2a969fa326..dd37239060 100644 --- a/activestorage/test/test_helper.rb +++ b/activestorage/test/test_helper.rb @@ -6,6 +6,7 @@ require "bundler/setup" require "active_support" require "active_support/test_case" require "active_support/testing/autorun" +require "mini_magick" begin require "byebug" @@ -44,7 +45,7 @@ class ActiveSupport::TestCase ActiveStorage::Blob.create_after_upload! io: StringIO.new(data), filename: filename, content_type: content_type end - def create_image_blob(filename: "racecar.jpg", content_type: "image/jpeg") + def create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg") ActiveStorage::Blob.create_after_upload! \ io: file_fixture(filename).open, filename: filename, content_type: content_type @@ -54,8 +55,8 @@ class ActiveSupport::TestCase ActiveStorage::Blob.create_before_direct_upload! filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type end - def read_image_variant(variant) - MiniMagick::Image.open variant.service.send(:path_for, variant.key) + def read_image(blob_or_variant) + MiniMagick::Image.open blob_or_variant.service.send(:path_for, blob_or_variant.key) end end |