title: Shrine 3.0.0

This guide covers all the changes in the 3.0.0 version of Shrine. If you’re currently using Shrine 2.x, see Upgrading to Shrine 3.x for instructions on how to upgrade.

Major features

Derivatives

The new derivatives plugin has been added for storing additional processed files alongside the main file.

Shrine.plugin :derivatives
class ImageUploader < Shrine
  Attacher.derivatives_processor 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(photo_params)
photo.image_derivatives! # creates derivatives
photo.save

This is a rewrite of the versions plugin, bringing numerous improvements:

“‘rb photo.image_data #=> # { # “id”: “original.jpg”, # “storage”: “store”, # “metadata”: { … }, # “derivatives”: { # “large”: { “id”: “large.jpg”, “storage”: “store”, “metadata”: { … } }, # “medium”: { “id”: “medium.jpg”, “storage”: “store”, “metadata”: { … } }, # “small”: { “id”: “small.jpg”, “storage”: “store”, “metadata”: { … } } # } # }

photo.image #=> #<Shrine::UploadedFile id=“original.jpg” …> photo.image_derivatives #=> # { # large: #<Shrine::UploadedFile id=“large.jpg” …>, # medium: #<Shrine::UploadedFile id=“medium.jpg” …>, # small: #<Shrine::UploadedFile id=“small.jpg” …>, # } “‘

rb photo = Photo.create(image: file) # promote original file to permanent storage photo.image_derivatives! # generate derivatives after promotion photo.save # save derivatives data

“‘rb class ImageUploader < Shrine Attacher.derivatives_processor :thumbnails do |original| # … end

Attacher.derivatives_processor :crop do |original, left:, top:, width:, height:|
  vips = ImageProcessing::Vips.source(original)

  { cropped: vips.crop!(left, top, width, height) }
end

end rb photo.image_derivatives!(:thumbnails) photo.image_derivatives #=> { large: …, medium: …, small: … } photo.save

# … sometime later …

photo.image_derivatives!(:crop, left: 0, top: 0, width: 300, height: 300) photo.image_derivatives #=> { large: …, medium: …, small: …, cropped: … } photo.save “‘

“‘rb class ImageUploader < Shrine # specify storage for all derivatives Attacher.derivatives_storage :other_store

# or specify storage per derivative
Attacher.derivatives_storage { |derivative| :other_store }

end rb photo = Photo.create(image: file) photo.image.storage_key #=> :store

photo.image_derivatives! photo.image_derivatives.storage_key #=> :other_store “‘

Attacher redesign

The Shrine::Attacher class has been rewritten and can now be used without models:

attacher = Shrine::Attacher.new
attacher.attach(file)
attacher.file #=> #<Shrine::UploadedFile>

The Attacher#data, Attacher#load_data, and Attacher.from_data methods have been added for dumping and loading the attached file:

# dump attached file into a serializable Hash
data = attacher.data #=> { "id" => "abc123.jpg", "storage" => "store", "metadata" => { ... } }
# initialize attacher from attached file data...
attacher = Shrine::Attacher.from_data(data)
attacher.file #=> #<Shrine::UploadedFile id="abc123.jpg" storage=:store metadata={...}>

# ...or load attached file into an existing attacher
attacher = Shrine::Attacher.new
attacher.load_data(data)
attacher.file #=> #<Shrine::UploadedFile>

Several more methods have been added:

Column

The new column plugin adds the ability to serialize attached file data, in format suitable for writing into a database column.

Shrine.plugin :column
# dump attached file data into a JSON string
data = attacher.column_data #=> '{"id":"abc123.jpg","storage":"store","metadata":{...}}'
# initialize attacher from attached file data...
attacher = Shrine::Attacher.from_column(data)
attacher.file #=> #<Shrine::UploadedFile id="abc123.jpg" storage=:store metadata={...}>

# ...or load attached file into an existing attacher
attacher = Shrine::Attacher.new
attacher.load_column(data)
attacher.file #=> #<Shrine::UploadedFile>

Entity

The new entity plugin adds support for immutable structs, which are commonly used with ROM, Hanami and dry-rb.

Shrine.plugin :entity
class Photo < Hanami::Entity
  include Shrine::Attachment(:image)
end
photo = Photo.new(image_data: '{"id":"abc123.jpg","storage":"store","metadata":{...}}')
photo.image #=> #<Shrine::UploadedFile id="abc123.jpg" storage=:store ...>

Model

The new model plugin adds support for mutable structs, which is used by activerecord and sequel plugins.

Shrine.plugin :model
class Photo < Struct.new(:image_data)
  include Shrine::Attachment(:image)
end
photo = Photo.new
photo.image = file
photo.image #=> #<Shrine::UploadedFile id="abc123.jpg" storage=:cache ...>
photo.image_data #=> #=> '{"id":"abc123.jpg", "storage":"cache", "metadata":{...}}'

Backgrounding rewrite

Shrine.plugin :backgrounding
Shrine::Attacher.promote_block do
  PromoteJob.perform_async(self.class.name, record.class.name, record.id, name, file_data)
