—
title: Upgrading from Paperclip¶ ↑
This guide is aimed at helping Paperclip users transition to Shrine
, and it consists of three parts:
-
Explanation of the key differences in design between Paperclip and
Shrine
-
Instructions how to migrate an existing app that uses Paperclip to
Shrine
-
Extensive reference of Paperclip’s interface with
Shrine
equivalents
Overview¶ ↑
Uploader¶ ↑
In Paperclip, the attachment logic is configured directly inside Active Record models:
class Photo < ActiveRecord::Base has_attached_file :image, preserve_files: true, default_url: "/images/:style/missing.png" validated_attachment_content_type :image, content_type: "image/jpeg" end
Shrine
takes a more object-oriented approach, by encapsulating attachment logic in “uploader” classes:
class ImageUploader < Shrine plugin :keep_files plugin :default_url plugin :validation_helpers Attacher.default_url do |derivative: nil, **| "/images/#{derivative}/missing.png" if derivative end Attacher.validate do validate_mime_type %w[image/jpeg] end end
class Photo < ActiveRecord::Base include ImageUploader::Attachment(:image) end
Storage¶ ↑
Paperclip storage is configured together with other attachment options. Also, the storage implementations themselves are mixed into the attachment class, which couples them to the attachment flow.
class Photo < ActiveRecord::Base has_attached_file :image, storage: :s3, s3_credentials: { bucket: "my-bucket", access_key_id: "abc", secret_access_key: "xyz", }, s3_region: "eu-west-1", end
Shrine
storage objects are configured separately and are decoupled from attachment:
Shrine.storages[:store] = Shrine::Storage::S3.new( bucket: "my-bucket", access_key_id: "abc", secret_access_key: "xyz", region: "eu-west-1", )
Shrine
also has a concept of “temporary” storage, which enables retaining uploaded files in case of validation errors and direct uploads.
Shrine.storages = { cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"), store: Shrine::Storage::S3.new(bucket: "my-bucket", **s3_options), }
Persistence¶ ↑
When using Paperclip, the attached file data will be persisted into several columns:
-
<name>_file_name
-
<name>_content_type
-
<name>_file_size
-
<name>_updated_at
-
<name>_created_at
(optional) -
<name>_fingerprint
(optional)
In contrast, Shrine
uses a single <name>_data
column to store data in JSON format:
{ "id": "path/to/image.jpg", "storage": "store", "metadata": { "filename": "nature.jpg", "size": 4739472, "mime_type": "image/jpeg" } }
photo.image.id #=> "path/to/image.jpg" photo.image.storage_key #=> :store photo.image.metadata #=> { "filename" => "...", "size" => ..., "mime_type" => "..." } photo.image.original_filename #=> "nature.jpg" photo.image.size #=> 4739472 photo.image.mime_type #=> "image/jpeg"
This column can be queried if it’s made a JSON column. Alternatively, you can use the {metadata_attributes
} plugin to save metadata into separate columns.
ORM¶ ↑
While Paperclip works only with Active Record, Shrine
is designed to integrate with any persistence library (there are integrations for Active Record, Sequel, ROM, Hanami and Mongoid), and can also be used standalone:
attacher = ImageUploader::Attacher.new attacher.attach File.open("nature.jpg") attacher.file #=> #<Shrine::UploadedFile id="f4ba5bdbf366ef0b.jpg" ...> attacher.url #=> "https://my-bucket.s3.amazonaws.com/f4ba5bdbf366ef0b.jpg" attacher.data #=> { "id" => "f4ba5bdbf366ef0b.jpg", "storage" => "store", "metadata" => { ... } }
Location¶ ↑
Paperclip persists only the filename of the uploaded file, and recalculates the full location dynamically based on location configuration. This can be dangerous, because if some component of the location happens to change, all existing links might become invalid.
To avoid this, Shrine
persists the full location on attachment, and uses it when generating file URL. So, even if you change how file locations are generated, existing files that are on old locations will still remain accessible.
Processing¶ ↑
In Shrine
, processing is defined and performed on the instance level, which gives you more control. You’re also not coupled to ImageMagick, e.g. you can use libvips instead (both integrations are provided by the image_processing gem).
class Photo < ActiveRecord::Base has_attached_file :image, styles: { large: "800x800>", medium: "500x500>", small: "300x300>", } end
require "image_processing/mini_magick" class ImageUploader < Shrine plugin :derivatives Attacher.derivatives 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
Shrine
is agnostic as to how you’re performing your processing, so you can easily use any other processing tools. You can also combine different processors for different versions.
Retrieving versions¶ ↑
When retrieving versions, Paperclip returns a list of declared styles which may or may not have been generated. In contrast, Shrine
persists data of uploaded processed files into the database (including any extracted metadata), which then becomes the source of truth on which versions have been generated.
photo.image #=> #<Shrine::UploadedFile id="original.jpg" ...> photo.image_derivatives #=> {} photo.image_derivatives! # triggers processing photo.image_derivatives #=> # { # large: #<Shrine::UploadedFile id="large.jpg" metadata={"size"=>873232, ...} ...>, # medium: #<Shrine::UploadedFile id="medium.jpg" metadata={"size"=>94823, ...} ...>, # small: #<Shrine::UploadedFile id="small.jpg" metadata={"size"=>37322, ...} ...>, # }
Reprocessing versions¶ ↑
Shrine
doesn’t have a built-in way of regenerating versions, because that has to be written and optimized differently depending on what versions have changed which persistence library you’re using, how many records there are in the table etc.
However, there is an extensive guide for Managing Derivatives, which provides instructions on how to make these changes safely and with zero downtime.
Validation¶ ↑
File validation in Shrine
is also instance-level, which allows using conditionals:
class Photo < ActiveRecord::Base has_attached_file :image validates_attachment :image, size: { in: 0..10.megabytes }, content_type: { content_type: %w[image/jpeg image/png image/webp] } end
class ImageUploader < Shrine plugin :validation_helpers Attacher.validate do validate_max_size 10*1024*1024 if validate_mime_type %w[image/jpeg image/png image/webp] validate_max_dimensions [5000, 5000] end end end
Custom metadata¶ ↑
With Shrine
you can also extract and validate any custom metadata:
class VideoUploader < Shrine plugin :add_metadata plugin :validation add_metadata :duration do |io| FFMPEG::Movie.new(io.path).duration end Attacher.validate do if file.duration > 5*60*60 errors << "must not be longer than 5 hours" end end end
MIME type spoofing¶ ↑
Paperclip attempts to detect MIME type spoofing, which turned out to be unreliable due to differences in MIME type databases between different ruby libraries.
Shrine
on the other hand simply allows you to determine MIME type from file content, which you can then validate.
Shrine.plugin :determine_mime_type, analyzer: :marcel
file = uploader.upload StringIO.new("<?php ... ?>") file.mime_type #=> "application/x-php"
Migrating from Paperclip¶ ↑
You have an existing app using Paperclip and you want to transfer it to Shrine
. Let’s assume we have a Photo
model with the “image” attachment.
1. Add Shrine
column¶ ↑
First we need to create the image_data
column for Shrine:
add_column :photos, :image_data, :text
2. Dual write¶ ↑
Next, we need to make new Paperclip attachments write to the image_data
column. This can be done by including the below module to all models that have Paperclip attachments:
require "shrine" Shrine.storages = { cache: ..., store: ..., } Shrine.plugin :model Shrine.plugin :derivatives module PaperclipShrineSynchronization def self.included(model) model.before_save do Paperclip::AttachmentRegistry.each_definition do |klass, name, options| write_shrine_data(name) if changes.key?(:"#{name}_file_name") && klass == self.class end end end def write_shrine_data(name) attachment = send(name) attacher = Shrine::Attacher.from_model(self, name) if attachment.size.present? attacher.set shrine_file(attachment) attachment.styles.each do |style_name, style| attacher.merge_derivatives(style_name => shrine_file(style)) end else attacher.set nil end end private def shrine_file(object) if object.is_a?(Paperclip::Attachment) shrine_attachment_file(object) else shrine_style_file(object) end end def shrine_attachment_file(attachment) location = attachment.path # if you're storing files on disk, make sure to subtract the absolute path location = location.sub(%r{^#{storage.prefix}/}, "") if storage.prefix Shrine.uploaded_file( storage: :store, id: location, metadata: { "size" => attachment.size, "filename" => attachment.original_filename, "mime_type" => attachment.content_type, } ) end # If you'll be using a `:prefix` on your Shrine storage, or you're storing # files on the filesystem, make sure to subtract the appropriate part # from the path assigned to `:id`. def shrine_style_file(style) location = style.attachment.path(style.name) # if you're storing files on disk, make sure to subtract the absolute path location = location.sub(%r{^#{storage.prefix}/}, "") if storage.prefix Shrine.uploaded_file( storage: :store, id: location, metadata: {}, ) end def storage Shrine.storages[:store] end end
class Photo < ActiveRecord::Base has_attached_file :image include PaperclipShrineSynchronization # needs to be after `has_attached_file` end
After you deploy this code, the image_data
column should now be successfully synchronized with new attachments.
3. Data migration¶ ↑
Next step is to run a script which writes all existing Paperclip attachments to image_data
:
Photo.find_each do |photo| photo.write_shrine_data(:image) photo.save! end
4. Rewrite code¶ ↑
Now you should be able to rewrite your application so that it uses Shrine
instead of Paperclip (you can consult the reference in the next section). You can remove the PaperclipShrineSynchronization
module as well.
5. Remove Paperclip columns¶ ↑
If everything is looking good, we can remove Paperclip columns:
remove_column :photos, :image_file_name remove_column :photos, :image_file_size remove_column :photos, :image_content_type remove_column :photos, :image_updated_at
Paperclip to Shrine
direct mapping¶ ↑
has_attached_file
¶ ↑
As mentioned above, Shrine’s equivalent of has_attached_file
is including an attachment module:
class Photo < Sequel::Model include ImageUploader::Attachment(:image) # adds `image`, `image=` and `image_url` methods end
Now we’ll list all options that has_attached_file
accepts, and explain Shrine’s equivalents:
:storage
¶ ↑
In Shrine
attachments will automatically use :cache
and :store
storages which you have to register:
Shrine.storages = { cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"), store: Shrine::Storage::FileSystem.new("public", prefix: "uploads"), }
You can change that for a specific uploader with the default_storage
plugin.
:styles
, :processors
, :convert_options
¶ ↑
Processing is defined by using the derivatives
plugin:
class ImageUploader < Shrine plugin :derivatives Attacher.derivatives 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
:default_url
¶ ↑
For default URLs you can use the default_url
plugin:
class ImageUploader < Shrine plugin :default_url Attacher.default_url do |derivative: nil, **| "/images/placeholders/#{derivative || "original"}.jpg" end end
:preserve_files
¶ ↑
Shrine
provides a keep_files
plugin which allows you to keep files that would otherwise be deleted:
Shrine.plugin :keep_files
:path
, :url
, :interpolator
, :url_generator
¶ ↑
Shrine
by default stores your files in the same directory, but you can also load the pretty_location
plugin for nice folder structure:
Shrine.plugin :pretty_location
Alternatively, if you want to generate locations yourself you can override the #generate_location
method:
class ImageUploader < Shrine def generate_location(io, record: nil, name: nil, **) [ storage_key, record && record.class.name.underscore, record && record.id, super, io.original_filename ].compact.join("/") end end
cache/user/123/2feff8c724e7ce17/nature.jpg store/user/456/7f99669fde1e01fc/kitten.jpg ...
:validate_media_type
¶ ↑
Shrine
has this functionality in the determine_mime_type
plugin.
validates_attachment
¶ ↑
:presence
¶ ↑
For presence validation you can use your ORM’s presence validator:
class Photo < ActiveRecord::Base include ImageUploader::Attachment(:image) validates_presence_of :image end
:content_type
¶ ↑
You can do MIME type validation with Shrine’s validation_helpers
plugin:
class ImageUploader < Shrine plugin :validation_helpers Attacher.validate do validate_mime_type %w[image/jpeg image/png image/webp] end end
Make sure to also load the determine_mime_type
plugin to detect MIME type from file content.
# Gemfile gem "mimemagic"
Shrine.plugin :determine_mime_type, analyzer: -> (io, analyzers) do analyzers[:mimemagic].call(io) || analyzers[:file].call(io) end
:size
¶ ↑
You can do filesize validation with Shrine’s validation_helpers
plugin:
class ImageUploader < Shrine plugin :validation_helpers Attacher.validate do validate_max_size 10*1024*1024 end end
Paperclip::Attachment
¶ ↑
This section explains the equivalent of Paperclip attachment’s methods, in Shrine
this is an instance of Shrine::UploadedFile
.
#url
¶ ↑
In Shrine
you can generate URLs with #<name>_url
:
photo.image_url #=> "https://example.com/path/to/original.jpg" photo.image_url(:large) #=> "https://example.com/path/to/large.jpg"
#styles
¶ ↑
In Shrine
you can use #<name>_derivatives
to retrieve a list of versions:
photo.image_derivatives #=> # { # small: #<Shrine::UploadedFile>, # medium: #<Shrine::UploadedFile>, # large: #<Shrine::UploadedFile>, # } photo.image_derivatives[:small] #=> #<Shrine::UploadedFile> # or photo.image(:small) #=> #<Shrine::UploadedFile>
#path
¶ ↑
Shrine
doesn’t have this because storages are abstract and this would be specific to the filesystem, but the closest is probably #id
:
photo.image.id #=> "photo/342/image/398543qjfdsf.jpg"
#reprocess!
¶ ↑
Shrine
doesn’t have an equivalent to this, but the Managing Derivatives guide provides some useful tips on how to do this.
Paperclip::Storage::S3
¶ ↑
The built-in {Shrine::Storage::S3
} storage is a direct replacement for Paperclip::Storage::S3
.
:s3_credentials
, :s3_region
, :bucket
¶ ↑
The Shrine
storage accepts :access_key_id
, :secret_access_key
, :region
, and :bucket
options in the initializer:
Shrine::Storage::S3.new( access_key_id: "...", secret_access_key: "...", region: "...", bucket: "...", )
:s3_headers
, :s3_permissions
, :s3_metadata
¶ ↑
These can be configured via the :upload_options
option:
Shrine::Storage::S3.new( upload_options: { content_disposition: "attachment", # headers acl: "private", # permissions metadata: { "key" => "value" }, # metadata }, **options )
:s3_protocol
, :s3_host_alias
, :s3_host_name
¶ ↑
The #url
method accepts a :host
option for specifying a CDN host. You can use the url_options
plugin to set it by default:
Shrine.plugin :url_options, store: { host: "http://abc123.cloudfront.net" }
:path
¶ ↑
The #upload
method accepts the destination location as the second argument.
s3 = Shrine::Storage::S3.new(**options) s3.upload(io, "object/destination/path")
:url
¶ ↑
The Shrine
storage has no replacement for the :url
Paperclip option, and it isn’t needed.