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 objects that you deal with in Shrine:
- Storage
Shrine
Shrine::UploadedFile
Shrine::Attacher
Shrine::Attachment
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 = Shrine::Storage::FileSystem.new("uploads")
filesystem.upload(file, "foo")
filesystem.url("foo") #=> "uploads/foo"
filesystem.delete("foo")
A storage is a PORO which responds to certain methods:
class Shrine
module Storage
class MyStorage
def upload(io, id, shrine_metadata: {}, **upload_options)
# uploads `io` to the location `id`
end
def open(id)
# returns the remote file as an IO-like object
end
def exists?(id)
# checks if the file exists on the storage
end
def delete(id)
# deletes the file from the storage
end
def url(id, options = {})
# URL to the remote file, accepts options for customizing the URL
end
end
end
end
Storages are typically not used directly, but through Shrine
.
A Shrine
object (also called an "uploader") is essentially a wrapper around
the #upload
storage method. First the storage needs to be registered under a
name:
Shrine.storages[:file_system] = Shrine::Storage::FileSystem.new("uploads")
Now we can instantiate an uploader with this identifier, and upload files:
uploader = Shrine.new(:file_system)
uploaded_file = uploader.upload(file)
uploaded_file #=> #<Shrine::UploadedFile>
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
Shrine
class and subclasses are also used for loading plugins that extend all
core classes. Each Shrine
subclass has its own subclass of each of the core
classes (Shrine::UploadedFile
, Shrine::Attacher
, and Shrine::Attachment
),
which makes it possible to have different Shrine
subclasses with differently
customized attachment logic. 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.
Shrine::UploadedFile
represents a file that was uploaded to a storage, and is
the result of Shrine#upload
. It is essentially a wrapper around a data hash
containing information about the uploaded file.
uploaded_file #=> #<Shrine::UploadedFile>
uploaded_file.data #=>
# {
# "storage" => "file_system",
# "id" => "9260ea09d8effd.pdf",
# "metadata" => {
# "filename" => "resume.pdf",
# "mime_type" => "application/pdf",
# "size" => 983294,
# },
# }
The data hash contains the storage the file was uploaded to, the location, and
some metadata: original filename, MIME type and filesize. The
Shrine::UploadedFile
object has handy methods which use this data:
# metadata methods
uploaded_file.original_filename
uploaded_file.mime_type
uploaded_file.size
# ...
# storage methods
uploaded_file.url
uploaded_file.exists?
uploaded_file.open
uploaded_file.download
uploaded_file.delete
# ...
A Shrine::UploadedFile
is itself an IO-like object (representing the
remote file), 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 the responsibility of
Shrine::Attacher
. A Shrine::Attacher
uses Shrine
uploaders and
Shrine::UploadedFile
objects internally.
The attaching process requires a temporary and a permanent storage to be
registered (by default that's :cache
and :store
):
Shrine.storages = {
cache: Shrine::Storage::FileSystem.new("uploads/cache"),
store: Shrine::Storage::FileSystem.new("uploads/store"),
}
A Shrine::Attacher
is instantiated with a model instance and an attachment
name (an "image" attachment will be saved to image_data
field):
attacher = Shrine::Attacher.new(photo, :image)
attacher.assign(file)
attacher.get #=> #<Shrine::UploadedFile>
attacher.record.image_data #=> "{\"storage\":\"cache\",\"id\":\"9260ea09d8effd.jpg\",\"metadata\":{...}}"
attacher._promote
attacher.get #=> #<Shrine::UploadedFile>
attacher.record.image_data #=> "{\"storage\":\"store\",\"id\":\"ksdf02lr9sf3la.jpg\",\"metadata\":{...}}"
Above a file is assigned by the attacher, which "caches" (uploads) the file to
the temporary storage. The cached file is then "promoted" (uploaded) to
permanent storage. Behind the scenes a cached Shrine::UploadedFile
is given
to Shrine#upload
, which works because Shrine::UploadedFile
is an IO-like
object. After both caching and promoting the data hash of the uploaded file is
assigned to the record's column as JSON.
For more details see Using Attacher.
Shrine::Attachment
is the highest level of abstraction. A
Shrine::Attachment
module exposes the Shrine::Attacher
object through the
model instance. The Shrine::Attachment
class is a sublcass of Module
, which
means that an instance of Shrine::Attachment
is a module:
Shrine::Attachment.new(:image).is_a?(Module) #=> true
Shrine::Attachment.new(:image).instance_methods #=> [:image=, :image, :image_url, :image_attacher]
# equivalents
Shrine::Attachment.new(:image)
Shrine.attachment(:image)
Shrine[:image]
We can include this module to a model:
class Photo
include Shrine::Attachment.new(:image)
end
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>
When an ORM plugin is loaded, 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/removed or the record destroyed