aboutsummaryrefslogtreecommitdiffstats
path: root/activestorage
diff options
context:
space:
mode:
Diffstat (limited to 'activestorage')
-rw-r--r--activestorage/README.md4
-rw-r--r--activestorage/app/jobs/active_storage/analyze_job.rb8
-rw-r--r--activestorage/app/jobs/active_storage/base_job.rb5
-rw-r--r--activestorage/app/jobs/active_storage/purge_job.rb4
-rw-r--r--activestorage/app/models/active_storage/attachment.rb7
-rw-r--r--activestorage/app/models/active_storage/blob.rb57
-rw-r--r--activestorage/lib/active_storage.rb6
-rw-r--r--activestorage/lib/active_storage/analyzer.rb33
-rw-r--r--activestorage/lib/active_storage/analyzer/image_analyzer.rb36
-rw-r--r--activestorage/lib/active_storage/analyzer/null_analyzer.rb13
-rw-r--r--activestorage/lib/active_storage/analyzer/video_analyzer.rb79
-rw-r--r--activestorage/lib/active_storage/attached/one.rb6
-rw-r--r--activestorage/lib/active_storage/downloading.rb26
-rw-r--r--activestorage/lib/active_storage/engine.rb19
-rw-r--r--activestorage/lib/active_storage/log_subscriber.rb2
-rw-r--r--activestorage/lib/active_storage/previewer.rb29
-rw-r--r--activestorage/lib/active_storage/previewer/pdf_previewer.rb2
-rw-r--r--activestorage/lib/active_storage/previewer/video_previewer.rb10
-rw-r--r--activestorage/lib/active_storage/service.rb2
-rw-r--r--activestorage/lib/active_storage/service/azure_storage_service.rb8
-rw-r--r--activestorage/lib/active_storage/service/s3_service.rb6
-rw-r--r--activestorage/test/analyzer/image_analyzer_test.rb16
-rw-r--r--activestorage/test/analyzer/video_analyzer_test.rb35
-rw-r--r--activestorage/test/database/setup.rb2
-rwxr-xr-xactivestorage/test/dummy/bin/bundle2
-rw-r--r--activestorage/test/fixtures/files/rotated_video.mp4bin0 -> 275090 bytes
-rw-r--r--activestorage/test/fixtures/files/video_without_video_stream.mp4bin0 -> 16252 bytes
-rw-r--r--activestorage/test/models/attachments_test.rb84
-rw-r--r--activestorage/test/previewer/pdf_previewer_test.rb3
-rw-r--r--activestorage/test/previewer/video_previewer_test.rb3
-rw-r--r--activestorage/test/service/shared_service_tests.rb10
-rw-r--r--activestorage/test/test_helper.rb14
32 files changed, 472 insertions, 59 deletions
diff --git a/activestorage/README.md b/activestorage/README.md
index 8814887950..78e4463c5a 100644
--- a/activestorage/README.md
+++ b/activestorage/README.md
@@ -12,6 +12,10 @@ A key difference to how Active Storage works compared to other attachment soluti
`Blob` models store attachment metadata (filename, content-type, etc.), and their identifier key in the storage service. Blob models do not store the actual binary data. They are intended to be immutable in spirit. One file, one blob. You can associate the same blob with multiple application models as well. And if you want to do transformations of a given `Blob`, the idea is that you'll simply create a new one, rather than attempt to mutate the existing one (though of course you can delete the previous version later if you don't need it).
+## Installation
+
+Run `rails active_storage:install` to copy over active_storage migrations.
+
## Examples
One attachment:
diff --git a/activestorage/app/jobs/active_storage/analyze_job.rb b/activestorage/app/jobs/active_storage/analyze_job.rb
new file mode 100644
index 0000000000..2a952f9f74
--- /dev/null
+++ b/activestorage/app/jobs/active_storage/analyze_job.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+# Provides asynchronous analysis of ActiveStorage::Blob records via ActiveStorage::Blob#analyze_later.
+class ActiveStorage::AnalyzeJob < ActiveStorage::BaseJob
+ def perform(blob)
+ blob.analyze
+ end
+end
diff --git a/activestorage/app/jobs/active_storage/base_job.rb b/activestorage/app/jobs/active_storage/base_job.rb
new file mode 100644
index 0000000000..6caab42a2d
--- /dev/null
+++ b/activestorage/app/jobs/active_storage/base_job.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ActiveStorage::BaseJob < ActiveJob::Base
+ queue_as { ActiveStorage.queue }
+end
diff --git a/activestorage/app/jobs/active_storage/purge_job.rb b/activestorage/app/jobs/active_storage/purge_job.rb
index 990ab27c9f..98874d2250 100644
--- a/activestorage/app/jobs/active_storage/purge_job.rb
+++ b/activestorage/app/jobs/active_storage/purge_job.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-# Provides delayed purging of ActiveStorage::Blob records via ActiveStorage::Blob#purge_later.
-class ActiveStorage::PurgeJob < ActiveJob::Base
+# Provides asynchronous purging of ActiveStorage::Blob records via ActiveStorage::Blob#purge_later.
+class ActiveStorage::PurgeJob < ActiveStorage::BaseJob
# FIXME: Limit this to a custom ActiveStorage error
retry_on StandardError
diff --git a/activestorage/app/models/active_storage/attachment.rb b/activestorage/app/models/active_storage/attachment.rb
index 29226e8ee9..9f61a5dbf3 100644
--- a/activestorage/app/models/active_storage/attachment.rb
+++ b/activestorage/app/models/active_storage/attachment.rb
@@ -14,6 +14,8 @@ class ActiveStorage::Attachment < ActiveRecord::Base
delegate_missing_to :blob
+ after_create_commit :analyze_blob_later
+
# Synchronously purges the blob (deletes it from the configured service) and destroys the attachment.
def purge
blob.purge
@@ -25,4 +27,9 @@ class ActiveStorage::Attachment < ActiveRecord::Base
blob.purge_later
destroy
end
+
+ private
+ def analyze_blob_later
+ blob.analyze_later unless blob.analyzed?
+ end
end
diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb
index 84b8f3827b..2aa05d665e 100644
--- a/activestorage/app/models/active_storage/blob.rb
+++ b/activestorage/app/models/active_storage/blob.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_storage/analyzer/null_analyzer"
+
# A blob is a record that contains the metadata about a file and a key for where that file resides on the service.
# Blobs can be created in two ways:
#
@@ -20,7 +22,7 @@ class ActiveStorage::Blob < ActiveRecord::Base
self.table_name = "active_storage_blobs"
has_secure_token :key
- store :metadata, coder: JSON
+ store :metadata, accessors: [ :analyzed ], coder: JSON
class_attribute :service
@@ -224,6 +226,46 @@ class ActiveStorage::Blob < ActiveRecord::Base
end
+ # Extracts and stores metadata from the file associated with this blob using a relevant analyzer. Active Storage comes
+ # with built-in analyzers for images and videos. See ActiveStorage::Analyzer::ImageAnalyzer and
+ # ActiveStorage::Analyzer::VideoAnalyzer for information about the specific attributes they extract and the third-party
+ # libraries they require.
+ #
+ # To choose the analyzer for a blob, Active Storage calls +accept?+ on each registered analyzer in order. It uses the
+ # first analyzer for which +accept?+ returns true when given the blob. If no registered analyzer accepts the blob, no
+ # metadata is extracted from it.
+ #
+ # In a Rails application, add or remove analyzers by manipulating +Rails.application.config.active_storage.analyzers+
+ # in an initializer:
+ #
+ # # Add a custom analyzer for Microsoft Office documents:
+ # Rails.application.config.active_storage.analyzers.append DOCXAnalyzer
+ #
+ # # Remove the built-in video analyzer:
+ # Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer
+ #
+ # Outside of a Rails application, manipulate +ActiveStorage.analyzers+ instead.
+ #
+ # You won't ordinarily need to call this method from a Rails application. New blobs are automatically and asynchronously
+ # analyzed via #analyze_later when they're attached for the first time.
+ def analyze
+ update! metadata: metadata.merge(extract_metadata_via_analyzer)
+ end
+
+ # Enqueues an ActiveStorage::AnalyzeJob which calls #analyze.
+ #
+ # This method is automatically called for a blob when it's attached for the first time. You can call it to analyze a blob
+ # again (e.g. if you add a new analyzer or modify an existing one).
+ def analyze_later
+ ActiveStorage::AnalyzeJob.perform_later(self)
+ end
+
+ # Returns true if the blob has been analyzed.
+ def analyzed?
+ analyzed
+ end
+
+
# Deletes the file on the service that's associated with this blob. This should only be done if the blob is going to be
# deleted as well or you will essentially have a dead reference. It's recommended to use the +#purge+ and +#purge_later+
# methods in most circumstances.
@@ -255,4 +297,17 @@ class ActiveStorage::Blob < ActiveRecord::Base
io.rewind
end.base64digest
end
+
+
+ def extract_metadata_via_analyzer
+ analyzer.metadata.merge(analyzed: true)
+ end
+
+ def analyzer
+ analyzer_class.new(self)
+ end
+
+ def analyzer_class
+ ActiveStorage.analyzers.detect { |klass| klass.accept?(self) } || ActiveStorage::Analyzer::NullAnalyzer
+ end
end
diff --git a/activestorage/lib/active_storage.rb b/activestorage/lib/active_storage.rb
index 44d9c25504..d1ff6b7032 100644
--- a/activestorage/lib/active_storage.rb
+++ b/activestorage/lib/active_storage.rb
@@ -26,7 +26,7 @@
require "active_record"
require "active_support"
require "active_support/rails"
-require_relative "active_storage/version"
+require "active_storage/version"
module ActiveStorage
extend ActiveSupport::Autoload
@@ -34,7 +34,11 @@ module ActiveStorage
autoload :Attached
autoload :Service
autoload :Previewer
+ autoload :Analyzer
+ mattr_accessor :logger
mattr_accessor :verifier
+ mattr_accessor :queue
mattr_accessor :previewers, default: []
+ mattr_accessor :analyzers, default: []
end
diff --git a/activestorage/lib/active_storage/analyzer.rb b/activestorage/lib/active_storage/analyzer.rb
new file mode 100644
index 0000000000..837785a12b
--- /dev/null
+++ b/activestorage/lib/active_storage/analyzer.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require "active_storage/downloading"
+
+module ActiveStorage
+ # This is an abstract base class for analyzers, which extract metadata from blobs. See
+ # ActiveStorage::Analyzer::ImageAnalyzer for an example of a concrete subclass.
+ class Analyzer
+ include Downloading
+
+ attr_reader :blob
+
+ # Implement this method in a concrete subclass. Have it return true when given a blob from which
+ # the analyzer can extract metadata.
+ def self.accept?(blob)
+ false
+ end
+
+ def initialize(blob)
+ @blob = blob
+ end
+
+ # Override this method in a concrete subclass. Have it return a Hash of metadata.
+ def metadata
+ raise NotImplementedError
+ end
+
+ private
+ def logger
+ ActiveStorage.logger
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/analyzer/image_analyzer.rb b/activestorage/lib/active_storage/analyzer/image_analyzer.rb
new file mode 100644
index 0000000000..25e0251e6e
--- /dev/null
+++ b/activestorage/lib/active_storage/analyzer/image_analyzer.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ # Extracts width and height in pixels from an image blob.
+ #
+ # Example:
+ #
+ # ActiveStorage::Analyzer::ImageAnalyzer.new(blob).metadata
+ # # => { 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.
+ class Analyzer::ImageAnalyzer < Analyzer
+ def self.accept?(blob)
+ blob.image?
+ end
+
+ def metadata
+ read_image do |image|
+ { width: image.width, height: image.height }
+ end
+ rescue LoadError
+ logger.info "Skipping image analysis because the mini_magick gem isn't installed"
+ {}
+ end
+
+ private
+ def read_image
+ download_blob_to_tempfile do |file|
+ require "mini_magick"
+ yield MiniMagick::Image.new(file.path)
+ end
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/analyzer/null_analyzer.rb b/activestorage/lib/active_storage/analyzer/null_analyzer.rb
new file mode 100644
index 0000000000..8ff7ce48e5
--- /dev/null
+++ b/activestorage/lib/active_storage/analyzer/null_analyzer.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Analyzer::NullAnalyzer < Analyzer # :nodoc:
+ def self.accept?(blob)
+ true
+ end
+
+ def metadata
+ {}
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/analyzer/video_analyzer.rb b/activestorage/lib/active_storage/analyzer/video_analyzer.rb
new file mode 100644
index 0000000000..408b5e58e9
--- /dev/null
+++ b/activestorage/lib/active_storage/analyzer/video_analyzer.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/compact"
+
+module ActiveStorage
+ # Extracts the following from a video blob:
+ #
+ # * Width (pixels)
+ # * Height (pixels)
+ # * Duration (seconds)
+ # * Angle (degrees)
+ # * Aspect ratio
+ #
+ # Example:
+ #
+ # ActiveStorage::VideoAnalyzer.new(blob).metadata
+ # # => { width: 640, height: 480, duration: 5.0, angle: 0, 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.
+ class Analyzer::VideoAnalyzer < Analyzer
+ def self.accept?(blob)
+ blob.video?
+ end
+
+ def metadata
+ { width: width, height: height, duration: duration, angle: angle, aspect_ratio: aspect_ratio }.compact
+ end
+
+ private
+ def width
+ Integer(video_stream["width"]) if video_stream["width"]
+ end
+
+ def height
+ Integer(video_stream["height"]) if video_stream["height"]
+ end
+
+ def duration
+ Float(video_stream["duration"]) if video_stream["duration"]
+ end
+
+ def angle
+ Integer(tags["rotate"]) if tags["rotate"]
+ end
+
+ def aspect_ratio
+ if descriptor = video_stream["display_aspect_ratio"]
+ descriptor.split(":", 2).collect(&:to_i)
+ end
+ end
+
+
+ def tags
+ @tags ||= video_stream["tags"] || {}
+ end
+
+ def video_stream
+ @video_stream ||= streams.detect { |stream| stream["codec_type"] == "video" } || {}
+ end
+
+ def streams
+ probe["streams"] || []
+ end
+
+ def probe
+ download_blob_to_tempfile { |file| probe_from(file) }
+ end
+
+ def probe_from(file)
+ IO.popen([ "ffprobe", "-print_format", "json", "-show_streams", "-v", "error", file.path ]) do |output|
+ JSON.parse(output.read)
+ end
+ rescue Errno::ENOENT
+ logger.info "Skipping video analysis because ffmpeg isn't installed"
+ {}
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/attached/one.rb b/activestorage/lib/active_storage/attached/one.rb
index c66be08f58..dc19512484 100644
--- a/activestorage/lib/active_storage/attached/one.rb
+++ b/activestorage/lib/active_storage/attached/one.rb
@@ -59,12 +59,16 @@ module ActiveStorage
def replace(attachable)
blob.tap do
transaction do
- destroy
+ destroy_attachment
write_attachment create_attachment_from(attachable)
end
end.purge_later
end
+ def destroy_attachment
+ attachment.destroy
+ end
+
def create_attachment_from(attachable)
ActiveStorage::Attachment.create!(record: record, name: name, blob: create_blob_from(attachable))
end
diff --git a/activestorage/lib/active_storage/downloading.rb b/activestorage/lib/active_storage/downloading.rb
new file mode 100644
index 0000000000..3dac6b116a
--- /dev/null
+++ b/activestorage/lib/active_storage/downloading.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+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|
+ download_blob_to file
+ yield file
+ end
+ end
+
+ # Efficiently downloads blob data into the given file.
+ 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:
+ Dir.tmpdir
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb
index 335eae8dd8..6cf6635c4f 100644
--- a/activestorage/lib/active_storage/engine.rb
+++ b/activestorage/lib/active_storage/engine.rb
@@ -6,20 +6,25 @@ require "active_storage"
require "active_storage/previewer/pdf_previewer"
require "active_storage/previewer/video_previewer"
+require "active_storage/analyzer/image_analyzer"
+require "active_storage/analyzer/video_analyzer"
+
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.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ]
config.eager_load_namespaces << ActiveStorage
- initializer "active_storage.logger" do
- require "active_storage/service"
-
+ initializer "active_storage.configs" do
config.after_initialize do |app|
- ActiveStorage::Service.logger = app.config.active_storage.logger || Rails.logger
+ ActiveStorage.logger = app.config.active_storage.logger || Rails.logger
+ ActiveStorage.queue = app.config.active_storage.queue
+ ActiveStorage.previewers = app.config.active_storage.previewers || []
+ ActiveStorage.analyzers = app.config.active_storage.analyzers || []
end
end
@@ -63,11 +68,5 @@ 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/log_subscriber.rb b/activestorage/lib/active_storage/log_subscriber.rb
index 0d00a75c0e..5cbf4bd1a5 100644
--- a/activestorage/lib/active_storage/log_subscriber.rb
+++ b/activestorage/lib/active_storage/log_subscriber.rb
@@ -27,7 +27,7 @@ module ActiveStorage
end
def logger
- ActiveStorage::Service.logger
+ ActiveStorage.logger
end
private
diff --git a/activestorage/lib/active_storage/previewer.rb b/activestorage/lib/active_storage/previewer.rb
index 930b376067..ed75bae3b5 100644
--- a/activestorage/lib/active_storage/previewer.rb
+++ b/activestorage/lib/active_storage/previewer.rb
@@ -1,10 +1,14 @@
# frozen_string_literal: true
+require "active_storage/downloading"
+
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
+ include Downloading
+
attr_reader :blob
# Implement this method in a concrete subclass. Have it return true when given a blob from which
@@ -24,37 +28,22 @@ module ActiveStorage
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
+ # Use this method to shell out to a system library (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|
+ # download_blob_to_tempfile 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
+ #
+ # The output tempfile is opened in the directory returned by ActiveStorage::Downloading#tempdir.
def draw(*argv) # :doc:
- Tempfile.open("output") do |file|
+ Tempfile.open("ActiveStorage", tempdir) do |file|
capture(*argv, to: file)
yield file
end
diff --git a/activestorage/lib/active_storage/previewer/pdf_previewer.rb b/activestorage/lib/active_storage/previewer/pdf_previewer.rb
index 31a2a8f120..a2f05c74a6 100644
--- a/activestorage/lib/active_storage/previewer/pdf_previewer.rb
+++ b/activestorage/lib/active_storage/previewer/pdf_previewer.rb
@@ -7,7 +7,7 @@ module ActiveStorage
end
def preview
- open do |input|
+ download_blob_to_tempfile 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
diff --git a/activestorage/lib/active_storage/previewer/video_previewer.rb b/activestorage/lib/active_storage/previewer/video_previewer.rb
index 840d87f100..49f128d142 100644
--- a/activestorage/lib/active_storage/previewer/video_previewer.rb
+++ b/activestorage/lib/active_storage/previewer/video_previewer.rb
@@ -7,11 +7,17 @@ module ActiveStorage
end
def preview
- open do |input|
- draw "ffmpeg", "-i", input.path, "-y", "-vcodec", "png", "-vf", "thumbnail", "-vframes", "1", "-f", "image2", "-" do |output|
+ download_blob_to_tempfile do |input|
+ draw_relevant_frame_from input do |output|
yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
end
end
end
+
+ private
+ def draw_relevant_frame_from(file, &block)
+ draw "ffmpeg", "-i", file.path, "-y", "-vcodec", "png",
+ "-vf", "thumbnail", "-vframes", "1", "-f", "image2", "-", &block
+ end
end
end
diff --git a/activestorage/lib/active_storage/service.rb b/activestorage/lib/active_storage/service.rb
index 1f012da1e7..aa150e4d8a 100644
--- a/activestorage/lib/active_storage/service.rb
+++ b/activestorage/lib/active_storage/service.rb
@@ -41,8 +41,6 @@ module ActiveStorage
extend ActiveSupport::Autoload
autoload :Configurator
- class_attribute :logger
-
class_attribute :url_expires_in, default: 5.minutes
class << self
diff --git a/activestorage/lib/active_storage/service/azure_storage_service.rb b/activestorage/lib/active_storage/service/azure_storage_service.rb
index 27dd192ce6..f3877ad9c9 100644
--- a/activestorage/lib/active_storage/service/azure_storage_service.rb
+++ b/activestorage/lib/active_storage/service/azure_storage_service.rb
@@ -28,7 +28,7 @@ module ActiveStorage
end
end
- def download(key)
+ def download(key, &block)
if block_given?
instrument :streaming_download, key do
stream(key, &block)
@@ -108,15 +108,15 @@ module ActiveStorage
end
# Reads the object for the given key in chunks, yielding each to the block.
- def stream(key, options = {}, &block)
+ def stream(key)
blob = blob_for(key)
chunk_size = 5.megabytes
offset = 0
while offset < blob.properties[:content_length]
- _, io = blobs.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1)
- yield io
+ _, chunk = blobs.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1)
+ yield chunk.force_encoding(Encoding::BINARY)
offset += chunk_size
end
end
diff --git a/activestorage/lib/active_storage/service/s3_service.rb b/activestorage/lib/active_storage/service/s3_service.rb
index 3e93cdd072..6957119780 100644
--- a/activestorage/lib/active_storage/service/s3_service.rb
+++ b/activestorage/lib/active_storage/service/s3_service.rb
@@ -26,7 +26,7 @@ module ActiveStorage
end
end
- def download(key)
+ def download(key, &block)
if block_given?
instrument :streaming_download, key do
stream(key, &block)
@@ -85,14 +85,14 @@ module ActiveStorage
end
# Reads the object for the given key in chunks, yielding each to the block.
- def stream(key, options = {}, &block)
+ def stream(key)
object = object_for(key)
chunk_size = 5.megabytes
offset = 0
while offset < object.content_length
- yield object.read(options.merge(range: "bytes=#{offset}-#{offset + chunk_size - 1}"))
+ yield object.get(range: "bytes=#{offset}-#{offset + chunk_size - 1}").body.read.force_encoding(Encoding::BINARY)
offset += chunk_size
end
end
diff --git a/activestorage/test/analyzer/image_analyzer_test.rb b/activestorage/test/analyzer/image_analyzer_test.rb
new file mode 100644
index 0000000000..9087072215
--- /dev/null
+++ b/activestorage/test/analyzer/image_analyzer_test.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+require "active_storage/analyzer/image_analyzer"
+
+class ActiveStorage::Analyzer::ImageAnalyzerTest < ActiveSupport::TestCase
+ test "analyzing an image" do
+ blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg")
+ metadata = blob.tap(&:analyze).metadata
+
+ assert_equal 4104, metadata[:width]
+ assert_equal 2736, metadata[:height]
+ end
+end
diff --git a/activestorage/test/analyzer/video_analyzer_test.rb b/activestorage/test/analyzer/video_analyzer_test.rb
new file mode 100644
index 0000000000..4a3c4a8bfc
--- /dev/null
+++ b/activestorage/test/analyzer/video_analyzer_test.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+require "active_storage/analyzer/video_analyzer"
+
+class ActiveStorage::Analyzer::VideoAnalyzerTest < ActiveSupport::TestCase
+ test "analyzing a video" do
+ blob = create_file_blob(filename: "video.mp4", content_type: "video/mp4")
+ metadata = blob.tap(&:analyze).metadata
+
+ assert_equal 640, metadata[:width]
+ assert_equal 480, metadata[:height]
+ assert_equal [4, 3], metadata[:aspect_ratio]
+ assert_equal 5.166648, metadata[:duration]
+ assert_not_includes metadata, :angle
+ end
+
+ test "analyzing a rotated video" do
+ blob = create_file_blob(filename: "rotated_video.mp4", content_type: "video/mp4")
+ metadata = blob.tap(&:analyze).metadata
+
+ assert_equal 640, metadata[:width]
+ assert_equal 480, metadata[:height]
+ assert_equal [4, 3], metadata[:aspect_ratio]
+ assert_equal 5.227975, metadata[:duration]
+ assert_equal 90, metadata[:angle]
+ end
+
+ test "analyzing a video without a video stream" do
+ blob = create_file_blob(filename: "video_without_video_stream.mp4", content_type: "video/mp4")
+ assert_equal({ "analyzed" => true }, blob.tap(&:analyze).metadata)
+ end
+end
diff --git a/activestorage/test/database/setup.rb b/activestorage/test/database/setup.rb
index 87564499e6..705650a25d 100644
--- a/activestorage/test/database/setup.rb
+++ b/activestorage/test/database/setup.rb
@@ -3,5 +3,5 @@
require_relative "create_users_migration"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
-ActiveRecord::Migrator.migrate File.expand_path("../../../db/migrate", __FILE__)
+ActiveRecord::Migrator.migrate File.expand_path("../../db/migrate", __dir__)
ActiveStorageCreateUsers.migrate(:up)
diff --git a/activestorage/test/dummy/bin/bundle b/activestorage/test/dummy/bin/bundle
index 277e128251..5015ba6f8b 100755
--- a/activestorage/test/dummy/bin/bundle
+++ b/activestorage/test/dummy/bin/bundle
@@ -1,5 +1,5 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
-ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__)
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
load Gem.bin_path("bundler", "bundle")
diff --git a/activestorage/test/fixtures/files/rotated_video.mp4 b/activestorage/test/fixtures/files/rotated_video.mp4
new file mode 100644
index 0000000000..4c7a4e9e57
--- /dev/null
+++ b/activestorage/test/fixtures/files/rotated_video.mp4
Binary files differ
diff --git a/activestorage/test/fixtures/files/video_without_video_stream.mp4 b/activestorage/test/fixtures/files/video_without_video_stream.mp4
new file mode 100644
index 0000000000..e6a55f868b
--- /dev/null
+++ b/activestorage/test/fixtures/files/video_without_video_stream.mp4
Binary files differ
diff --git a/activestorage/test/models/attachments_test.rb b/activestorage/test/models/attachments_test.rb
index 379ae0a416..96bc963cff 100644
--- a/activestorage/test/models/attachments_test.rb
+++ b/activestorage/test/models/attachments_test.rb
@@ -15,7 +15,7 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
assert_equal "funky.jpg", @user.avatar.filename.to_s
end
- test "attach existing sgid blob" do
+ test "attach existing blob from a signed ID" do
@user.avatar.attach create_blob(filename: "funky.jpg").signed_id
assert_equal "funky.jpg", @user.avatar.filename.to_s
end
@@ -63,6 +63,41 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
assert_equal "funky.jpg", @user.avatar_attachment.blob.filename.to_s
end
+ test "analyze newly-attached blob" do
+ perform_enqueued_jobs do
+ @user.avatar.attach create_file_blob
+ end
+
+ assert_equal 4104, @user.avatar.reload.metadata[:width]
+ assert_equal 2736, @user.avatar.metadata[:height]
+ end
+
+ test "analyze attached blob only once" do
+ blob = create_file_blob
+
+ perform_enqueued_jobs do
+ @user.avatar.attach blob
+ end
+
+ assert blob.reload.analyzed?
+
+ @user.avatar.attachment.destroy
+
+ assert_no_enqueued_jobs do
+ @user.reload.avatar.attach blob
+ end
+ end
+
+ test "preserve existing metadata when analyzing a newly-attached blob" do
+ blob = create_file_blob(metadata: { foo: "bar" })
+
+ perform_enqueued_jobs do
+ @user.avatar.attach blob
+ end
+
+ assert_equal "bar", blob.reload.metadata[:foo]
+ end
+
test "purge attached blob" do
@user.avatar.attach create_blob(filename: "funky.jpg")
avatar_key = @user.avatar.key
@@ -135,6 +170,53 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
assert_equal "town.jpg", @user.highlights_attachments.first.blob.filename.to_s
end
+ test "analyze newly-attached blobs" do
+ perform_enqueued_jobs do
+ @user.highlights.attach(
+ create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg"),
+ create_file_blob(filename: "video.mp4", content_type: "video/mp4"))
+ end
+
+ assert_equal 4104, @user.highlights.first.metadata[:width]
+ assert_equal 2736, @user.highlights.first.metadata[:height]
+
+ assert_equal 640, @user.highlights.second.metadata[:width]
+ assert_equal 480, @user.highlights.second.metadata[:height]
+ end
+
+ test "analyze attached blobs only once" do
+ blobs = [
+ create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg"),
+ create_file_blob(filename: "video.mp4", content_type: "video/mp4")
+ ]
+
+ perform_enqueued_jobs do
+ @user.highlights.attach(blobs)
+ end
+
+ assert blobs.each(&:reload).all?(&:analyzed?)
+
+ @user.highlights.attachments.destroy_all
+
+ assert_no_enqueued_jobs do
+ @user.highlights.attach(blobs)
+ end
+ end
+
+ test "preserve existing metadata when analyzing newly-attached blobs" do
+ blobs = [
+ create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg", metadata: { foo: "bar" }),
+ create_file_blob(filename: "video.mp4", content_type: "video/mp4", metadata: { foo: "bar" })
+ ]
+
+ perform_enqueued_jobs do
+ @user.highlights.attach(blobs)
+ end
+
+ blobs.each do |blob|
+ assert_equal "bar", blob.reload.metadata[:foo]
+ end
+ end
test "purge attached blobs" do
@user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "wonky.jpg")
diff --git a/activestorage/test/previewer/pdf_previewer_test.rb b/activestorage/test/previewer/pdf_previewer_test.rb
index 60f075d1b2..fe32f39be4 100644
--- a/activestorage/test/previewer/pdf_previewer_test.rb
+++ b/activestorage/test/previewer/pdf_previewer_test.rb
@@ -1,5 +1,8 @@
# frozen_string_literal: true
+require "test_helper"
+require "database/setup"
+
require "active_storage/previewer/pdf_previewer"
class ActiveStorage::Previewer::PDFPreviewerTest < ActiveSupport::TestCase
diff --git a/activestorage/test/previewer/video_previewer_test.rb b/activestorage/test/previewer/video_previewer_test.rb
index 967d5d5ba9..dba9b0d7e2 100644
--- a/activestorage/test/previewer/video_previewer_test.rb
+++ b/activestorage/test/previewer/video_previewer_test.rb
@@ -1,5 +1,8 @@
# frozen_string_literal: true
+require "test_helper"
+require "database/setup"
+
require "active_storage/previewer/video_previewer"
class ActiveStorage::Previewer::VideoPreviewerTest < ActiveSupport::TestCase
diff --git a/activestorage/test/service/shared_service_tests.rb b/activestorage/test/service/shared_service_tests.rb
index a9e1cb6ce9..ade91ab89a 100644
--- a/activestorage/test/service/shared_service_tests.rb
+++ b/activestorage/test/service/shared_service_tests.rb
@@ -50,6 +50,16 @@ module ActiveStorage::Service::SharedServiceTests
assert_equal FIXTURE_DATA, @service.download(FIXTURE_KEY)
end
+ test "downloading in chunks" do
+ chunks = []
+
+ @service.download(FIXTURE_KEY) do |chunk|
+ chunks << chunk
+ end
+
+ assert_equal [ FIXTURE_DATA ], chunks
+ end
+
test "existing" do
assert @service.exist?(FIXTURE_KEY)
assert_not @service.exist?(FIXTURE_KEY + "nonsense")
diff --git a/activestorage/test/test_helper.rb b/activestorage/test/test_helper.rb
index dd37239060..55da781f2a 100644
--- a/activestorage/test/test_helper.rb
+++ b/activestorage/test/test_helper.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require File.expand_path("../../test/dummy/config/environment.rb", __FILE__)
+require_relative "dummy/config/environment.rb"
require "bundler/setup"
require "active_support"
@@ -23,7 +23,7 @@ Minitest.backtrace_filter = Minitest::BacktraceFilter.new
require "yaml"
SERVICE_CONFIGURATIONS = begin
- erb = ERB.new(Pathname.new(File.expand_path("../service/configurations.yml", __FILE__)).read)
+ erb = ERB.new(Pathname.new(File.expand_path("service/configurations.yml", __dir__)).read)
configuration = YAML.load(erb.result) || {}
configuration.deep_symbolize_keys
rescue Errno::ENOENT
@@ -33,22 +33,20 @@ end
require "tmpdir"
ActiveStorage::Blob.service = ActiveStorage::Service::DiskService.new(root: Dir.mktmpdir("active_storage_tests"))
-ActiveStorage::Service.logger = ActiveSupport::Logger.new(nil)
+ActiveStorage.logger = ActiveSupport::Logger.new(nil)
ActiveStorage.verifier = ActiveSupport::MessageVerifier.new("Testing")
class ActiveSupport::TestCase
- self.file_fixture_path = File.expand_path("../fixtures/files", __FILE__)
+ self.file_fixture_path = File.expand_path("fixtures/files", __dir__)
private
def create_blob(data: "Hello world!", filename: "hello.txt", content_type: "text/plain")
ActiveStorage::Blob.create_after_upload! io: StringIO.new(data), filename: filename, content_type: content_type
end
- 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
+ def create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg", metadata: nil)
+ ActiveStorage::Blob.create_after_upload! io: file_fixture(filename).open, filename: filename, content_type: content_type, metadata: metadata
end
def create_blob_before_direct_upload(filename: "hello.txt", byte_size:, checksum:, content_type: "text/plain")