This guide is aimed at helping Refile users transition to Shrine, and it consists of three parts:
- Explanation of the key differences in design between Refile and Shrine
- Instructions how to migrate and existing app that uses Refile to Shrine
- Extensive reference of Refile's interface with Shrine equivalents
Shrine borrows many great concepts from Refile: Refile's "backends" are here named "storages", it uses the same IO abstraction for uploading and representing uploaded files, similar attachment logic, and direct uploads are also supported.
While in Refile you work with storages directly, Shrine uses uploaders which act as wrappers around storages:
storage = Shrine.storages[:store]
storage #=> #<Shrine::Storage::S3 ...>
uploader = Shrine.new(:store)
uploader #=> #<Shrine @storage_key=:store @storage=#<Shrine::Storage::S3>>
uploader.storage #=> #<Shrine::Storage::S3 ...>
uploaded_file = uploader.upload(image)
uploaded_file #=> #<Shrine::UploadedFile>
This way Shrine can perform tasks like generating location, extracting metadata, processing, and logging, which are all storage-agnostic, and leave storages to deal only with actual file storage. And these tasks can be configured differently depending on the types of files you're uploading:
class ImageUploader < Shrine
add_metadata :exif do |io, context|
MiniMagick::Image.new(io).exif
end
end
class VideoUploader < Shrine
add_metadata :duration do |io, context|
FFMPEG::Movie.new(io.path).duration
end
end
Refile implements on-the-fly processing, serving all files through the Rack endpoint. However, it doesn't offer any abilities for processing on upload. Shrine, on the other hand, generates URLs to specific storages and offers processing on upload (like CarrierWave and Paperclip), but doesn't support on-the-fly processing.
The reason for this decision is that an image server is a completely separate responsibility, and it's better to use any of the generic services for on-the-fly processing. Shrine already has integrations for many such services: shrine-cloudinary, shrine-imgix, and shrine-uploadcare. There is even an open-source solution, Attache, which you can also use with Shrine.
This is how you would process multiple versions in Shrine:
class ImageUploader < Shrine
include ImageProcessing::MiniMagick
plugin :processing
plugin :versions
process(:store) do |io, context|
size_800 = resize_to_limit(io.download, 800, 800)
size_500 = resize_to_limit(size_800, 500, 500)
size_300 = resize_to_limit(size_500, 300, 300)
{original: size_800, medium: size_500, small: size_300}
end
end
While Refile serves all files through the Rack endpoint mounted in your app, Shrine serves files directly from storage services:
Refile.attachment_url(@photo, :image) #=> "/attachments/cache/50dfl833lfs0gfh.jpg"
@photo.image.url #=> "https://my-bucket.s3.amazonaws.com/cache/50dfl833lfs0gfh.jpg"
If you're using storage which don't expose files over URL (e.g. a database storage), or you want to secure your downloads, you can also serve files through your app using the download_endpoint plugin.
While in Refile you configure attachments by passing options to .attachment
,
in Shrine you define all your uploading logic inside uploaders, and then
generate an attachment module with that uploader which is included into the
model:
class Photo < Sequel::Model
extend Shrine::Sequel::Attachment
attachment :image, destroy: false
end
class ImageUploader < Shrine
plugin :sequel
plugin :keep_files, destroyed: true
end
class Photo < Sequel::Model
include ImageUploader::Attachment.new(:image)
end
This way we can encapsulate all attachment logic inside a class and share it between different models.
Refile allows you to save additional metadata about uploaded files in additional
columns, so you can define <attachment>_filename
, <attachment>_content_type
,
or <attachment>_size
.
Shrine, on the other hand, saves all metadata into a single <attachment>_data
column:
photo.image_data #=>
# {
# "storage" => "store",
# "id" => "photo/1/image/0d9o8dk42.png",
# "metadata" => {
# "filename" => "nature.png",
# "size" => 49349138,
# "mime_type" => "image/png"
# }
# }
photo.image.original_filename #=> "nature.png"
photo.image.size #=> 49349138
photo.image.mime_type #=> "image/png"
By default "filename", "size" and "mime_type" is stored, but you can also store image dimensions, or define any other custom metadata. This also allow storages to add their own metadata.
In Refile you define validations by passing options to .attachment
, while
in Shrine you define validations on the instance-level, which allows them to
be dynamic:
class Photo < Sequel::Model
attachment :image,
extension: %w[jpg jpeg png gif],
content_type: %w[image/jpeg image/png image/gif]
end
class ImageUploader < Shrine
plugin :validation_helpers
Attacher.validate do
validate_extension_inclusion %w[jpg jpeg png gif]
validate_mime_type_inclusion %w[image/jpeg image/png image/gif]
validate_max_size 10*1024*1024 unless record.admin?
end
end
Refile extracts the MIME type from the file extension, which means it can
easily be spoofed (just give a PHP file a .jpg
extension). Shrine has the
determine_mime_type plugin for determining MIME type from file content.
Shrine doesn't have a built-in solution for accepting multiple uploads, but it's actually very easy to do manually, see the demo app on how you can do multiple uploads directly to S3.
Shrine borrows Refile's idea of direct uploads, and ships with
upload_endpoint
and presign_endpoint
plugins which provide endpoints for
uploading files and generating presigns.
Shrine.plugin :upload_endpoint
Shrine.upload_endpoint(:cache) # Rack app that uploads files to specified storage
Shrine.plugin :upload_endpoint
Shrine.presign_endpoint(:cache) # Rack app that generates presigns for specified storage
Unlike Refile, Shrine doesn't ship with complete JavaScript which you can just include to make it work. However, Uppy is an excellent JavaScript file upload library that integrates wonderfully with Shrine, see the demo app for a complete example.
You have an existing app using Refile and you want to transfer it to
Shrine. Let's assume we have a Photo
model with the "image" attachment. First
we need to create the image_data
column for Shrine:
add_column :photos, :image_data, :text
Afterwards we need to make new uploads write to the image_data
column. This
can be done by including the below module to all models that have Refile
attachments:
module RefileShrineSynchronization
def write_shrine_data(name)
if read_attribute("#{name}_id").present?
data = {
storage: :store,
id: send("#{name}_id"),
metadata: {
size: (send("#{name}_size") if respond_to?("#{name}_size")),
filename: (send("#{name}_filename") if respond_to?("#{name}_filename")),
mime_type: (send("#{name}_content_type") if respond_to?("#{name}_content_type")),
}
}
write_attribute(:"#{name}_data", data.to_json)
else
write_attribute(:"#{name}_data", nil)
end
end
end
class Photo < ActiveRecord::Base
attachment :image
include RefileShrineSynchronization
before_save do
write_shrine_data(:image) if changes.key?(:image_id)
end
end
After you deploy this code, the image_data
column should now be successfully
synchronized with new attachments. Next step is to run a script which writes
all existing Refile attachments to image_data
:
Photo.find_each do |photo|
photo.write_shrine_data(:image)
photo.save!
end
Now you should be able to rewrite your application so that it uses Shrine instead of Refile, using equivalent Shrine storages. For help with translating the code from Refile to Shrine, you can consult the reference below.
Shrine calles these "storages", and it doesn't have special accessors for
:cache
and :store
:
Shrine.storages = {
cache: Shrine::Storage::Foo.new(*args),
store: Shrine::Storage::Bar.new(*args),
}
The upload_endpoint
and presign_endpoint
plugins provide methods for
generating Rack apps, but you need to mount them explicitly:
# config/routes.rb
Rails.application.routes.draw do
# adds `POST /images/upload` endpoint
mount ImageUploader.upload_endpoint(:cache) => "/images/upload"
end
The Shrine.upload_endpoint
and Shrine.presign_endpoint
require you to
specify the storage that will be used.
Shrine.plugin :logging
class MyUploader < Shrine
plugin :processing
process(:store) do |io, context|
# ...
end
end
In Shrine validations are done by calling .validate
on the attacher class:
class MyUploader < Shrine
plugin :validation_helpers
Attacher.validate do
validate_max_size 5*1024*1024
end
end
In Shrine equivalents are (private) methods Shrine#extract_filename
and
Shrine#extract_mime_type
.
You should use your framework to generate the URL to your mounted direct enpdoint.
You can call #url
on the uploaded file, or #<name>_url
on the model.
Additionally you can use the download_endpoint
plugin.
These should be generated directly by you, it depends on where you've mounted the direct endpoint.
Not needed since Shrine doesn't offer on-the-fly processing.
Not needed since Shrine doesn't offer on-the-fly processing.
Shrine's equivalent to calling the attachment is including an attachment module of an uploader:
class User
include ImageUploader::Attachment.new(:avatar)
end
In Shrine validations are done instance-level inside the uploader, most
commonly with the validation_helpers
plugin:
class ImageUploader < Shrine
plugin :validation_helpers
Attacher.validate do
validate_extension_inclusion %w[jpg jpeg png]
validate_mime_type_inclusion %w[image/jpeg image/png]
end
end
Shrine provides a default_storage
plugin for setting custom storages on the
uploader:
Shrine.storages[:custom_cache] = Shrine::Storage::Foo.new(*args)
Shrine.storages[:custom_store] = Shrine::Storage::Bar.new(*args)
class ImageUploader < Shrine
plugin :default_storage, cache: :custom_cache, store: :custom_store
end
No equivalent currently exists in Shrine.
No equivalent in Shrine, but take a look at the "Multiple Files" guide.
The following Refile code
<%= form_for @user do |form| %>
<%= form.attachment_field :profile_image %>
<% end %>
is equivalent to the following Shrine code
Shrine.plugin :cached_attachment_data
<%= form_for @user do |form| %>
<%= form.hidden_field :profile_image, value: @user.cached_profile_image_data %>
<%= form.file_field :profile_image %>
<% end %>
Shrine comes with a remove_attachment
plugin which adds the same
#remove_<attachment>
method to the model.
Shrine.plugin :remove_attachment
<%= form_for @user do |form| %>
<%= form.hidden_field :profile_image, value: @user.cached_profile_image_data %>
<%= form.file_field :profile_image %>
<%= form.check_box :remove_profile_image %>
<% end %>
Shrine comes with a remote_url
plugin which adds the same
#<attachment>_remote_url
method to the model.
Shrine.plugin :remote_url
<%= form_for @user do |form| %>
<%= form.hidden_field :profile_image, value: @user.cached_profile_image_data %>
<%= form.file_field :profile_image %>
<%= form.text_field :profile_image_remote_url %>
<% end %>