Derivatives
The derivatives
plugin allows storing processed files ("derivatives") alongside
the main attached file. The processed file data will be saved together with the
main attachment data in the same record attribute.
Shrine.plugin :derivatives
Quick start
You'll usually want to create derivatives from an attached file. The simplest way to do this is to define a processor which returns the processed files, and then trigger it when you want to create derivatives.
Here is an example of generating image thumbnails:
# Gemfile
gem "image_processing", "~> 1.8"
require "image_processing/mini_magick"
class ImageUploader < Shrine
Attacher.derivatives do |original|
magick = ImageProcessing::MiniMagick.source(original)
{
small: magick.resize_to_limit!(300, 300),
medium: magick.resize_to_limit!(500, 500),
large: magick.resize_to_limit!(800, 800),
}
end
end
photo = Photo.new(image: file)
photo.image_derivatives! # creates derivatives
photo.save
You can then retrieve the URL of a processed derivative:
photo.image_url(:large) #=> "https://s3.amazonaws.com/path/to/large.jpg"
The derivatives data is stored in the <attachment>_data
column alongside the
main file:
photo.image_data #=>
# {
# "id": "path/to/original.jpg",
# "store": "store",
# "metadata": { ... },
# "derivatives": {
# "small": { "id": "path/to/small.jpg", "storage": "store", "metadata": { ... } },
# "medium": { "id": "path/to/medium.jpg", "storage": "store", "metadata": { ... } },
# "large": { "id": "path/to/large.jpg", "storage": "store", "metadata": { ... } },
# }
# }
And they can be retrieved as Shrine::UploadedFile
objects:
photo.image(:large) #=> #<Shrine::UploadedFile id="path/to/large.jpg" storage=:store metadata={...}>
photo.image(:large).url #=> "https://s3.amazonaws.com/path/to/large.jpg"
photo.image(:large).size #=> 5825949
photo.image(:large).mime_type #=> "image/jpeg"
Retrieving derivatives
The list of stored derivatives can be retrieved with #<name>_derivatives
:
photo.image_derivatives #=>
# {
# small: #<Shrine::UploadedFile ...>,
# medium: #<Shrine::UploadedFile ...>,
# large: #<Shrine::UploadedFile ...>,
# }
A specific derivative can be retrieved in any of the following ways:
photo.image_derivatives[:small] #=> #<Shrine::UploadedFile ...>
photo.image_derivatives(:small) #=> #<Shrine::UploadedFile ...>
photo.image(:small) #=> #<Shrine::UploadedFile ...>
Or with nested derivatives:
photo.image_derivatives #=> { thumbnail: { small: ..., medium: ..., large: ... } }
photo.image_derivatives.dig(:thumbnail, :small) #=> #<Shrine::UploadedFile ...>
photo.image_derivatives(:thumbnail, :small) #=> #<Shrine::UploadedFile ...>
photo.image(:thumbnails, :small) #=> #<Shrine::UploadedFile ...>
Derivative URL
You can retrieve the URL of a derivative URL with #<name>_url
:
photo.image_url(:small) #=> "https://example.com/small.jpg"
photo.image_url(:medium) #=> "https://example.com/medium.jpg"
photo.image_url(:large) #=> "https://example.com/large.jpg"
For nested derivatives you can pass multiple keys:
photo.image_derivatives #=> { thumbnail: { small: ..., medium: ..., large: ... } }
photo.image_url(:thumbnail, :medium) #=> "https://example.com/medium.jpg"
By default, #<name>_url
method will return nil
if derivative is not found.
You can use the default_url
plugin to set up URL fallbacks:
Attacher.default_url do |derivative: nil, **|
"/fallbacks/#{derivative}.jpg" if derivative
end
photo.image_url(:medium) #=> "https://example.com/fallbacks/medium.jpg"
Any additional URL options passed to #<name>_url
will be forwarded to the
storage:
photo.image_url(:small, response_content_disposition: "attachment")
You can also retrieve the derivative URL via UploadedFile#url
:
photo.image_derivatives[:large].url
Attacher API
The derivatives API is primarily defined on the Shrine::Attacher
class, with
some important methods also being exposed through the Shrine::Attachment
module.
Here is a model example with equivalent attacher code:
photo.image_derivatives!(:thumbnails)
photo.image_derivatives #=> { ... }
photo.image_url(:large) #=> "https://..."
photo.image(:large) #=> #<Shrine::UploadedFile ...>
attacher.create_derivatives(:thumbnails)
attacher.get_derivatives #=> { ... }
attacher.url(:large) #=> "https://..."
attacher.get(:large) #=> "#<Shrine::UploadedFile>"
Creating derivatives
By default, the Attacher#create_derivatives
method downloads the attached
file, calls the processor, uploads results to attacher's permanent storage, and
saves uploaded files on the attacher.
attacher.file #=> #<Shrine::UploadedFile id="original.jpg" storage=:store ...>
attacher.create_derivatives # calls default processor and uploads results
attacher.derivatives #=>
# {
# small: #<Shrine::UploadedFile id="small.jpg" storage=:store ...>,
# medium: #<Shrine::UploadedFile id="medium.jpg" storage=:store ...>,
# large: #<Shrine::UploadedFile id="large.jpg" storage=:store ...>,
# }
Any additional arguments are forwarded to
Attacher#process_derivatives
:
attacher.create_derivatives(different_source) # pass a different source file
attacher.create_derivatives(foo: "bar") # pass custom options to the processor
Create on promote
You can also have derivatives created automatically on promotion:
Shrine.plugin :derivatives, create_on_promote: true
attacher.assign(file)
attacher.finalize # creates derivatives on promotion
attacher.derivatives #=> { small: ..., medium: ..., large: ... }
Naming processors
If you want to have multiple processors for an uploader, you can assign each processor a name:
class ImageUploader < Shrine
Attacher.derivatives :thumbnails do |original|
{ large: ..., medium: ..., small: ... }
end
Attacher.derivatives :crop do |original|
{ cropped: ... }
end
end
Then when creating derivatives you can specify the name of the desired processor. New derivatives will be merged with any existing ones.
attacher.create_derivatives(:thumbnails)
attacher.derivatives #=> { large: ..., medium: ..., small: ... }
attacher.create_derivatives(:crop)
attacher.derivatives #=> { large: ..., medium: ..., small: ..., cropped: ... }
Derivatives storage
By default, derivatives are uploaded to the permanent storage of the attacher.
You can change the destination storage by passing :storage
to the creation
call:
attacher.create_derivatives(storage: :cache) # will be promoted together with main file
attacher.create_derivatives(storage: :other_store)
You can also change the default destination storage with the :storage
plugin
option:
plugin :derivatives, storage: :other_store
The storage can be dynamic based on the derivative name:
plugin :derivatives, storage: -> (derivative) do
if derivative == :thumb
:thumbnail_store
else
:store
end
end
You can also set this option with Attacher.derivatives_storage
:
Attacher.derivatives_storage :other_store
# or
Attacher.derivatives_storage do |derivative|
if derivative == :thumb
:thumbnail_store
else
:store
end
end
The storage block is evaluated in the context of a Shrine::Attacher
instance:
Attacher.derivatives_storage do |derivative|
self #=> #<Shrine::Attacher>
file #=> #<Shrine::UploadedFile>
record #=> #<Photo>
name #=> :image
context #=> { ... }
# ...
end
Nesting derivatives
Derivatives can be nested to any level, using both hashes and arrays, but the top-level object must be a hash.
Attacher.derivatives :tiff do |original|
{
thumbnail: {
small: small,
medium: medium,
large: large,
},
layers: [
layer_1,
layer_2,
# ...
]
}
end
attacher.derivatives #=>
# {
# thumbnail: {
# small: #<Shrine::UploadedFile ...>,
# medium: #<Shrine::UploadedFile ...>,
# large: #<Shrine::UploadedFile ...>,
# },
# layers: [
# #<Shrine::UploadedFile ...>,
# #<Shrine::UploadedFile ...>,
# # ...
# ]
# }
Processing derivatives
A derivatives processor block takes the original file, and is expected to return a hash of processed files (it can be nested).
Attacher.derivatives :my_processor do |original|
# return a hash of processed files
end
The Attacher#create_derivatives
method internally calls
Attacher#process_derivatives
, which in turn calls the processor:
files = attacher.process_derivatives(:my_processor)
attacher.add_derivatives(files)
Dynamic processing
The processor block is evaluated in context of the Shrine::Attacher
instance,
which allows you to change your processing logic based on the record data.
Attacher.derivatives :my_processor do |original|
self #=> #<Shrine::Attacher>
file #=> #<Shrine::UploadedFile>
record #=> #<Photo>
name #=> :image
context #=> { ... }
# ...
end
Moreover, any options passed to Attacher#process_derivatives
will be
forwarded to the processor:
attacher.process_derivatives(:my_processor, foo: "bar")
Attacher.derivatives :my_processor do |original, **options|
options #=> { :foo => "bar" }
# ...
end
Source file
By default, the Attacher#process_derivatives
method will download the
attached file and pass it to the processor:
Attacher.derivatives :my_processor do |original|
original #=> #<File:...>
# ...
end
attacher.process_derivatives(:my_processor) # downloads attached file and passes it to the processor
If you want to use a different source file, you can pass it in to the process
call. Typically you'd pass a local file on disk. If you pass a
Shrine::UploadedFile
object or another IO-like object, it will be
automatically downloaded/copied to a local TempFile on disk.
# named processor:
attacher.process_derivatives(:my_processor, source_file)
# default processor:
attacher.process_derivatives(source_file)
If you want to call multiple processors in a row with the same source file, you can use this to avoid re-downloading the same source file each time:
attacher.file.download do |original|
attacher.process_derivatives(:thumbnails, original)
attacher.process_derivatives(:colors, original)
end
If a processor might not always need a local source file, you avoid a
potentially expensive download/copy by registering the processor with
download: false
, in which case the source file will be passed to the
processor as is.
Attacher.derivatives :my_processor, download: false do |source|
source #=> Could be File, Shrine::UploadedFile, or other IO-like object
shrine_class.with_file(source) do |file|
# can force download/copy if necessary with `with_file`,
end
end
Adding derivatives
If you already have processed files that you want to save, you can do that with
Attacher#add_derivatives
:
attacher.add_derivatives({
one: file_1,
two: file_2,
# ...
})
attacher.derivatives #=>
# {
# one: #<Shrine::UploadedFile>,
# two: #<Shrine::UploadedFile>,
# ...
# }
New derivatives will be merged with existing ones:
attacher.derivatives #=> { one: #<Shrine::UploadedFile> }
attacher.add_derivatives({ two: two_file })
attacher.derivatives #=> { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile> }
The merging is deep, so the following will work as well:
attacher.derivatives #=> { nested: { one: #<Shrine::UploadedFile> } }
attacher.add_derivatives({ nested: { two: two_file } })
attacher.derivatives #=> { nested: { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile> } }
For adding a single derivative, you can also use the singular
Attacher#add_derivative
:
attacher.add_derivative(:thumb, thumbnail_file)
Note that new derivatives will replace any existing derivatives living under the same key, but won't delete them. If this is your case, make sure to save a reference to the old derivatives before assigning new ones, and then delete them after persisting the change.
Any options passed to Attacher#add_derivative(s)
will be forwarded to
Attacher#upload_derivatives
.
attacher.add_derivative(:thumb, thumbnail_file, storage: :thumbnails_store) # specify destination storage
attacher.add_derivative(:thumb, thumbnail_file, upload_options: { acl: "public-read" }) # pass uploader options
The Attacher#add_derivative(s)
methods are thread-safe.
Uploading derivatives
If you want to upload processed files without setting them, you can use
Attacher#upload_derivatives
:
derivatives = attacher.upload_derivatives({
one: file_1,
two: file_2,
# ...
})
derivatives #=>
# {
# one: #<Shrine::UploadedFile>,
# two: #<Shrine::UploadedFile>,
# ...
# }
For uploading a single derivative, you can also use the singular
Attacher#upload_derivative
:
attacher.upload_derivative(:thumb, thumbnail_file)
#=> #<Shrine::UploadedFile>
Uploader options
You can specify the destination storage by passing :storage
option to
Attacher#upload_derivative(s)
. This will override the default derivatives
storage setting.
attacher.upload_derivative(:thumb, thumnbail_file, storage: :other_store)
#=> #<Shrine::UploadedFile @id="thumb.jpg" @storage_key=:other_store ...>
Any other options will be forwarded to the uploader:
attacher.upload_derivative :thumb, thumbnail_file,
upload_options: { acl: "public-read" },
metadata: { "foo" => "bar" }),
location: "path/to/derivative"
The :derivative
name is automatically passed to the uploader:
class MyUploader < Shrine
plugin :add_metadata
add_metadata :md5 do |io, derivative: nil, **|
calculate_signature(io, :md5) unless derivative
end
def generate_location(io, derivative: nil, **)
"location/for/#{derivative}"
end
plugin :upload_options, store: -> (io, derivative: nil, **) {
{ acl: "public-read" } if derivative
}
end
File deletion
Files given to Attacher#upload_derivative(s)
are assumed to be temporary, so
for convenience they're automatically closed and unlinked after upload.
If you want to disable this behaviour, pass delete: false
:
attacher.upload_derivative(:thumb, thumbnail_file, delete: false)
Merging derivatives
If you want to save already uploaded derivatives, you can use
Attacher#merge_derivatives
:
attacher.derivatives #=> { one: #<Shrine::UploadedFile> }
attacher.merge_derivatives attacher.upload_derivatives({ two: two_file })
attacher.derivatives #=> { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile> }
This does a deep merge, so the following will work as well:
attacher.derivatives #=> { nested: { one: #<Shrine::UploadedFile> } }
attacher.merge_derivatives attacher.upload_derivatives({ nested: { two: two_file } })
attacher.derivatives #=> { nested: { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile> } }
Note that new derivatives will replace any existing derivatives living under the same key, but won't delete them. If this is your case, make sure to save a reference to the old derivatives before assigning new ones, and then delete them after persisting the change.
The Attacher#merge_derivatives
method is thread-safe.
Setting derivatives
If instead of adding you want to override existing derivatives, you can use
Attacher#set_derivatives
:
attacher.derivatives #=> { one: #<Shrine::UploadedFile> }
attacher.set_derivatives attacher.upload_derivatives({ two: two_file })
attacher.derivatives #=> { two: #<Shrine::UploadedFile> }
If you're using the model
plugin, this method will trigger writing
derivatives data into the column attribute.
Promoting derivatives
Any assigned derivatives that are uploaded to temporary storage will be
automatically uploaded to permanent storage on Attacher#promote
.
attacher.derivatives[:one].storage_key #=> :cache
attacher.promote
attacher.derivatives[:one].storage_key #=> :store
If you want more control over derivatives promotion, you can use
Attacher#promote_derivatives
. Any additional options passed to it are
forwarded to the uploader.
attacher.derivatives[:one].storage_key #=> :cache
attacher.promote_derivatives(upload_options: { acl: "public-read" })
attacher.derivatives[:one].storage_key #=> :store
Removing derivatives
If you want to manually remove certain derivatives, you can do that with
Attacher#remove_derivative
.
attacher.derivatives #=> { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile> }
attacher.remove_derivative(:two) #=> #<Shrine::UploadedFile> (removed derivative)
attacher.derivatives #=> { one: #<Shrine::UploadedFile> }
You can also use the plural Attacher#remove_derivatives
for removing multiple
derivatives:
attacher.derivatives #=> { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile>, three: #<Shrine::UploadedFile> }
attacher.remove_derivative(:two, :three) #=> [#<Shrine::UploadedFile>, #<Shrine::UploadedFile>] (removed derivatives)
attacher.derivatives #=> { one: #<Shrine::UploadedFile> }
It's possible to remove nested derivatives as well:
attacher.derivatives #=> { nested: { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile> } }
attacher.remove_derivative([:nested, :one]) #=> #<Shrine::UploadedFile> (removed derivative)
attacher.derivatives #=> { nested: { one: #<Shrine::UploadedFile> } }
The removed derivatives are not automatically deleted, because it's safer to first persist the removal change, and only then perform the deletion.
derivative = attacher.remove_derivative(:two)
# ... persist removal change ...
derivative.delete
If you still want to delete the derivative at the time of removal, you can
pass delete: true
:
derivative = attacher.remove_derivative(:two, delete: true)
derivative.exists? #=> false
Deleting derivatives
If you want to delete a collection of derivatives, you can use
Attacher#delete_derivatives
:
derivatives #=> { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile> }
attacher.delete_derivatives(derivatives)
derivatives[:one].exists? #=> false
derivatives[:two].exists? #=> false
Without arguments Attacher#delete_derivatives
deletes current derivatives:
attacher.derivatives #=> { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile> }
attacher.delete_derivatives
attacher.derivatives[:one].exists? #=> false
attacher.derivatives[:two].exists? #=> false
Derivatives are automatically deleted on Attacher#destroy
.
Miscellaneous
Without original
You can store derivatives even if there is no main attached file:
attacher.file #=> nil
attacher.add_derivatives({ one: one_file, two: two_file })
attacher.data #=>
# {
# "derivatives" => {
# "one" => { "id" => "...", "storage" => "...", "metadata": { ... } },
# "two" => { "id" => "...", "storage" => "...", "metadata": { ... } },
# }
# }
However, note that in this case operations such as promotion and deletion will not be automatically triggered in the attachment flow, you'd need to trigger them manually as needed.
Iterating derivatives
If you want to iterate over a nested hash of derivatives (which can be
Shrine::UploadedFile
objects or raw files), you can use
Attacher#map_derivative
or Shrine.map_derivative
:
derivatives #=>
# {
# one: #<Shrine::UploadedFile>,
# two: { three: #<Shrine::UploadedFile> },
# four: [#<Shrine::UploadedFile>],
# }
# or Shrine.map_derivative
attacher.map_derivative(derivatives) do |name, file|
puts "#{name}, #{file}"
end
# output:
#
# :one, #<Shrine::UploadedFile>
# [:two, :three], #<Shrine::UploadedFile>
# [:four, 0], #<Shrine::UploadedFile>
Parsing derivatives
If you want to directly parse derivatives data written to a record attribute,
you can use Shrine.derivatives
(counterpart to Shrine.uploaded_file
):
# or MyUploader.derivatives
derivatives = Shrine.derivatives({
"one" => { "id" => "...", "storage" => "...", "metadata" => { ... } },
"two" => { "three" => { "id" => "...", "storage" => "...", "metadata" => { ... } } }
"four" => [{ "id" => "...", "storage" => "...", "metadata" => { ... } }]
})
derivatives #=>
# {
# one: #<Shrine::UploadedFile>,
# two: { three: #<Shrine::UploadedFile> },
# four: [#<Shrine::UploadedFile>],
# }
Like Shrine.uploaded_file
, the Shrine.derivatives
method accepts data as a
hash (stringified or symbolized) or a JSON string.
Marshalling
The Attacher
instance uses a mutex to make Attacher#merge_derivatives
thread-safe, which is not marshallable. If you want to be able to marshal the
attacher instance, you can skip mutex usage:
plugin :derivatives, mutex: false
Instrumentation
If the instrumentation
plugin has been loaded, the derivatives
plugin adds
instrumentation around derivatives processing.
# instrumentation plugin needs to be loaded *before* derivatives
plugin :instrumentation
plugin :derivatives
Processing derivatives will trigger a derivatives.shrine
event with the
following payload:
Key | Description |
---|---|
:processor | Name of the derivatives processor |
:processor_options | Any options passed to the processor |
:io | The source file passed to the processor |
:attacher | The attacher instance doing the processing |
:uploader | The uploader class that sent the event |
A default log subscriber is added as well which logs these events:
Derivatives (2133ms) – {:processor=>:thumbnails, :processor_options=>{}, :uploader=>ImageUploader}
You can also use your own log subscriber:
plugin :derivatives, log_subscriber: -> (event) {
Shrine.logger.info JSON.generate(name: event.name, duration: event.duration, **event.payload)
}
{"name":"derivatives","duration":2133,"processor":"thumbnails","processor_options":{},"io":"#<File:...>","uploader":"ImageUploader"}
Or disable logging altogether:
plugin :derivatives, log_subscriber: nil