diff options
Diffstat (limited to 'activestorage/lib/active_storage')
13 files changed, 110 insertions, 75 deletions
diff --git a/activestorage/lib/active_storage/analyzer.rb b/activestorage/lib/active_storage/analyzer.rb index 837785a12b..7c4168c1a0 100644 --- a/activestorage/lib/active_storage/analyzer.rb +++ b/activestorage/lib/active_storage/analyzer.rb @@ -26,7 +26,7 @@ module ActiveStorage end private - def logger + def logger #:doc: ActiveStorage.logger end end diff --git a/activestorage/lib/active_storage/analyzer/image_analyzer.rb b/activestorage/lib/active_storage/analyzer/image_analyzer.rb index 25e0251e6e..5231168a7c 100644 --- a/activestorage/lib/active_storage/analyzer/image_analyzer.rb +++ b/activestorage/lib/active_storage/analyzer/image_analyzer.rb @@ -9,8 +9,7 @@ module ActiveStorage # # => { width: 4104, height: 2736 } # # This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. MiniMagick requires - # the {ImageMagick}[http://www.imagemagick.org] system library. These libraries are not provided by Rails; you must - # install them yourself to use this analyzer. + # the {ImageMagick}[http://www.imagemagick.org] system library. class Analyzer::ImageAnalyzer < Analyzer def self.accept?(blob) blob.image? diff --git a/activestorage/lib/active_storage/analyzer/video_analyzer.rb b/activestorage/lib/active_storage/analyzer/video_analyzer.rb index 1c144baa37..656e362187 100644 --- a/activestorage/lib/active_storage/analyzer/video_analyzer.rb +++ b/activestorage/lib/active_storage/analyzer/video_analyzer.rb @@ -9,41 +9,40 @@ module ActiveStorage # * Height (pixels) # * Duration (seconds) # * Angle (degrees) - # * Aspect ratio + # * Display aspect ratio # # Example: # # ActiveStorage::VideoAnalyzer.new(blob).metadata - # # => { width: 640, height: 480, duration: 5.0, angle: 0, aspect_ratio: [4, 3] } + # # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3] } # - # This analyzer requires the {ffmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails. You must - # install ffmpeg yourself to use this analyzer. + # When a video's angle is 90 or 270 degrees, its width and height are automatically swapped for convenience. + # + # This analyzer requires the {ffmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails. class Analyzer::VideoAnalyzer < Analyzer - class_attribute :ffprobe_path, default: "ffprobe" - def self.accept?(blob) blob.video? end def metadata - { width: width, height: height, duration: duration, angle: angle, aspect_ratio: aspect_ratio }.compact + { width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio }.compact end private def width - rotated? ? raw_height : raw_width + if rotated? + computed_height || encoded_height + else + encoded_width + end end def height - rotated? ? raw_width : raw_height - end - - def raw_width - Integer(video_stream["width"]) if video_stream["width"] - end - - def raw_height - Integer(video_stream["height"]) if video_stream["height"] + if rotated? + encoded_width + else + computed_height || encoded_height + end end def duration @@ -54,16 +53,40 @@ module ActiveStorage Integer(tags["rotate"]) if tags["rotate"] end - def aspect_ratio + def display_aspect_ratio if descriptor = video_stream["display_aspect_ratio"] - descriptor.split(":", 2).collect(&:to_i) + if terms = descriptor.split(":", 2) + numerator = Integer(terms[0]) + denominator = Integer(terms[1]) + + [numerator, denominator] unless numerator == 0 + end end end + def rotated? angle == 90 || angle == 270 end + def computed_height + if encoded_width && display_height_scale + encoded_width * display_height_scale + end + end + + def encoded_width + @encoded_width ||= Float(video_stream["width"]) if video_stream["width"] + end + + def encoded_height + @encoded_height ||= Float(video_stream["height"]) if video_stream["height"] + end + + def display_height_scale + @display_height_scale ||= Float(display_aspect_ratio.last) / display_aspect_ratio.first if display_aspect_ratio + end + def tags @tags ||= video_stream["tags"] || {} @@ -89,5 +112,9 @@ module ActiveStorage logger.info "Skipping video analysis because ffmpeg isn't installed" {} end + + def ffprobe_path + ActiveStorage.paths[:ffprobe] || "ffprobe" + end end end diff --git a/activestorage/lib/active_storage/attached/macros.rb b/activestorage/lib/active_storage/attached/macros.rb index 2b38a9b887..c51efa9d6b 100644 --- a/activestorage/lib/active_storage/attached/macros.rb +++ b/activestorage/lib/active_storage/attached/macros.rb @@ -38,13 +38,13 @@ module ActiveStorage end CODE - has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record + has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob scope :"with_attached_#{name}", -> { includes("#{name}_attachment": :blob) } if dependent == :purge_later - before_destroy { public_send(name).purge_later } + after_destroy_commit { public_send(name).purge_later } end end @@ -83,13 +83,13 @@ module ActiveStorage end CODE - has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment" + has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) } if dependent == :purge_later - before_destroy { public_send(name).purge_later } + after_destroy_commit { public_send(name).purge_later } end end end diff --git a/activestorage/lib/active_storage/downloading.rb b/activestorage/lib/active_storage/downloading.rb index 3dac6b116a..295289c1e7 100644 --- a/activestorage/lib/active_storage/downloading.rb +++ b/activestorage/lib/active_storage/downloading.rb @@ -1,25 +1,27 @@ # frozen_string_literal: true +require "tmpdir" + module ActiveStorage module Downloading private # Opens a new tempfile in #tempdir and copies blob data into it. Yields the tempfile. - def download_blob_to_tempfile # :doc: - Tempfile.open("ActiveStorage", tempdir) do |file| + def download_blob_to_tempfile #:doc: + Tempfile.open([ "ActiveStorage", blob.filename.extension_with_delimiter ], tempdir) do |file| download_blob_to file yield file end end # Efficiently downloads blob data into the given file. - def download_blob_to(file) # :doc: + def download_blob_to(file) #:doc: file.binmode blob.download { |chunk| file.write(chunk) } file.rewind end # Returns the directory in which tempfiles should be opened. Defaults to +Dir.tmpdir+. - def tempdir # :doc: + def tempdir #:doc: Dir.tmpdir end end diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb index b870e6d4d6..8ba32490b1 100644 --- a/activestorage/lib/active_storage/engine.rb +++ b/activestorage/lib/active_storage/engine.rb @@ -18,6 +18,18 @@ module ActiveStorage config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ] config.active_storage.paths = ActiveSupport::OrderedOptions.new + config.active_storage.variable_content_types = %w( image/png image/gif image/jpg image/jpeg image/vnd.adobe.photoshop ) + config.active_storage.content_types_to_serve_as_binary = %w( + text/html + text/javascript + image/svg+xml + application/postscript + application/x-shockwave-flash + text/xml + application/xml + application/xhtml+xml + ) + config.eager_load_namespaces << ActiveStorage initializer "active_storage.configs" do @@ -26,6 +38,10 @@ module ActiveStorage ActiveStorage.queue = app.config.active_storage.queue ActiveStorage.previewers = app.config.active_storage.previewers || [] ActiveStorage.analyzers = app.config.active_storage.analyzers || [] + ActiveStorage.paths = app.config.active_storage.paths || {} + + ActiveStorage.variable_content_types = app.config.active_storage.variable_content_types || [] + ActiveStorage.content_types_to_serve_as_binary = app.config.active_storage.content_types_to_serve_as_binary || [] end end @@ -69,21 +85,5 @@ module ActiveStorage end end end - - initializer "active_storage.paths" do - config.after_initialize do |app| - if ffprobe_path = app.config.active_storage.paths.ffprobe - ActiveStorage::Analyzer::VideoAnalyzer.ffprobe_path = ffprobe_path - end - - if ffmpeg_path = app.config.active_storage.paths.ffmpeg - ActiveStorage::Previewer::VideoPreviewer.ffmpeg_path = ffmpeg_path - end - - if mutool_path = app.config.active_storage.paths.mutool - ActiveStorage::Previewer::PDFPreviewer.mutool_path = mutool_path - end - end - end end end diff --git a/activestorage/lib/active_storage/errors.rb b/activestorage/lib/active_storage/errors.rb new file mode 100644 index 0000000000..f099b13f5b --- /dev/null +++ b/activestorage/lib/active_storage/errors.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module ActiveStorage + class InvariableError < StandardError; end + class UnpreviewableError < StandardError; end + class UnrepresentableError < StandardError; end +end diff --git a/activestorage/lib/active_storage/previewer.rb b/activestorage/lib/active_storage/previewer.rb index 3d485988e9..dacab1e7df 100644 --- a/activestorage/lib/active_storage/previewer.rb +++ b/activestorage/lib/active_storage/previewer.rb @@ -42,20 +42,22 @@ module ActiveStorage # end # # The output tempfile is opened in the directory returned by ActiveStorage::Downloading#tempdir. - def draw(*argv) # :doc: - Tempfile.open("ActiveStorage", tempdir) do |file| - capture(*argv, to: file) - yield file + def draw(*argv) #:doc: + ActiveSupport::Notifications.instrument("preview.active_storage") do + Tempfile.open("ActiveStorage", tempdir) do |file| + capture(*argv, to: file) + yield file + end end end def capture(*argv, to:) to.binmode - IO.popen(argv) { |out| IO.copy_stream(out, to) } + IO.popen(argv, err: File::NULL) { |out| IO.copy_stream(out, to) } to.rewind end - def logger + def logger #:doc: ActiveStorage.logger end end diff --git a/activestorage/lib/active_storage/previewer/pdf_previewer.rb b/activestorage/lib/active_storage/previewer/pdf_previewer.rb index b84aefcc9c..426ff51eb1 100644 --- a/activestorage/lib/active_storage/previewer/pdf_previewer.rb +++ b/activestorage/lib/active_storage/previewer/pdf_previewer.rb @@ -2,8 +2,6 @@ module ActiveStorage class Previewer::PDFPreviewer < Previewer - class_attribute :mutool_path, default: "mutool" - def self.accept?(blob) blob.content_type == "application/pdf" end @@ -20,5 +18,9 @@ module ActiveStorage def draw_first_page_from(file, &block) draw mutool_path, "draw", "-F", "png", "-o", "-", file.path, "1", &block end + + def mutool_path + ActiveStorage.paths[:mutool] || "mutool" + end end end diff --git a/activestorage/lib/active_storage/previewer/video_previewer.rb b/activestorage/lib/active_storage/previewer/video_previewer.rb index 5d06e33f44..2f28a3d341 100644 --- a/activestorage/lib/active_storage/previewer/video_previewer.rb +++ b/activestorage/lib/active_storage/previewer/video_previewer.rb @@ -2,8 +2,6 @@ module ActiveStorage class Previewer::VideoPreviewer < Previewer - class_attribute :ffmpeg_path, default: "ffmpeg" - def self.accept?(blob) blob.video? end @@ -21,5 +19,9 @@ module ActiveStorage draw ffmpeg_path, "-i", file.path, "-y", "-vcodec", "png", "-vf", "thumbnail", "-vframes", "1", "-f", "image2", "-", &block end + + def ffmpeg_path + ActiveStorage.paths[:ffmpeg] || "ffmpeg" + end end end diff --git a/activestorage/lib/active_storage/service.rb b/activestorage/lib/active_storage/service.rb index c8f675db86..f2e1269f27 100644 --- a/activestorage/lib/active_storage/service.rb +++ b/activestorage/lib/active_storage/service.rb @@ -97,7 +97,7 @@ module ActiveStorage # Returns a signed, temporary URL that a direct upload file can be PUT to on the +key+. # The URL will be valid for the amount of seconds specified in +expires_in+. - # You most also provide the +content_type+, +content_length+, and +checksum+ of the file + # You must also provide the +content_type+, +content_length+, and +checksum+ of the file # that will be uploaded. All these attributes will be validated by the service upon upload. def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) raise NotImplementedError diff --git a/activestorage/lib/active_storage/service/azure_storage_service.rb b/activestorage/lib/active_storage/service/azure_storage_service.rb index 19b09991b3..0a9eb7f23d 100644 --- a/activestorage/lib/active_storage/service/azure_storage_service.rb +++ b/activestorage/lib/active_storage/service/azure_storage_service.rb @@ -46,7 +46,7 @@ module ActiveStorage begin blobs.delete_blob(container, key) rescue Azure::Core::Http::HTTPError - false + # Ignore files already deleted end end end diff --git a/activestorage/lib/active_storage/service/disk_service.rb b/activestorage/lib/active_storage/service/disk_service.rb index a8728c5bc3..d17eea9046 100644 --- a/activestorage/lib/active_storage/service/disk_service.rb +++ b/activestorage/lib/active_storage/service/disk_service.rb @@ -9,10 +9,10 @@ module ActiveStorage # Wraps a local disk path as an Active Storage service. See ActiveStorage::Service for the generic API # documentation that applies to all services. class Service::DiskService < Service - attr_reader :root + attr_reader :root, :host - def initialize(root:) - @root = root + def initialize(root:, host: "http://localhost:3000") + @root, @host = root, host end def upload(key, io, checksum: nil) @@ -69,14 +69,13 @@ module ActiveStorage verified_key_with_expiration = ActiveStorage.verifier.generate(key, expires_in: expires_in, purpose: :blob_key) generated_url = - if defined?(Rails.application) - Rails.application.routes.url_helpers.rails_disk_service_path \ - verified_key_with_expiration, - 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=#{content_disposition_with(type: disposition, filename: filename)}" - end + Rails.application.routes.url_helpers.rails_disk_service_url( + verified_key_with_expiration, + filename: filename, + disposition: content_disposition_with(type: disposition, filename: filename), + content_type: content_type, + host: host + ) payload[:url] = generated_url @@ -97,12 +96,7 @@ module ActiveStorage purpose: :blob_token } ) - generated_url = - if defined?(Rails.application) - Rails.application.routes.url_helpers.update_rails_disk_service_path verified_token_with_expiration - else - "/rails/active_storage/disk/#{verified_token_with_expiration}" - end + generated_url = Rails.application.routes.url_helpers.update_rails_disk_service_url(verified_token_with_expiration, host: host) payload[:url] = generated_url |