aboutsummaryrefslogtreecommitdiffstats
path: root/activestorage
diff options
context:
space:
mode:
Diffstat (limited to 'activestorage')
-rw-r--r--activestorage/README.md4
-rw-r--r--activestorage/app/controllers/active_storage/disk_controller.rb2
-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/config/routes.rb20
-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/macros.rb8
-rw-r--r--activestorage/lib/active_storage/attached/many.rb12
-rw-r--r--activestorage/lib/active_storage/attached/one.rb19
-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/gcs_service.rb17
-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/dummy/config/application.rb2
-rw-r--r--activestorage/test/dummy/config/environments/test.rb3
-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.rb188
-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/gcs_service_test.rb9
-rw-r--r--activestorage/test/service/shared_service_tests.rb10
-rw-r--r--activestorage/test/test_helper.rb15
40 files changed, 632 insertions, 90 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/controllers/active_storage/disk_controller.rb b/activestorage/app/controllers/active_storage/disk_controller.rb
index a4fd427cb2..a7e10c0696 100644
--- a/activestorage/app/controllers/active_storage/disk_controller.rb
+++ b/activestorage/app/controllers/active_storage/disk_controller.rb
@@ -5,6 +5,8 @@
# Always go through the BlobsController, or your own authenticated controller, rather than directly
# to the service url.
class ActiveStorage::DiskController < ActionController::Base
+ skip_forgery_protection if default_protect_from_forgery
+
def show
if key = decode_verified_key
send_data disk_service.download(key),
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/config/routes.rb b/activestorage/config/routes.rb
index c659e079fd..1eae21445a 100644
--- a/activestorage/config/routes.rb
+++ b/activestorage/config/routes.rb
@@ -3,38 +3,38 @@
Rails.application.routes.draw do
get "/rails/active_storage/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob, internal: true
- direct :rails_blob do |blob|
- route_for(:rails_service_blob, blob.signed_id, blob.filename)
+ direct :rails_blob do |blob, options|
+ route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
end
- resolve("ActiveStorage::Blob") { |blob| route_for(:rails_blob, blob) }
- resolve("ActiveStorage::Attachment") { |attachment| route_for(:rails_blob, attachment.blob) }
+ resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob) }
+ resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
get "/rails/active_storage/variants/:signed_blob_id/:variation_key/*filename" => "active_storage/variants#show", as: :rails_blob_variation, internal: true
- direct :rails_variant do |variant|
+ direct :rails_variant do |variant, options|
signed_blob_id = variant.blob.signed_id
variation_key = variant.variation.key
filename = variant.blob.filename
- route_for(:rails_blob_variation, signed_blob_id, variation_key, filename)
+ route_for(:rails_blob_variation, signed_blob_id, variation_key, filename, options)
end
- resolve("ActiveStorage::Variant") { |variant| route_for(:rails_variant, variant) }
+ resolve("ActiveStorage::Variant") { |variant, options| route_for(:rails_variant, variant, options) }
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|
+ direct :rails_preview do |preview, options|
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)
+ route_for(:rails_blob_preview, signed_blob_id, variation_key, filename, options)
end
- resolve("ActiveStorage::Preview") { |preview| route_for(:rails_preview, preview) }
+ resolve("ActiveStorage::Preview") { |preview, options| route_for(:rails_preview, preview, options) }
get "/rails/active_storage/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service, internal: true
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/macros.rb b/activestorage/lib/active_storage/attached/macros.rb
index f0256718ac..2b38a9b887 100644
--- a/activestorage/lib/active_storage/attached/macros.rb
+++ b/activestorage/lib/active_storage/attached/macros.rb
@@ -32,6 +32,10 @@ module ActiveStorage
def #{name}
@active_storage_attached_#{name} ||= ActiveStorage::Attached::One.new("#{name}", self, dependent: #{dependent == :purge_later ? ":purge_later" : "false"})
end
+
+ def #{name}=(attachable)
+ #{name}.attach(attachable)
+ end
CODE
has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record
@@ -73,6 +77,10 @@ module ActiveStorage
def #{name}
@active_storage_attached_#{name} ||= ActiveStorage::Attached::Many.new("#{name}", self, dependent: #{dependent == :purge_later ? ":purge_later" : "false"})
end
+
+ def #{name}=(attachables)
+ #{name}.attach(attachables)
+ end
CODE
has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment"
diff --git a/activestorage/lib/active_storage/attached/many.rb b/activestorage/lib/active_storage/attached/many.rb
index 1e0657c33c..6eace65b79 100644
--- a/activestorage/lib/active_storage/attached/many.rb
+++ b/activestorage/lib/active_storage/attached/many.rb
@@ -13,7 +13,6 @@ module ActiveStorage
end
# Associates one or several attachments with the current record, saving them to the database.
- # Examples:
#
# document.images.attach(params[:images]) # Array of ActionDispatch::Http::UploadedFile objects
# document.images.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
@@ -21,7 +20,11 @@ module ActiveStorage
# document.images.attach([ first_blob, second_blob ])
def attach(*attachables)
attachables.flatten.collect do |attachable|
- attachments.create!(name: name, blob: create_blob_from(attachable))
+ if record.new_record?
+ attachments.build(record: record, blob: create_blob_from(attachable))
+ else
+ attachments.create!(record: record, blob: create_blob_from(attachable))
+ end
end
end
@@ -36,6 +39,11 @@ module ActiveStorage
attachments.any?
end
+ # Deletes associated attachments without purging them, leaving their respective blobs in place.
+ def detach
+ attachments.destroy_all if attached?
+ end
+
# Directly purges each associated attachment (i.e. destroys the blobs and
# attachments and deletes the files on the service).
def purge
diff --git a/activestorage/lib/active_storage/attached/one.rb b/activestorage/lib/active_storage/attached/one.rb
index c66be08f58..0244232b2c 100644
--- a/activestorage/lib/active_storage/attached/one.rb
+++ b/activestorage/lib/active_storage/attached/one.rb
@@ -14,7 +14,6 @@ module ActiveStorage
end
# Associates a given attachment with the current record, saving it to the database.
- # Examples:
#
# person.avatar.attach(params[:avatar]) # ActionDispatch::Http::UploadedFile object
# person.avatar.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
@@ -24,7 +23,7 @@ module ActiveStorage
if attached? && dependent == :purge_later
replace attachable
else
- write_attachment create_attachment_from(attachable)
+ write_attachment build_attachment_from(attachable)
end
end
@@ -39,6 +38,14 @@ module ActiveStorage
attachment.present?
end
+ # Deletes the attachment without purging it, leaving its blob in place.
+ def detach
+ if attached?
+ attachment.destroy
+ write_attachment nil
+ end
+ end
+
# Directly purges the attachment (i.e. destroys the blob and
# attachment and deletes the file on the service).
def purge
@@ -59,14 +66,14 @@ module ActiveStorage
def replace(attachable)
blob.tap do
transaction do
- destroy
- write_attachment create_attachment_from(attachable)
+ detach
+ write_attachment build_attachment_from(attachable)
end
end.purge_later
end
- def create_attachment_from(attachable)
- ActiveStorage::Attachment.create!(record: record, name: name, blob: create_blob_from(attachable))
+ def build_attachment_from(attachable)
+ ActiveStorage::Attachment.new(record: record, name: name, blob: create_blob_from(attachable))
end
def write_attachment(attachment)
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/gcs_service.rb b/activestorage/lib/active_storage/service/gcs_service.rb
index b4ffeeeb8a..be6ddf32a0 100644
--- a/activestorage/lib/active_storage/service/gcs_service.rb
+++ b/activestorage/lib/active_storage/service/gcs_service.rb
@@ -7,11 +7,8 @@ module ActiveStorage
# Wraps the Google Cloud Storage as an Active Storage service. See ActiveStorage::Service for the generic API
# documentation that applies to all services.
class Service::GCSService < Service
- attr_reader :client, :bucket
-
- def initialize(project:, keyfile:, bucket:, **options)
- @client = Google::Cloud::Storage.new(project: project, keyfile: keyfile, **options)
- @bucket = @client.bucket(bucket)
+ def initialize(**config)
+ @config = config
end
def upload(key, io, checksum: nil)
@@ -85,8 +82,18 @@ module ActiveStorage
end
private
+ attr_reader :config
+
def file_for(key)
bucket.file(key, skip_lookup: true)
end
+
+ def bucket
+ @bucket ||= client.bucket(config.fetch(:bucket))
+ end
+
+ def client
+ @client ||= Google::Cloud::Storage.new(config.except(:bucket))
+ end
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/dummy/config/application.rb b/activestorage/test/dummy/config/application.rb
index 7ee6625bb5..06cbc453a2 100644
--- a/activestorage/test/dummy/config/application.rb
+++ b/activestorage/test/dummy/config/application.rb
@@ -19,7 +19,7 @@ Bundler.require(*Rails.groups)
module Dummy
class Application < Rails::Application
- config.load_defaults 5.1
+ config.load_defaults 5.2
config.active_storage.service = :local
end
diff --git a/activestorage/test/dummy/config/environments/test.rb b/activestorage/test/dummy/config/environments/test.rb
index ce0889e8ae..74a802d98c 100644
--- a/activestorage/test/dummy/config/environments/test.rb
+++ b/activestorage/test/dummy/config/environments/test.rb
@@ -30,6 +30,9 @@ Rails.application.configure do
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
+ # Disable request forgery protection in test environment.
+ config.action_controller.allow_forgery_protection = false
+
# Raises error for missing translations
# config.action_view.raise_on_missing_translations = true
end
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..20eec3c220 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
@@ -27,7 +27,7 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
test "attach new blob from an UploadedFile" do
file = file_fixture "racecar.jpg"
- @user.avatar.attach Rack::Test::UploadedFile.new file
+ @user.avatar.attach Rack::Test::UploadedFile.new file.to_s
assert_equal "racecar.jpg", @user.avatar.filename.to_s
end
@@ -56,6 +56,40 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
assert ActiveStorage::Blob.service.exist?(@user.avatar.key)
end
+ test "attach blob to new record" do
+ user = User.new(name: "Jason")
+
+ assert_no_changes -> { user.new_record? } do
+ assert_no_difference -> { ActiveStorage::Attachment.count } do
+ user.avatar.attach create_blob(filename: "funky.jpg")
+ end
+ end
+
+ assert user.avatar.attached?
+ assert_equal "funky.jpg", user.avatar.filename.to_s
+
+ assert_difference -> { ActiveStorage::Attachment.count }, +1 do
+ user.save!
+ end
+
+ assert user.reload.avatar.attached?
+ assert_equal "funky.jpg", user.avatar.filename.to_s
+ end
+
+ test "build new record with attached blob" do
+ assert_no_difference -> { ActiveStorage::Attachment.count } do
+ @user = User.new(name: "Jason", avatar: { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" })
+ end
+
+ assert @user.new_record?
+ assert @user.avatar.attached?
+ assert_equal "town.jpg", @user.avatar.filename.to_s
+
+ @user.save!
+ assert @user.reload.avatar.attached?
+ assert_equal "town.jpg", @user.avatar.filename.to_s
+ end
+
test "access underlying associations of new blob" do
@user.avatar.attach create_blob(filename: "funky.jpg")
assert_equal @user, @user.avatar_attachment.record
@@ -63,6 +97,52 @@ 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 "detach blob" do
+ @user.avatar.attach create_blob(filename: "funky.jpg")
+ avatar_blob_id = @user.avatar.blob.id
+ avatar_key = @user.avatar.key
+
+ @user.avatar.detach
+ assert_not @user.avatar.attached?
+ assert ActiveStorage::Blob.exists?(avatar_blob_id)
+ assert ActiveStorage::Blob.service.exist?(avatar_key)
+ end
+
test "purge attached blob" do
@user.avatar.attach create_blob(filename: "funky.jpg")
avatar_key = @user.avatar.key
@@ -114,6 +194,48 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
assert_equal "country.jpg", @user.highlights.second.filename.to_s
end
+ test "attach blobs to new record" do
+ user = User.new(name: "Jason")
+
+ assert_no_changes -> { user.new_record? } do
+ assert_no_difference -> { ActiveStorage::Attachment.count } do
+ user.highlights.attach(
+ { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" },
+ { io: StringIO.new("IT"), filename: "country.jpg", content_type: "image/jpg" })
+ end
+ end
+
+ assert user.highlights.attached?
+ assert_equal "town.jpg", user.highlights.first.filename.to_s
+ assert_equal "country.jpg", user.highlights.second.filename.to_s
+
+ assert_difference -> { ActiveStorage::Attachment.count }, +2 do
+ user.save!
+ end
+
+ assert user.reload.highlights.attached?
+ assert_equal "town.jpg", user.highlights.first.filename.to_s
+ assert_equal "country.jpg", user.highlights.second.filename.to_s
+ end
+
+ test "build new record with attached blobs" do
+ assert_no_difference -> { ActiveStorage::Attachment.count } do
+ @user = User.new(name: "Jason", highlights: [
+ { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" },
+ { io: StringIO.new("IT"), filename: "country.jpg", content_type: "image/jpg" }])
+ end
+
+ assert @user.new_record?
+ assert @user.highlights.attached?
+ assert_equal "town.jpg", @user.highlights.first.filename.to_s
+ assert_equal "country.jpg", @user.highlights.second.filename.to_s
+
+ @user.save!
+ assert @user.reload.highlights.attached?
+ assert_equal "town.jpg", @user.highlights.first.filename.to_s
+ assert_equal "country.jpg", @user.highlights.second.filename.to_s
+ end
+
test "find attached blobs" do
@user.highlights.attach(
{ io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" },
@@ -135,6 +257,68 @@ 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 "detach blobs" do
+ @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "wonky.jpg")
+ highlight_blob_ids = @user.highlights.collect { |highlight| highlight.blob.id }
+ highlight_keys = @user.highlights.collect(&:key)
+
+ @user.highlights.detach
+ assert_not @user.highlights.attached?
+
+ assert ActiveStorage::Blob.exists?(highlight_blob_ids.first)
+ assert ActiveStorage::Blob.exists?(highlight_blob_ids.second)
+
+ assert ActiveStorage::Blob.service.exist?(highlight_keys.first)
+ assert ActiveStorage::Blob.service.exist?(highlight_keys.second)
+ 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/gcs_service_test.rb b/activestorage/test/service/gcs_service_test.rb
index 5566c664a9..1860149da9 100644
--- a/activestorage/test/service/gcs_service_test.rb
+++ b/activestorage/test/service/gcs_service_test.rb
@@ -32,13 +32,8 @@ if SERVICE_CONFIGURATIONS[:gcs]
end
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%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: ActiveStorage::Filename.new("test.txt"), content_type: "text/plain")
- end
+ assert_match(/storage\.googleapis\.com\/.*response-content-disposition=inline.*test\.txt.*response-content-type=text%2Fplain/,
+ @service.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("test.txt"), content_type: "text/plain"))
end
end
else
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..aaf1d452ea 100644
--- a/activestorage/test/test_helper.rb
+++ b/activestorage/test/test_helper.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
-require File.expand_path("../../test/dummy/config/environment.rb", __FILE__)
+ENV["RAILS_ENV"] ||= "test"
+require_relative "dummy/config/environment.rb"
require "bundler/setup"
require "active_support"
@@ -23,7 +24,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 +34,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")