Better File Uploads with Shrine: Attachment
In the previous post I talked about the foundation of Shrine’s design. In this post I want to show you Shrine’s high-level interface for attaching uploaded files to model instances, which builds upon this foundation.
Attachment
With most file attachment libraries, in order to create a file attachment attribute on the model, you first extend your model with a module, and then you use the gained class methods to create attachment attributes.
class Photo
extend CarrierWave::Mount # done automatically by CarrierWave
mount_uploader :image, ImageUploader
end
However, CarrierWave here adds a total of 5 class methods and 24 instance methods to your model, which to me is a lot of pollution. Other file attachment libraries are better in this regard, though.
Shrine takes a cleaner approach here. With Shrine you use your uploader to
generate an attachment module for a certain attribute, and then you include
it
directly to your model. This is called the module builder pattern.
class Photo
include ImageUploader::Attachment(:image)
end
This way for a single attachment Shrine adds only 4 instance methods and
1 class method to your model by default. Singe table inheritance
inheritance is supported as well (Paperclip and CarrierWave don’t support STI).
The included Shrine::Attachment
module will be nicely displayed when listing
model ancestors, because it’s not an anonymous module:
Photo.ancestors #=>
# [
# Photo,
# #<ImageUploader::Attachment(image)>,
# Object,
# BasicObject,
# ]
Attaching
Single column
As we talked about in the previous post, when a Shrine uploader uploads a given
file, it returns a Shrine::UploadedFile
object. This object contains
the storage name it was uploaded to, the location, and the metadata extracted
before upload.
Shrine’s attacher persists this information into a single database column, by
converting the Shrine::UploadedFile
object into its JSON represntation.
add_column :photos, :image_data # only a single column is used for the attachment
Paperclip, for example, mandates 4 columns for an attached file. Refile and Dragonfly also require additional columns if you want to save additional file metadata. CarrierWave doesn’t have native support for additional metadata, but you can use carrierwave-meta, though it’s ActiveRecord-specific and image-specific, and pollutes your model with all the metadata methods.
Temporary & permanent storage
Shrine uses a temporary and permanent storage when attaching. When a file is assigned, it is uploaded to temporary storage, and then after validations pass and record is saved, the cached file is reuploaded to permanent storage.
Shrine.storages = {
cache: Shrine::Storage::FileSystem.new("public", prefix: "cache"), # temporary
store: Shrine::Storage::FileSystem.new("public", prefix: "store"), # permanent
}
photo = Photo.new
photo.image = file # Saves the file to temporary storage
photo.image_data #=> '{"storage":"cache","id":"ds9ga94.jpg","metadata":{...}}'
photo.save # Promotes the file from temporary to permanent storage
photo.image_data #=> '{"storage":"store","id":"l0fgla8.jpg","metadata":{...}}'
photo.image #=> #<Shrine::UploadedFile>
This separation of temporary and permanent storage enables features like retaining the uploaded file in case of validation errors, direct uploads and backgrounding, without the possibility of having orphan files in your main storage.
Presence
While CarrierWave and Paperclip provide #present?
and #blank?
methods to
check whether a file is attached, Shrine will simply return nil
if there is
no file attached.
photo.image # returns either `Shrine::UploadedFile` or `nil`
Location
When attaching an uploaded file, CarrierWave and Paperclip store only the filename to the database column, and the full location to the file is generated dynamically from your configured directory.
This is not a good design decision, because it makes it very difficult to migrate files to a new directory. If you try to first change the directory option to a new directory, all URLs for the existing files will now point at the wrong location, because those files are still in the old location. If you however try to first move files themselves, the URLs would again start pointing to the wrong location, because files are now located at the new location.
class ImageUploader < CarrierWave::Uploader::Base
def store_dir
"#{model.name.downcase}/#{model.id}"
end
end
# Only the filename is saved, the path is always dynamically generated
photo.attributes[:image] #=> "nature.jpg"
Shrine learns from this mistake, and instead saves the whole generated path to the attachment column. And if you change how the location is generated, all existing files will still remain fully accessible, because their location is still read directly from the column. Then later you can move them manually if you want.
class ImageUploader < Shrine
plugin :pretty_location
end
# Shrine saves the full path to the file
photo.attributes[:image_data] #=> '{"id":"photo/45/image/d0sg8fglf.jpg",...}'
ORM integration
To use Shrine with an ORM, you just need to load the ORM plugin, which will automatically add callbacks and validations when an attachment module is included.
Shrine.plugin :sequel # :activerecord
Shrine’s ORM implementation is much simpler than CarrierWave’s, which means that writing new ORM integrations is also simpler. One reason for this simplicity is that Shrine properly utilizes dirty tracking by writing the cached file to the attachment column on assignment, while most other file attachment libraries have to add <attribute>_will_change! hacks everywhere where the attachment could change, so that callbacks are always invoked.
Shrine ships with plugins for Sequel and ActiveRecord, but there are also external plugins for Mongoid and Hanami::Model. A ROM plugin is also in the making.
Attacher
The model interface provided by the Shrine::Attachment
module is just a thin
wrapper around a Shrine::Attacher
object (inspired by Refile), which you can
also use directly:
attacher = ImageUploader::Attacher.from_model(photo, :image)
attacher.assign(file) # equivalent to `photo.image = file`
attacher.get # equivalent to `photo.image`
attacher.url # equivalent to `photo.image_url`
So if you prefer not to add any additional methods to your model, and prefer
explicitness over callbacks, you can simply use Shrine::Attacher
directly
without including the attachment module to your model. See the Using Attacher
guide for more examples.
Validations
Shrine supports validating attached files, and ships with a
validation_helpers
plugin which provides methods for common file validations.
class ImageUploader < Shrine
plugin :validation_helpers
Attacher.validate do
validate_mime_type %w[image/jpeg image/png image/webp]
validate_extension %w[jpg jpeg png webp]
validate_max_size 10*1024*1024 # 10 MB
validate_max_dimensions [5000, 5000]
end
end
Inspired by Sequel validations, Shrine validations are performed at the instance level (as opposed to using a class-level DSL), which means that you can use regular Ruby conditionals and do custom file validation. For example, you could validate maximum duration of a video:
class VideoUploader < Shrine
Attacher.validate do
if file.duration > 5*60*60
errors << "must not be longer than 5 hours"
end
end
end
Conclusion
We learned about two new Shrine core classes. One is Shrine::Attachment
, a
subclass of Module
, which can generate attachment modules for adding file
attachment attributes to your models. The other one is Shrine::Attacher
,
which is in charge of the actual file attachment logic, and can be used
directly.
Combined with Shrine
and Shrine::UploadedFile
, these are the 4 core classes
of Shrine. In future posts I will talk about all the advanced features that are
possible with these core classes. The next post will be about file processing
with Shrine, so stay tuned!