aboutsummaryrefslogtreecommitdiffstats
path: root/activestorage/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'activestorage/app/models')
-rw-r--r--activestorage/app/models/active_storage/attachment.rb2
-rw-r--r--activestorage/app/models/active_storage/blob.rb12
-rw-r--r--activestorage/app/models/active_storage/blob/identifiable.rb11
-rw-r--r--activestorage/app/models/active_storage/blob/representable.rb16
-rw-r--r--activestorage/app/models/active_storage/current.rb5
-rw-r--r--activestorage/app/models/active_storage/filename.rb2
-rw-r--r--activestorage/app/models/active_storage/identification.rb38
-rw-r--r--activestorage/app/models/active_storage/preview.rb9
-rw-r--r--activestorage/app/models/active_storage/variant.rb79
-rw-r--r--activestorage/app/models/active_storage/variation.rb78
10 files changed, 141 insertions, 111 deletions
diff --git a/activestorage/app/models/active_storage/attachment.rb b/activestorage/app/models/active_storage/attachment.rb
index 19f48c57d6..c59877a9a5 100644
--- a/activestorage/app/models/active_storage/attachment.rb
+++ b/activestorage/app/models/active_storage/attachment.rb
@@ -14,7 +14,7 @@ class ActiveStorage::Attachment < ActiveRecord::Base
delegate_missing_to :blob
- after_create_commit :identify_blob, :analyze_blob_later
+ after_create_commit :analyze_blob_later, :identify_blob
# Synchronously purges the blob (deletes it from the configured service) and destroys the attachment.
def purge
diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb
index a1e69e2264..0cd4ad8128 100644
--- a/activestorage/app/models/active_storage/blob.rb
+++ b/activestorage/app/models/active_storage/blob.rb
@@ -14,7 +14,13 @@
# update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file.
# If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one.
class ActiveStorage::Blob < ActiveRecord::Base
- include Analyzable, Identifiable, Representable
+ require_dependency "active_storage/blob/analyzable"
+ require_dependency "active_storage/blob/identifiable"
+ require_dependency "active_storage/blob/representable"
+
+ include Analyzable
+ include Identifiable
+ include Representable
self.table_name = "active_storage_blobs"
@@ -25,6 +31,8 @@ class ActiveStorage::Blob < ActiveRecord::Base
has_many :attachments
+ scope :unattached, -> { left_joins(:attachments).where(ActiveStorage::Attachment.table_name => { blob_id: nil }) }
+
class << self
# You can used the signed ID of a blob to refer to it on the client side without fear of tampering.
# This is particularly helpful for direct uploads where the client-side needs to refer to the blob
@@ -194,4 +202,6 @@ class ActiveStorage::Blob < ActiveRecord::Base
def forcibly_serve_as_binary?
ActiveStorage.content_types_to_serve_as_binary.include?(content_type)
end
+
+ ActiveSupport.run_load_hooks(:active_storage_blob, self)
end
diff --git a/activestorage/app/models/active_storage/blob/identifiable.rb b/activestorage/app/models/active_storage/blob/identifiable.rb
index 40ca84ac70..049e45dc3e 100644
--- a/activestorage/app/models/active_storage/blob/identifiable.rb
+++ b/activestorage/app/models/active_storage/blob/identifiable.rb
@@ -2,10 +2,19 @@
module ActiveStorage::Blob::Identifiable
def identify
- ActiveStorage::Identification.new(self).apply
+ update! content_type: identify_content_type, identified: true unless identified?
end
def identified?
identified
end
+
+ private
+ def identify_content_type
+ Marcel::MimeType.for download_identifiable_chunk, name: filename.to_s, declared_type: content_type
+ end
+
+ def download_identifiable_chunk
+ service.download_chunk key, 0...4.kilobytes
+ end
end
diff --git a/activestorage/app/models/active_storage/blob/representable.rb b/activestorage/app/models/active_storage/blob/representable.rb
index 0ad2e2fd77..03d5511481 100644
--- a/activestorage/app/models/active_storage/blob/representable.rb
+++ b/activestorage/app/models/active_storage/blob/representable.rb
@@ -10,7 +10,7 @@ module ActiveStorage::Blob::Representable
# Returns an ActiveStorage::Variant instance with the set of +transformations+ provided. This is only relevant for image
# files, and it allows any image to be transformed for size, colors, and the like. Example:
#
- # avatar.variant(resize: "100x100").processed.service_url
+ # avatar.variant(resize_to_fit: [100, 100]).processed.service_url
#
# This will create and process a variant of the avatar blob that's constrained to a height and width of 100px.
# Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
@@ -18,16 +18,16 @@ module ActiveStorage::Blob::Representable
# Frequently, though, you don't actually want to transform the variant right away. But rather simply refer to a
# specific variant that can be created by a controller on-demand. Like so:
#
- # <%= image_tag Current.user.avatar.variant(resize: "100x100") %>
+ # <%= image_tag Current.user.avatar.variant(resize_to_fit: [100, 100]) %>
#
- # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController
+ # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
# can then produce on-demand.
#
# Raises ActiveStorage::InvariableError if ImageMagick cannot transform the blob. To determine whether a blob is
# variable, call ActiveStorage::Blob#variable?.
def variant(transformations)
if variable?
- ActiveStorage::Variant.new(self, ActiveStorage::Variation.wrap(transformations))
+ ActiveStorage::Variant.new(self, transformations)
else
raise ActiveStorage::InvariableError
end
@@ -43,19 +43,19 @@ module ActiveStorage::Blob::Representable
# from a non-image blob. Active Storage comes with built-in previewers for videos and PDF documents. The video previewer
# extracts the first frame from a video and the PDF previewer extracts the first page from a PDF document.
#
- # blob.preview(resize: "100x100").processed.service_url
+ # blob.preview(resize_to_fit: [100, 100]).processed.service_url
#
# Avoid processing previews synchronously in views. Instead, link to a controller action that processes them on demand.
# Active Storage provides one, but you may want to create your own (for example, if you need authentication). Here’s
# how to use the built-in version:
#
- # <%= image_tag video.preview(resize: "100x100") %>
+ # <%= image_tag video.preview(resize_to_fit: [100, 100]) %>
#
# This method raises ActiveStorage::UnpreviewableError if no previewer accepts the receiving blob. To determine
# whether a blob is accepted by any previewer, call ActiveStorage::Blob#previewable?.
def preview(transformations)
if previewable?
- ActiveStorage::Preview.new(self, ActiveStorage::Variation.wrap(transformations))
+ ActiveStorage::Preview.new(self, transformations)
else
raise ActiveStorage::UnpreviewableError
end
@@ -69,7 +69,7 @@ module ActiveStorage::Blob::Representable
# Returns an ActiveStorage::Preview for a previewable blob or an ActiveStorage::Variant for a variable image blob.
#
- # blob.representation(resize: "100x100").processed.service_url
+ # blob.representation(resize_to_fit: [100, 100]).processed.service_url
#
# Raises ActiveStorage::UnrepresentableError if the receiving blob is neither variable nor previewable. Call
# ActiveStorage::Blob#representable? to determine whether a blob is representable.
diff --git a/activestorage/app/models/active_storage/current.rb b/activestorage/app/models/active_storage/current.rb
new file mode 100644
index 0000000000..7e431d8462
--- /dev/null
+++ b/activestorage/app/models/active_storage/current.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ActiveStorage::Current < ActiveSupport::CurrentAttributes #:nodoc:
+ attribute :host
+end
diff --git a/activestorage/app/models/active_storage/filename.rb b/activestorage/app/models/active_storage/filename.rb
index 2b8880716e..bebb5e61b3 100644
--- a/activestorage/app/models/active_storage/filename.rb
+++ b/activestorage/app/models/active_storage/filename.rb
@@ -3,6 +3,8 @@
# Encapsulates a string representing a filename to provide convenient access to parts of it and sanitization.
# A Filename instance is returned by ActiveStorage::Blob#filename, and is comparable so it can be used for sorting.
class ActiveStorage::Filename
+ require_dependency "active_storage/filename/parameters"
+
include Comparable
class << self
diff --git a/activestorage/app/models/active_storage/identification.rb b/activestorage/app/models/active_storage/identification.rb
deleted file mode 100644
index 4f295257ae..0000000000
--- a/activestorage/app/models/active_storage/identification.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-class ActiveStorage::Identification
- attr_reader :blob
-
- def initialize(blob)
- @blob = blob
- end
-
- def apply
- blob.update!(content_type: content_type, identified: true) unless blob.identified?
- end
-
- private
- def content_type
- Marcel::MimeType.for(identifiable_chunk, name: filename, declared_type: declared_content_type)
- end
-
-
- def identifiable_chunk
- Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |client|
- client.get(uri, "Range" => "0-4096").body
- end
- end
-
- def uri
- @uri ||= URI.parse(blob.service_url)
- end
-
-
- def filename
- blob.filename.to_s
- end
-
- def declared_content_type
- blob.content_type
- end
-end
diff --git a/activestorage/app/models/active_storage/preview.rb b/activestorage/app/models/active_storage/preview.rb
index be5053edae..de58763399 100644
--- a/activestorage/app/models/active_storage/preview.rb
+++ b/activestorage/app/models/active_storage/preview.rb
@@ -21,10 +21,9 @@
#
# Outside of a Rails application, modify +ActiveStorage.previewers+ instead.
#
-# The built-in previewers rely on third-party system libraries:
-#
-# * {ffmpeg}[https://www.ffmpeg.org]
-# * {mupdf}[https://mupdf.com]
+# The built-in previewers rely on third-party system libraries. Specifically, the built-in video previewer requires
+# {ffmpeg}[https://www.ffmpeg.org]. Two PDF previewers are provided: one requires {Poppler}[https://poppler.freedesktop.org],
+# and the other requires {mupdf}[https://mupdf.com] (version 1.8 or newer). To preview PDFs, install either Poppler or mupdf.
#
# These libraries are not provided by Rails. You must install them yourself to use the built-in previewers. Before you
# install and use third-party software, make sure you understand the licensing implications of doing so.
@@ -39,7 +38,7 @@ class ActiveStorage::Preview
# Processes the preview if it has not been processed yet. Returns the receiving Preview instance for convenience:
#
- # blob.preview(resize: "100x100").processed.service_url
+ # blob.preview(resize_to_fit: [100, 100]).processed.service_url
#
# Processing a preview generates an image from its blob and attaches the preview image to the blob. Because the preview
# image is stored with the blob, it is only generated once.
diff --git a/activestorage/app/models/active_storage/variant.rb b/activestorage/app/models/active_storage/variant.rb
index e08a2271ec..b782489a92 100644
--- a/activestorage/app/models/active_storage/variant.rb
+++ b/activestorage/app/models/active_storage/variant.rb
@@ -6,36 +6,52 @@ require "active_storage/downloading"
# These variants are used to create thumbnails, fixed-size avatars, or any other derivative image from the
# original.
#
-# Variants rely on {MiniMagick}[https://github.com/minimagick/minimagick] for the actual transformations
-# of the file, so you must add <tt>gem "mini_magick"</tt> to your Gemfile if you wish to use variants.
+# Variants rely on {ImageProcessing}[https://github.com/janko-m/image_processing] gem for the actual transformations
+# of the file, so you must add <tt>gem "image_processing"</tt> to your Gemfile if you wish to use variants. By
+# default, images will be processed with {ImageMagick}[http://imagemagick.org] using the
+# {MiniMagick}[https://github.com/minimagick/minimagick] gem, but you can also switch to the
+# {libvips}[http://jcupitt.github.io/libvips/] processor operated by the {ruby-vips}[https://github.com/jcupitt/ruby-vips]
+# gem).
#
-# Note that to create a variant it's necessary to download the entire blob file from the service and load it
-# into memory. The larger the image, the more memory is used. Because of this process, you also want to be
-# considerate about when the variant is actually processed. You shouldn't be processing variants inline in a
-# template, for example. Delay the processing to an on-demand controller, like the one provided in
-# ActiveStorage::VariantsController.
+# Rails.application.config.active_storage.variant_processor
+# # => :mini_magick
+#
+# Rails.application.config.active_storage.variant_processor = :vips
+# # => :vips
+#
+# Note that to create a variant it's necessary to download the entire blob file from the service. Because of this process,
+# you also want to be considerate about when the variant is actually processed. You shouldn't be processing variants inline
+# in a template, for example. Delay the processing to an on-demand controller, like the one provided in
+# ActiveStorage::RepresentationsController.
#
# To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided
# by Active Storage like so:
#
-# <%= image_tag Current.user.avatar.variant(resize: "100x100") %>
+# <%= image_tag Current.user.avatar.variant(resize_to_fit: [100, 100]) %>
#
-# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController
+# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
# can then produce on-demand.
#
# When you do want to actually produce the variant needed, call +processed+. This will check that the variant
# has already been processed and uploaded to the service, and, if so, just return that. Otherwise it will perform
# the transformations, upload the variant to the service, and return itself again. Example:
#
-# avatar.variant(resize: "100x100").processed.service_url
+# avatar.variant(resize_to_fit: [100, 100]).processed.service_url
#
# This will create and process a variant of the avatar blob that's constrained to a height and width of 100.
# Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
#
-# A list of all possible transformations is available at https://www.imagemagick.org/script/mogrify.php. You can
-# combine as many as you like freely:
+# You can combine any number of ImageMagick/libvips operations into a variant, as well as any macros provided by the
+# ImageProcessing gem (such as +resize_to_fit+):
+#
+# avatar.variant(resize_to_fit: [800, 800], monochrome: true, flip: "-90")
+#
+# Visit the following links for a list of available ImageProcessing commands and ImageMagick/libvips operations:
#
-# avatar.variant(resize: "100x100", monochrome: true, flip: "-90")
+# * {ImageProcessing::MiniMagick}[https://github.com/janko-m/image_processing/blob/master/doc/minimagick.md#methods]
+# * {ImageMagick reference}[https://www.imagemagick.org/script/mogrify.php]
+# * {ImageProcessing::Vips}[https://github.com/janko-m/image_processing/blob/master/doc/vips.md#methods]
+# * {ruby-vips reference}[http://www.rubydoc.info/gems/ruby-vips/Vips/Image]
class ActiveStorage::Variant
include ActiveStorage::Downloading
@@ -65,7 +81,7 @@ class ActiveStorage::Variant
# it allows permanent URLs that redirect to the +service_url+ to be cached in the view.
#
# Use <tt>url_for(variant)</tt> (or the implied form, like +link_to variant+ or +redirect_to variant+) to get the stable URL
- # for a variant that points to the ActiveStorage::VariantsController, which in turn will use this +service_call+ method
+ # for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method
# for its redirection.
def service_url(expires_in: service.url_expires_in, disposition: :inline)
service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
@@ -82,10 +98,10 @@ class ActiveStorage::Variant
end
def process
- open_image do |image|
- transform image
- format image
- upload image
+ download_blob_to_tempfile do |image|
+ transform image do |output|
+ upload output
+ end
end
end
@@ -102,31 +118,18 @@ class ActiveStorage::Variant
blob.content_type.presence_in(WEB_IMAGE_CONTENT_TYPES) || "image/png"
end
-
- def open_image(&block)
- image = download_image
+ def transform(image)
+ format = "png" unless WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type)
+ result = variation.transform(image, format: format)
begin
- yield image
+ yield result
ensure
- image.destroy!
+ result.close!
end
end
- def download_image
- require "mini_magick"
- MiniMagick::Image.create { |file| download_blob_to(file) }
- end
-
- def transform(image)
- variation.transform(image)
- end
-
- def format(image)
- image.format("PNG") unless WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type)
- end
-
- def upload(image)
- File.open(image.path, "r") { |file| service.upload(key, file) }
+ def upload(file)
+ service.upload(key, file)
end
end
diff --git a/activestorage/app/models/active_storage/variation.rb b/activestorage/app/models/active_storage/variation.rb
index 12e7f9f0b5..42f00beb82 100644
--- a/activestorage/app/models/active_storage/variation.rb
+++ b/activestorage/app/models/active_storage/variation.rb
@@ -6,17 +6,9 @@
# In case you do need to use this directly, it's instantiated using a hash of transformations where
# the key is the command and the value is the arguments. Example:
#
-# ActiveStorage::Variation.new(resize: "100x100", monochrome: true, trim: true, rotate: "-90")
+# ActiveStorage::Variation.new(resize_to_fit: [100, 100], monochrome: true, trim: true, rotate: "-90")
#
-# You can also combine multiple transformations in one step, e.g. for center-weighted cropping:
-#
-# ActiveStorage::Variation.new(combine_options: {
-# resize: "100x100^",
-# gravity: "center",
-# crop: "100x100+0+0",
-# })
-#
-# A list of all possible transformations is available at https://www.imagemagick.org/script/mogrify.php.
+# The options map directly to {ImageProcessing}[https://github.com/janko-m/image_processing] commands.
class ActiveStorage::Variation
attr_reader :transformations
@@ -51,10 +43,49 @@ class ActiveStorage::Variation
@transformations = transformations
end
- # Accepts an open MiniMagick image instance, like what's returned by <tt>MiniMagick::Image.read(io)</tt>,
- # and performs the +transformations+ against it. The transformed image instance is then returned.
- def transform(image)
+ # Accepts a File object, performs the +transformations+ against it, and
+ # saves the transformed image into a temporary file. If +format+ is specified
+ # it will be the format of the result image, otherwise the result image
+ # retains the source format.
+ def transform(file, format: nil)
ActiveSupport::Notifications.instrument("transform.active_storage") do
+ if processor
+ image_processing_transform(file, format)
+ else
+ mini_magick_transform(file, format)
+ end
+ end
+ end
+
+ # Returns a signed key for all the +transformations+ that this variation was instantiated with.
+ def key
+ self.class.encode(transformations)
+ end
+
+ private
+ # Applies image transformations using the ImageProcessing gem.
+ def image_processing_transform(file, format)
+ operations = transformations.inject([]) do |list, (name, argument)|
+ if name.to_s == "combine_options"
+ ActiveSupport::Deprecation.warn("The ImageProcessing ActiveStorage variant backend doesn't need :combine_options, as it already generates a single MiniMagick command. In Rails 6.1 :combine_options will not be supported anymore.")
+ list.concat argument.to_a
+ else
+ list << [name, argument]
+ end
+ end
+
+ processor
+ .source(file)
+ .loader(page: 0)
+ .convert(format)
+ .apply(operations)
+ .call
+ end
+
+ # Applies image transformations using the MiniMagick gem.
+ def mini_magick_transform(file, format)
+ image = MiniMagick::Image.new(file.path, file)
+
transformations.each do |name, argument_or_subtransformations|
image.mogrify do |command|
if name.to_s == "combine_options"
@@ -66,15 +97,24 @@ class ActiveStorage::Variation
end
end
end
+
+ image.format(format) if format
+
+ image.tempfile.tap(&:open)
end
- end
- # Returns a signed key for all the +transformations+ that this variation was instantiated with.
- def key
- self.class.encode(transformations)
- end
+ # Returns the ImageProcessing processor class specified by `ActiveStorage.variant_processor`.
+ def processor
+ begin
+ require "image_processing"
+ rescue LoadError
+ ActiveSupport::Deprecation.warn("Using mini_magick gem directly is deprecated and will be removed in Rails 6.1. Please add `gem 'image_processing', '~> 1.2'` to your Gemfile.")
+ return nil
+ end
+
+ ImageProcessing.const_get(ActiveStorage.variant_processor.to_s.camelize) if ActiveStorage.variant_processor
+ end
- private
def pass_transform_argument(command, method, argument)
if eligible_argument?(argument)
command.public_send(method, argument)