end
Shrine::Attacher.destroy_block do
  DestroyJob.perform_async(self.class.name, 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.atomic_promote
  rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
    # attachment has changed or the record has been deleted, nothing to do
  end
end
class DestroyJob
  include Sidekiq::Worker

  def perform(attacher_class, data)
    attacher_class = Object.const_get(attacher_class)

    attacher = attacher_class.from_data(data)
    attacher.destroy
  end
end

There are several main differences compared to the old implementation:

We can now also register backgrounding hooks on an attacher instance, allowing us to pass additional parameters to the background job:

photo = Photo.new(photo_params)

photo.image_attacher.promote_block do |attacher|
  PromoteJob.perform_async(
    attacher.class.name,
    attacher.record.class.name,
    attacher.record.id,
    attacher.name,
    attacher.file_data,
    current_user.id, # <== parameters from the controller
  )
end

photo.save # will call our instance-level backgrounding hook

Persistence interface

The persistence plugins (activerecord, sequel) now implement a unified persistence interface:

Method Description
Attacher#persist persists attachment data
Attacher#atomic_persist persists attachment data if attachment hasn’t changed
Attacher#atomic_promote promotes cached file and atomically persists changes

The “atomic” methods use the new atomic_helpers plugin, and are useful for background jobs. For example, this is how we’d use them to implement metadata extraction in the background in a concurrency-safe way:

MetadataJob.perform_async(
  attacher.class.name,
  attacher.record.class.name,
  attacher.record.id,
  attacher.name,
  attacher.file_data,
)
class MetadataJob
  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.refresh_metadata! # extract metadata
    attacher.atomic_persist    # persist if attachment hasn't changed
  rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
    # attachment has changed or record has been deleted, nothing to do
  end
end

Other new plugins

“‘rb Shrine.storages = { cache: …, store: …, backup: … }

Shrine.plugin :mirroring, mirror: { store: :backup } rb file = Shrine.upload(io, :store) # uploads to :store and :backup file.delete # deletes from :store and :backup “‘

“‘rb Shrine.storages = { cache: …, cache_one: …, cache_two: …, store: … }

Shrine.plugin :multi_cache, additional_cache: [:cache_one, :cache_two] rb photo.image = { “id” => “…”, “storage” => “cache”, “metadata” => { … } } photo.image.storage_key #=> :cache # or photo.image = { “id” => “…”, “storage” => “cache_one”, “metadata” => { … } } photo.image.storage_key #=> :cache_one # or photo.image = { “id” => “…”, “storage” => “cache_two”, “metadata” => { … } } photo.image.storage_key #=> :cache_two “‘

rb Shrine.plugin :form_assign rb attacher = photo.image_attacher attacher.form_assign({ "image" => file, "title" => "...", "description" => "..." }) attacher.file #=> #<Shrine::UploadedFile id="..." storage=:cache ...>

Other features

rb Shrine.plugin :model, cache: false rb photo.image = file photo.image.storage_key #=> :store (permanent storage)

rb Rails.application.routes.draw do get "/attachments" => "files#download" end “‘rb class FilesController < ApplicationController def download # … we can now perform things like authentication here … set_rack_response Shrine.download_response(env) end

private

def set_rack_response((status, headers, body))
  self.status = status
  self.headers.merge!(headers)
  self.response_body = body
end

end “‘

rb attacher.file.refresh_metadata! attacher.write # can now be shortened to attacher.refresh_metadata!

rb class Photo include ImageUploader::Attachment(:image) end rb Photo.image_attacher #=> #<ImageUploader::Attacher ...>

“‘rb require “oj” # github.com/ohler55/oj

Shrine.plugin :column, serializer: Oj “‘

rb attacher.assign(file, validate: { foo: "bar" }) rb class MyUploader < Shrine Attacher.validate do |**options| options #=> { foo: "bar" } end end

rb attacher.attach(file, validate: false) # skip validation

rb storage.delete_prefixed("some_directory/")

Performance improvements

rb photo = Photo.find(photo_id) photo.image # parses and loads attached file photo.image # returns memoized attached file

Other Improvements

Core improvements

rb # Gemfile gem "shrine-memory" # this can be removed

“‘rb photo.image = { “id” => “…”, “storage” => “cache”, “metadata” => { … } } ““

rb Shrine.uploaded_file(id: "...", storage: :store, metadata: { ... })

rb file = uploader.upload(StringIO.new("some text"), metadata: { "filename" => "file.txt" }) file.id #=> "2a2467ee6acbc5cb.txt"

rb class Photo include ImageUploader::Attachment[:image] end

Plugin improvements

rb uploaded_file.url(response_content_disposition: "attachment") “‘rb plugin :url_options, store: -> (io, options) { disposition = options.delete(:response_content_disposition, “inline”)

{
  response_content_disposition: ContentDisposition.format(
    disposition: disposition,
    filename:    io.original_filename,
  )
}

} “‘

rb Attacher.default_cache { ... } Attacher.default_store { ... }

“‘rb plugin :pretty_location # “blogpost/aa357797-5845-451b-8662-08eecdc9f762/image/493g82jf23.jpg”

plugin :pretty_location, class_underscore: :true # “blog_post/aa357797-5845-451b-8662-08eecdc9f762/image/493g82jf23.jpg” “‘

Backwards compatibility

Plugin deprecation and removal

Attacher API

Attachment API

Uploader API

Uploaded File API

rb uploaded_file.id #=> "foo" uploaded_file.data["id"] = "bar" uploaded_file.id #=> "foo"

“‘rb # previous behaviour uploaded_file.storage_key #=> “store”

# new behaviour uploaded_file.storage_key #=> :store “‘

Storage API

“‘rb # this won’t work anymore def open(id) # … end

# this is now required def open(id, **options) # … end “‘

S3 API

FileSystem API

Plugins API