—
title: File Processing¶ ↑
Shrine
allows you to process attached files eagerly or on-the-fly. For example, if your app is accepting image uploads, you can generate a predefined set of of thumbnails when the image is attached to a record, or you can have thumbnails generated dynamically as they’re needed.
How you’re going to implement processing is entirely up to you. For images it’s recommended to use the {ImageProcessing}[https://github.com/janko/image_processing] gem, which provides wrappers for processing with MiniMagick and libvips. Here is an example of generating a thumbnail with ImageProcessing:
$ brew install imagemagick
# Gemfile gem "image_processing", "~> 1.8"
require "image_processing/mini_magick" thumbnail = ImageProcessing::MiniMagick .source(image) # input file .resize_to_limit(600, 400) # resize macro .colorspace("grayscale") # custom operation .convert("jpeg") # output type .saver(quality: 90) # output options .call # run the pipeline thumbnail #=> #<Tempfile:...> (a 600x400 thumbnail of the source image)
Eager processing¶ ↑
Let’s say we’re handling images, and want to generate a predefined set of thumbnails with various dimensions. We can use the {derivatives
}[https://shrinerb.com/docs/plugins/derivatives] plugin to upload and save the processed files:
Shrine.plugin :derivatives
require "image_processing/mini_magick" class ImageUploader < Shrine Attacher.derivatives do |original| magick = ImageProcessing::MiniMagick.source(original) { large: magick.resize_to_limit!(800, 800), medium: magick.resize_to_limit!(500, 500), small: magick.resize_to_limit!(300, 300), } end end
photo = Photo.new(image: file) if photo.valid? photo.image_derivatives! if photo.image_changed? # creates derivatives photo.save end
After the processed files are uploaded, their data is saved into the <attachment>_data
column. You can then retrieve the derivatives as {Shrine::UploadedFile
} objects:
photo.image(:large) #=> #<Shrine::UploadedFile ...> photo.image(:large).url #=> "/uploads/store/lg043.jpg" photo.image(:large).size #=> 5825949 photo.image(:large).mime_type #=> "image/jpeg"
Conditional derivatives¶ ↑
The Attacher.derivatives
block is evaluated in context of a Shrine::Attacher
instance:
Attacher.derivatives do |original| self #=> #<Shrine::Attacher> file #=> #<Shrine::UploadedFile> record #=> #<Photo> name #=> :image context #=> { ... } # ... end
This gives you the ability to branch the processing logic based on the attachment information:
Attacher.derivatives do |original| magick = ImageProcessing::MiniMagick.source(original) result = {} if record.is_a?(Photo) result[:jpg] = magick.convert!("jpeg") result[:gray] = magick.colorspace!("grayscale") end if file.mime_type == "image/svg+xml" result[:png] = magick.loader(transparent: "white").convert!("png") end result end
The {type_predicates
} plugin provides convenient predicate methods for branching based on the file type.
Backgrounding¶ ↑
Since file processing can be time consuming, it’s recommended to move it into a background job.
A) Creating derivatives with promotion¶ ↑
The simplest way is to use the {backgrounding
} plugin to move promotion into a background job, and then create derivatives as part of promotion:
Shrine.plugin :backgrounding Shrine::Attacher.promote_block do PromoteJob.perform_async(self.class.name, record.class.name, record.id, name, file_data) end
class PromoteJob include Sidekiq::Worker def perform(attacher_class, record_class, record_id, name, file_data) attacher_class = Object.const_get(attacher_class) record = Object.const_get(record_class).find(record_id) # if using Active Record attacher = attacher_class.retrieve(model: record, name: name, file: file_data) attacher.create_derivatives # calls derivatives processor attacher.atomic_promote rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound # attachment has changed or the record has been deleted, nothing to do end end
B) Creating derivatives separately from promotion¶ ↑
Derivatives don’t need to be created as part of the attachment flow, you can create them at any point after promotion:
DerivativesJob.perform_async( attacher.class.name, attacher.record.class.name, attacher.record.id, attacher.name, attacher.file_data, )
class DerivativesJob include Sidekiq::Worker def perform(attacher_class, record_class, record_id, name, file_data) attacher_class = Object.const_get(attacher_class) record = Object.const_get(record_class).find(record_id) # if using Active Record attacher = attacher_class.retrieve(model: record, name: name, file: file_data) attacher.create_derivatives # calls derivatives processor attacher.atomic_persist rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound attacher&.destroy_attached # delete now orphaned derivatives end end
C) Creating derivatives concurrently¶ ↑
You can also generate derivatives concurrently:
class ImageUploader < Shrine THUMBNAILS = { large: [800, 800], medium: [500, 500], small: [300, 300], } Attacher.derivatives do |original, name:| thumbnail = ImageProcessing::MiniMagick .source(original) .resize_to_limit!(*THUMBNAILS.fetch(name)) { name => thumbnail } end end
ImageUploader::THUMBNAILS.each_key do |derivative_name| DerivativeJob.perform_async( attacher.class.name, attacher.record.class.name, attacher.record.id, attacher.name, attacher.file_data, derivative_name, ) end
class DerivativeJob include Sidekiq::Worker def perform(attacher_class, record_class, record_id, name, file_data, derivative_name) attacher_class = Object.const_get(attacher_class) record = Object.const_get(record_class).find(record_id) # if using Active Record attacher = attacher_class.retrieve(model: record, name: name, file: file_data) attacher.create_derivatives(name: derivative_name) attacher.atomic_persist do |reloaded_attacher| # make sure we don't override derivatives created in other jobs attacher.merge_derivatives(reloaded_attacher.derivatives) end rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound attacher.derivatives[derivative_name].delete # delete now orphaned derivative end end
URL fallbacks¶ ↑
If you’re creating derivatives in a background job, you’ll likely want to use some fallbacks for derivative URLs while the background job is still processing. You can do that with the {default_url
} plugin.
Shrine.plugin :default_url
A) Fallback to original¶ ↑
You can fall back to the original file URL when the derivative is missing:
Attacher.default_url do |derivative: nil, **| file&.url if derivative end
photo.image_url(:large) #=> "https://example.com/path/to/original.jpg" # ... background job finishes ... photo.image_url(:large) #=> "https://example.com/path/to/large.jpg"
B) Fallback to derivative¶ ↑
You can fall back to another derivative URL when the derivative is missing:
Attacher.default_url do |derivative: nil, **| derivatives[:optimized]&.url if derivative end
photo.image_url(:large) #=> "https://example.com/path/to/optimized.jpg" # ... background job finishes ... photo.image_url(:large) #=> "https://example.com/path/to/large.jpg"
C) Fallback to on-the-fly¶ ↑
You can also fall back to on-the-fly processing, which should generally provide the best user experience.
THUMBNAILS = { small: [300, 300], medium: [500, 500], large: [800, 800], } Attacher.default_url do |derivative: nil, **| file&.derivation_url(:thumbnail, *THUMBNAILS.fetch(derivative)) if derivative end
photo.image_url(:large) #=> "../derivations/thumbnail/800/800/..." # ... background job finishes ... photo.image_url(:large) #=> "https://example.com/path/to/large.jpg"
On-the-fly processing¶ ↑
Having eagerly created image thumbnails can be a pain to maintain, because whenever you need to add a new version or change an existing one, you need to retroactively apply it to all existing attachments (see the Managing Derivatives guide for more details).
Sometimes it makes more sense to generate thumbnails dynamically as they’re requested, and then cache them for future requests. This strategy is known as processing “on-the-fly” or “on-demand”, and it’s suitable for short-running processing such as creating image thumbnails or document previews.
Shrine
provides on-the-fly processing functionality via the {derivation_endpoint
}[https://shrinerb.com/docs/plugins/derivation_endpoint] plugin. You set it up by loading the plugin with a secret key (you generate this yourself, maybe via something like SecureRandom.hex
) and a path prefix, mount its Rack app in your routes on the configured path prefix, and define processing you want to perform:
# config/initializers/shrine.rb (Rails) # ... Shrine.plugin :derivation_endpoint, secret_key: "<SHRINE_SECRET_KEY>"
require "image_processing/mini_magick" class ImageUploader < Shrine plugin :derivation_endpoint, prefix: "derivations/image" # matches mount point derivation :thumbnail do |file, width, height| ImageProcessing::MiniMagick .source(file) .resize_to_limit!(width.to_i, height.to_i) end end
# config/routes.rb (Rails) Rails.application.routes.draw do # ... mount ImageUploader.derivation_endpoint => "/derivations/image" end
Now you can generate thumbnail URLs from attached files, and the actual thumbnail will be generated when the URL is requested:
photo.image.derivation_url(:thumbnail, 600, 400) #=> "/derivations/image/thumbnail/600/400/eyJpZCI6ImZvbyIsInN0b3JhZ2UiOiJzdG9yZSJ9?signature=..."
The plugin is highly customizable, be sure to check out the documentation, especially the performance section.
Dynamic derivation¶ ↑
If you have multiple types of transformations and don’t want to have a derivation for each one, you can set up a single derivation that applies any series of transformations:
class ImageUploader < Shrine derivation :transform do |original, transformations| transformations = Shrine.urlsafe_deserialize(transformations) vips = ImageProcessing::Vips.source(original) vips.apply!(transformations) end end
photo.image.derivation_url :transform, Shrine.urlsafe_serialize( crop: [10, 10, 500, 500], resize_to_fit: [300, 300], gaussblur: 1, )
You can create a helper method for convenience:
def derivation_url(file, transformations) file.derivation_url(:transform, Shrine.urlsafe_serialize(transformations)) end
derivation_url photo.image, crop: [10, 10, 500, 500], resize_to_fit: [300, 300], gaussblur: 1
Processing other filetypes¶ ↑
So far we’ve only been talking about processing images. However, there is nothing image-specific in Shrine’s processing API, you can just as well process any other types of files. The processing tool doesn’t need to have any special Shrine
integration, the ImageProcessing gem that we saw earlier is a completely generic gem.
To demonstrate, here is an example of transcoding videos using streamio-ffmpeg:
# Gemfile gem "streamio-ffmpeg"
require "streamio-ffmpeg" class VideoUploader < Shrine Attacher.derivatives do |original| transcoded = Tempfile.new ["transcoded", ".mp4"] screenshot = Tempfile.new ["screenshot", ".jpg"] movie = FFMPEG::Movie.new(original.path) movie.transcode(transcoded.path) movie.screenshot(screenshot.path) { transcoded: transcoded, screenshot: screenshot } end end
Polymorphic uploader¶ ↑
Sometimes you might want an attachment attribute to accept multiple types of files, and apply different processing depending on the type. Since Shrine’s processing blocks are evaluated dynamically, you can use conditional logic:
class PolymorphicUploader < Shrine IMAGE_TYPES = %w[image/jpeg image/png image/webp] VIDEO_TYPES = %w[video/mp4 video/quicktime] PDF_TYPES = %w[application/pdf] Attacher.validate do validate_mime_type IMAGE_TYPES + VIDEO_TYPES + PDF_TYPES # ... end Attacher.derivatives do |original| case file.mime_type when *IMAGE_TYPES then process_derivatives(:image, original) when *VIDEO_TYPES then process_derivatives(:video, original) when *PDF_TYPES then process_derivatives(:pdf, original) end end Attacher.derivatives :image do |original| # ... end Attacher.derivatives :video do |original| # ... end Attacher.derivatives :pdf do |original| # ... end end
Extras¶ ↑
Automatic derivatives¶ ↑
If you would like derivatives to be automatically created with promotion, you can use the create_on_promote
option built-in to the derivatives plugin.
class Shrine::Attacher plugin :derivatives, create_on_promote: true end
This shouldn’t be needed if you’re processing in the background, as in that case you have a background worker that will be called for each attachment, so you can call Attacher#create_derivatives
there.
libvips¶ ↑
As mentioned, ImageProcessing gem also has an alternative backend for processing images with {libvips}[https://libvips.github.io/libvips/]. libvips is a full-featured image processing library like ImageMagick, with impressive performance characteristics – it’s often multiple times faster than ImageMagick and has low memory usage (see Why is libvips quick).
Using libvips is as easy as installing it and switching to the {ImageProcessing::Vips
} backend:
$ brew install vips
# Gemfile gem "image_processing", "~> 1.8"
require "image_processing/vips" # all we did was replace `ImageProcessing::MiniMagick` with `ImageProcessing::Vips` thumbnail = ImageProcessing::Vips .source(image) .resize_to_limit!(600, 400) thumbnail #=> #<Tempfile:...> (a 600x400 thumbnail of the source image)
Parallelize uploads¶ ↑
If you’re generating derivatives, you can parallelize the uploads using the concurrent-ruby gem:
# Gemfile gem "concurrent-ruby"
require "concurrent" derivatives = attacher.process_derivatives tasks = derivatives.map do |name, file| Concurrent::Promises.future(name, file) do |name, file| attacher.add_derivative(name, file) end end Concurrent::Promises.zip(*tasks).wait!
External processing¶ ↑
You can also integrate Shrine
with 3rd-party processing services such as Cloudinary and Imgix. In the most common case, you’d serve images directly from these services, see the corresponding plugin docs for more details (shrine-cloudinary, shrine-imgix and others)
You can also choose to use these services as an implementation detail of your application, by downloading the processed images and saving them to your storage. Here is how you might store files processed by Imgix as derivatives:
# Gemfile gem "down", "~> 5.0" gem "http", "~> 4.0" gem "shrine-imgix", "~> 0.5"
Shrine.plugin :derivatives Shrine.plugin :imgix, client: { host: "my-app.imgix.net", secure_url_token: "secret" }
require "down/http" class ImageUploader < Shrine IMGIX_THUMBNAIL = -> (file, width, height) do Down::Http.download(file.imgix_url(w: width, h: height)) end Attacher.derivatives do { large: IMGIX_THUMBNAIL[file, 800, 800], medium: IMGIX_THUMBNAIL[file, 500, 500], small: IMGIX_THUMBNAIL[file, 300, 300], } end end