Last Update: 2021-09-30 20:09:06 +0200

title: The Design of Shrine

If you want an in-depth walkthrough through the Shrine codebase, see {Notes on study of shrine implementation}[] article by Jonathan Rochkind.

There are five main types of classes that you deal with in Shrine:

| Class | Description | | :—- | :———- | | {Shrine::Storage::*} | Manages files on a particular storage service | | {Shrine} | Wraps uploads and handles loading plugins | | {Shrine::UploadedFile} | Represents a file uploaded to a storage | | {Shrine::Attacher} | Handles file attachment logic | | {Shrine::Attachment} | Provides convenience model attachment interface |


On the lowest level we have a storage. A storage class encapsulates file management logic on a particular service. It is what actually performs uploads, generation of URLs, deletions and similar. By convention it is namespaced under Shrine::Storage::*.

filesystem ="uploads")
filesystem.upload(file, "foo")
filesystem.url("foo") #=> "uploads/foo"

A storage is a PORO which implements the following interface:

class Shrine
  module Storage
    class MyStorage
      def upload(io, id, shrine_metadata: {}, **upload_options)
        # uploads `io` to the location `id`

      def open(id, **options)
        # returns the remote file as an IO-like object

      def exists?(id)
        # checks if the file exists on the storage

      def delete(id)
        # deletes the file from the storage

      def url(id, **options)
        # URL to the remote file, accepts options for customizing the URL

Storages are typically not used directly, but through {Shrine} and {Shrine::UploadedFile} classes.


The Shrine class (also called an “uploader”) primarily provides a wrapper method around Storage#upload. First, the storage needs to be registered under a name:

Shrine.storages[:disk] ="uploads")

Now we can upload files to the registered storage:

uploaded_file = Shrine.upload(file, :disk)
uploaded_file #=> #<Shrine::UploadedFile storage=:disk id="6a9fb596cc554efb" ...>

The argument to Shrine#upload must be an IO-like object. The method does the following:

  • generates a unique location

  • extracts metadata

  • uploads the file (calls Storage#upload)

  • closes the file

  • creates a Shrine::UploadedFile from the data


The Shrine class is also used for loading plugins, which provide additional functionality by extending core classes.

Shrine.plugin :derivatives

Shrine::UploadedFile.ancestors #=> [..., Shrine::Plugins::Derivatives::FileMethods, Shrine::UploadedFile::InstanceMethods, ...]
Shrine::Attacher.ancestors     #=> [..., Shrine::Plugins::Derivatives::AttacherMethods, Shrine::Attacher::InstanceMethods,  ...]
Shrine::Attachment.ancestors   #=> [..., Shrine::Plugins::Derivatives::AttachmentMethods, Shrine::Attachment::InstanceMethods, ...]

The plugins store their configuration in Shrine.opts:

Shrine.plugin :derivation_endpoint, secret_key: "foo"
Shrine.plugin :default_storage, store: :other_store
Shrine.plugin :activerecord

Shrine.opts #=>
# { derivation_endpoint: { options: { secret_key: "foo" }, derivations: {} },
#   default_storage: { store: :other_store },
#   column: { serializer: Shrine::Plugins::Column::JsonSerializer },
#   model: { cache: true },
#   activerecord: { callbacks: true, validations: true } }

Each Shrine subclass has its own copy of the core classes, storages and options, which makes it possible to customize attachment logic per uploader.

MyUploader =
MyUploader::UploadedFile.superclass #=> Shrine::UploadedFile
MyUploader::Attacher.superclass     #=> Shrine::Attacher
MyUploader::Attachment.superclass   #=> Shrine::Attachment

See Creating a New Plugin guide and the Plugin system of Sequel and Roda article for more details on the design of Shrine’s plugin system.


A Shrine::UploadedFile object represents a file that was uploaded to a storage, containing upload location, storage, and any metadata extracted during the upload.

uploaded_file #=> #<Shrine::UploadedFile id="949sdjg834.jpg" storage=:store metadata={...}>          #=> "949sdjg834.jpg"
uploaded_file.storage_key #=> :store     #=> #<Shrine::Storage::S3>
uploaded_file.metadata    #=> {...}

It has convenience methods for accessing metadata:

uploaded_file.metadata #=>
# {
#   "filename" => "matrix.mp4",
#   "mime_type" => "video/mp4",
#   "size" => 345993,
# }

uploaded_file.original_filename #=> "matrix.mp4"
uploaded_file.extension         #=> "mp4"
uploaded_file.mime_type         #=> "video/mp4"
uploaded_file.size              #=> 345993

It also has methods that delegate to the storage:

uploaded_file.url                     #=> "" { |io| ... }       # opens the uploaded file stream { |file| ... } # downloads the uploaded file to disk     # streams uploaded content into a writable destination
uploaded_file.exists?                 #=> true
uploaded_file.delete                  # deletes the uploaded file from the storage

A Shrine::UploadedFile is itself an IO-like object (built on top of Storage#open), so it can be passed to Shrine#upload as well.


We usually want to treat uploaded files as attachments to records, saving their data into a database column. This is done by Shrine::Attacher, which internally uses Shrine and Shrine::UploadedFile classes.

The attaching process requires a temporary and a permanent storage to be registered (by default that’s :cache and :store):

Shrine.storages = {

A Shrine::Attacher can be initialized standalone and handle the common attachment flow, which includes dirty tracking (promoting cached file to permanent storage, deleting previously attached file), validation, processing, serialization etc.

attacher =

# ... user uploads a file ...

attacher.assign(io) # uploads to temporary storage
attacher.file       #=> #<Shrine::UploadedFile storage=:cache ...>

# ... handle file validations ...

attacher.finalize   # uploads to permanent storage
attacher.file       #=> #<Shrine::UploadedFile storage=:store ...>

It can also be initialized with a model instance to handle serialization into a model attribute:

attacher = Shrine::Attacher.from_model(photo, :image)

photo.image_data #=> "{\"storage\":\"cache\",\"id\":\"9260ea09d8effd.jpg\",\"metadata\":{...}}"

photo.image_data #=> "{\"storage\":\"store\",\"id\":\"ksdf02lr9sf3la.jpg\",\"metadata\":{...}}"

For more details, see the Using Attacher guide and {entity}[]/{model}[] plugins.


A Shrine::Attachment module provides a convenience model interface around the Shrine::Attacher object. The Shrine::Attachment class is a subclass of Module, which means that an instance of Shrine::Attachment is a module: #=> true #=> [:image=, :image, :image_url, :image_attacher, ...]

# equivalents

We can include this module into a model:

Photo.include Shrine::Attachment(:image)
photo.image = file   # shorthand for `photo.image_attacher.assign(file)`
photo.image          # shorthand for `photo.image_attacher.get`
photo.image_url      # shorthand for `photo.image_attacher.url`

photo.image_attacher #=> #<Shrine::Attacher @cache_key=:cache @store_key=:store ...>

When a persistence plugin is loaded ({activerecord}, {sequel}), the Shrine::Attachment module also automatically:

  • syncs Shrine’s validation errors with the record

  • triggers promoting after record is saved

  • deletes the uploaded file if attachment was replaced or the record destroyed