Upgrading to Shrine 3.x
This guide provides instructions for upgrading Shrine in your apps to version 3.x. If you're looking for a full list of changes, see the 3.0 release notes.
Attacher
The Shrine::Attacher class has been rewritten in Shrine 3.0, though much of
the main API remained the same.
Model
The main change is that Attacher.new is now used for initializing the
attacher without a model:
attacher = Shrine::Attacher.new
#=> #<Shrine::Attacher>
attacher = Shrine::Attacher.new(photo, :image)
# ~> ArgumentError: invalid number of argumentsTo initialize an attacher with a model, use Attacher.from_model provided by
the new model plugin (which is automatically loaded by
activerecord and sequel plugins):
attacher = Shrine::Attacher.from_model(photo, :image)
# ...If you're using the Shrine::Attachment module with POROs, make sure to load
the model plugin.
Shrine.plugin :modelclass Photo < Struct.new(:image_data)
include Shrine::Attachment(:image)
endData attribute
The Attacher#read method has been removed. If you want to generate serialized
attachment data, use Attacher#column_data. Otherwise if you want to generate
hash attachment data, use Attacher#data.
attacher.column_data #=> '{"id":"...","storage":"...","metadata":{...}}'
attacher.data #=> { "id" => "...", "storage" => "...", "metadata" => { ... } }The Attacher#data_attribute has been renamed to Attacher#attribute.
State
The attacher now maintains its own state, so if you've previously modified the
#<name>_data record attribute and expected the changes to be picked up by the
attacher, you'll now need to call Attacher#reload for that:
attacher.file #=> nil
record.image_data = '{"id":"...","storage":"...","metadata":{...}}'
attacher.file #=> nil
attacher.reload
attacher.file #=> #<Shrine::UploadedFile ...>Assigning
The Attacher#assign method now raises an exception when non-cached uploaded
file data is assigned:
# Shrine 2.x
attacher.assign('{"id": "...", "storage": "store", "metadata": {...}}') # ignored
# Shrine 3.0
attacher.assign('{"id": "...", "storage": "store", "metadata": {...}}')
#~> Shrine::Error: expected cached file, got #<Shrine::UploadedFile storage=:store ...>Validation
The validation functionality has been extracted into the validation plugin.
If you're using the validation_helpers plugin, it will automatically load
validation for you. Otherwise you'll have to load it explicitly:
Shrine.plugin :validationclass MyUploader < Shrine
Attacher.validate do
# ...
end
endSetting
The Attacher#set method has been renamed to Attacher#change, and the
private Attacher#_set method has been renamed to Attacher#set and made
public:
attacher.change(uploaded_file) # sets file, remembers previous file, runs validations
attacher.set(uploaded_file) # sets fileIf you've previously used Attacher#replace directly to delete previous file,
it has now been renamed to Attacher#destroy_previous.
Also note that Attacher#attached? now returns whether a file is attached,
while Attacher#changed? continues to return whether the attachment has
changed.
Uploading and deleting
The Attacher#store! and Attacher#cache! methods have been removed, you
should now use Attacher#upload instead:
attacher.upload(io) # uploads to permanent storage
attacher.upload(io, :cache) # uploads to temporary storage
attacher.upload(io, :other_store) # uploads to another storageThe Attacher#delete! method has been removed as well, you should instead just
delete the file directly via UploadedFile#delete.
Promoting
If you were promoting manually, the Attacher#promote method will now only
save promoted file in memory, it won't persist the changes.
attacher.promote
# ...
record.save # you need to persist the changesIf you want the concurrenct-safe promotion with persistence, use the new
Attacher#atomic_promote method.
attacher.atomic_promoteThe Attacher#swap method has been removed. If you were using it directly, you
can use Attacher#set and Attacher#atomic_persist instead:
current_file = attacher.file
attacher.set(new_file)
attacher.atomic_persist(current_file)Backgrounding
The backgrounding plugin has been rewritten in Shrine 3.0 and has a new API.
Shrine.plugin :backgrounding
Shrine::Attacher.promote_block do
PromoteJob.perform_async(self.class.name, record.class.name, record.id, name, file_data)
end
Shrine::Attacher.destroy_block do
DestroyJob.perform_async(self.class.name, data)
endclass PromoteJob
include Sidekiq::Worker
def perform(attacher_class, record_class, record_id, name, file_data)
attacher_class = Object.const_get(attacher_class)
record = Object.const_get(record_class).find(record_id) # if using Active Record
attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
attacher.atomic_promote
rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
# attachment has changed or record has been deleted, nothing to do
end
endclass DestroyJob
include Sidekiq::Worker
def perform(attacher_class, data)
attacher_class = Object.const_get(attacher_class)
attacher = attacher_class.from_data(data)
attacher.destroy
end
endDual support
When you're making the switch in production, there might still be jobs in the queue that have the old argument format. So, we'll initially want to handle both argument formats, and then switch to the new one once the jobs with old format have been drained.
class PromoteJob
include Sidekiq::Worker
def perform(*args)
if args.one?
file_data, (record_class, record_id), name, shrine_class =
args.first.values_at("attachment", "record", "name", "shrine_class")
record = Object.const_get(record_class).find(record_id) # if using Active Record
attacher_class = Object.const_get(shrine_class)::Attacher
else
attacher_class, record_class, record_id, name, file_data = args
attacher_class = Object.const_get(attacher_class)
record = Object.const_get(record_class).find(record_id) # if using Active Record
end
attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
attacher.atomic_promote
rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
# attachment has changed or record has been deleted, nothing to do
end
endclass DestroyJob
include Sidekiq::Worker
def perform(*args)
if args.one?
data, shrine_class = args.first.values_at("attachment", "shrine_class")
data = JSON.parse(data)
attacher_class = Object.const_get(shrine_class)::Attacher
else
attacher_class, data = args
attacher_class = Object.const_get(attacher_class)
end
attacher = attacher_class.from_data(data)
attacher.destroy
end
endAttacher backgrounding
In Shrine 2.x, Attacher#_promote and Attacher#_delete methods could be used
to spawn promote and delete jobs. This is now done by Attacher#promote_cached
and Attacher#destroy_attached:
attacher.promote_cached # will spawn background job if registered
attacher.destroy_attached # will spawn background job if registeredIf you want to explicitly call backgrounding blocks, you can use
Attacher#promote_background and Attacher#destroy_background:
attacher.promote_background # calls promote block
attacher.destroy_background # calls destroy blockVersions
The versions, processing, recache, and delete_raw plugins have been
deprecated in favour of the new derivatives plugin.
Let's assume you have the following versions configuration:
class ImageUploader < Shrine
plugin :processing
plugin :versions
plugin :delete_raw
process(:store) do |file, context|
versions = { original: file }
file.download do |original|
magick = ImageProcessing::MiniMagick.source(original)
versions[:large] = magick.resize_to_limit!(800, 800)
versions[:medium] = magick.resize_to_limit!(500, 500)
versions[:small] = magick.resize_to_limit!(300, 300)
end
versions
end
endWhen an attached file is promoted to permanent storage, the versions would automatically get generated:
photo = Photo.new(photo_params)
if photo.valid?
photo.save # generates versions on promotion
# ...
else
# ...
endWith derivatives, the original file is automatically downloaded and retained
during processing, so the setup is simpler:
Shrine.plugin :derivatives,
create_on_promote: true, # automatically create derivatives on promotion
versions_compatibility: true # handle versions column formatclass ImageUploader < Shrine
Attacher.derivatives do |original|
magick = ImageProcessing::MiniMagick.source(original)
# the :original file should NOT be included anymore
{
large: magick.resize_to_limit!(800, 800),
medium: magick.resize_to_limit!(500, 500),
small: magick.resize_to_limit!(300, 300),
}
end
endphoto = Photo.new(photo_params)
if photo.valid?
photo.save # creates derivatives on promotion
# ...
else
# ...
endAccessing derivatives
The derivative URLs are accessed in the same way as versions:
photo.image_url(:small)But the files themselves are accessed differently:
# versions
photo.image #=>
# {
# original: #<Shrine::UploadedFile ...>,
# large: #<Shrine::UploadedFile ...>,
# medium: #<Shrine::UploadedFile ...>,
# small: #<Shrine::UploadedFile ...>,
# }
photo.image[:medium] #=> #<Shrine::UploadedFile ...># derivatives
photo.image_derivatives #=>
# {
# large: #<Shrine::UploadedFile ...>,
# medium: #<Shrine::UploadedFile ...>,
# small: #<Shrine::UploadedFile ...>,
# }
photo.image(:medium) #=> #<Shrine::UploadedFile ...>Migrating versions
The versions and derivatives plugins save processed file data to the
database column in different formats:
# versions
{
"original": { "id": "...", "storage": "...", "metadata": { ... } },
"large": { "id": "...", "storage": "...", "metadata": { ... } },
"medium": { "id": "...", "storage": "...", "metadata": { ... } },
"small": { "id": "...", "storage": "...", "metadata": { ... } }
}# derivatives
{
"id": "...",
"storage": "...",
"metadata": { ... },
"derivatives": {
"large": { "id": "...", "storage": "...", "metadata": { ... } },
"medium": { "id": "...", "storage": "...", "metadata": { ... } },
"small": { "id": "...", "storage": "...", "metadata": { ... } }
}
}The :versions_compatibility flag to the derivatives plugin enables it to
read the versions format, which aids in transition. Once the derivatives
plugin has been deployed to production, you can update existing records with
the new column format:
Photo.find_each do |photo|
photo.image_attacher.write
photo.image_attacher.atomic_persist
endAfterwards you should be able to remove the :versions_compatibility flag.
Backgrounding derivatives
If you're using the backgrounding plugin, you can trigger derivatives
creation in the PromoteJob instead of the controller:
class PromoteJob
include Sidekiq::Worker
def perform(attacher_class, record_class, record_id, name, file_data)
attacher_class = Object.const_get(attacher_class)
record = Object.const_get(record_class).find(record_id) # if using Active Record
attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
attacher.create_derivatives # call derivatives processor
attacher.atomic_promote
rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
# attachment has changed or record has been deleted, nothing to do
end
endRecache
If you were using the recache plugin, you can replicate the behaviour by
creating another derivatives processor that you will trigger in the controller:
class ImageUploader < Shrine
Attacher.derivatives do |original|
# this will be triggered in the background job
end
Attacher.derivatives :foreground do |original|
# this will be triggered in the controller
end
endphoto = Photo.new(photo_params)
if photo.valid?
photo.image_derivatives!(:foreground) if photo.image_changed?
photo.save
# ...
else
# ...
endDefault URL
If you were using the default_url plugin, the Attacher.default_url now
receives a :derivative option:
Attacher.default_url do |derivative: nil, **|
"https://my-app.com/fallbacks/#{derivative}.jpg" if derivative
endFallback to original
With the versions plugin, a missing version URL would automatically fall back
to the original file. The derivatives plugin has no such fallback, but you
can configure it manually:
Attacher.default_url do |derivative: nil, **|
file&.url if derivative
endFallback to version
The versions plugin had the ability to fall back missing version URL to
another version that already exists. The derivatives plugin doesn't have this
built in, but you can implement it as follows:
DERIVATIVE_FALLBACKS = { foo: :bar, ... }
Attacher.default_url do |derivative: nil, **|
derivatives[DERIVATIVE_FALLBACKS[derivative]]&.url if derivative
endLocation
The Shrine#generate_location method will now receive a :derivative
parameter instead of :version:
class MyUploader < Shrine
def generate_location(io, derivative: nil, **)
derivative #=> :large, :medium, :small, ...
# ...
end
endOverwriting original
With the derivatives plugin, saving processed files separately from the
original file, so the original file is automatically kept. This means it's not
possible anymore to overwrite the original file as part of processing.
However, it's highly recommended to always keep the original file, even if you don't plan to use it. That way, if there is ever a need to reprocess derivatives, you have the original file to use as a base.
That being said, if you still want to overwrite the original file, this thread has some tips.
Other
Processing
The processing plugin has been deprecated over the new
derivatives plugin. If you were previously replacing the
original file:
class MyUploader < Shrine
plugin :processing
process(:store) do |io, context|
ImageProcessing::MiniMagick
.source(io.download)
.resize_to_limit!(1600, 1600)
end
endyou should now add the processed file as a derivative:
class MyUploader < Shrine
plugin :derivatives
Attacher.derivatives do |original|
magick = ImageProcessing::MiniMagick.source(original)
{ normalized: magick.resize_to_limit!(1600, 1600) }
end
endParallelize
The parallelize plugin has been removed. With derivatives plugin you can
parallelize uploading processed files manually:
# Gemfile
gem "concurrent-ruby"require "concurrent"
attacher = photo.image_attacher
derivatives = attacher.process_derivatives
tasks = derivatives.map do |name, file|
Concurrent::Promises.future(name, file) do |name, file|
attacher.add_derivative(name, file)
end
end
Concurrent::Promises.zip(*tasks).wait!Logging
The logging plugin has been removed in favour of the
instrumentation plugin. You can replace code like
Shrine.plugin :logging, logger: Rails.loggerwith
Shrine.logger = Rails.logger
Shrine.plugin :instrumentationBackup
The backup plugin has been removed in favour of the new
mirroring plugin. You can replace code like
Shrine.plugin :backup, storage: :backup_storewith
Shrine.plugin :mirroring, mirror: { store: :backup_store }Copy
The copy plugin has been removed as its behaviour can now be achieved easily.
You can replace code like
Shrine.plugin :copyattacher.copy(other_attacher)with
attacher.set nil # clear original attachment
attacher.attach other_attacher.file, storage: other_attacher.file.storage_key
attacher.add_derivatives other_attacher.derivatives # if using derivativesMoving
The moving plugin has been removed in favour of the :move option for
FileSystem#upload. You can set this option as default using the
upload_options plugin (the example assumes both :cache and :store are
FileSystem storages):
Shrine.plugin :upload_options, cache: { move: true }, store: { move: true }Parsed JSON
The parsed_json plugin has been removed as it's now the default behaviour.
# this now works by default
photo.image = { "id" => "d7e54d6ef2.jpg", "storage" => "cache", "metadata" => { ... } }Module Include
The module_include plugin has been deprecated over overriding core classes
directly. You can replace code like
class MyUploader < Shrine
plugin :module_include
file_methods do
def image?
mime_type.start_with?("image")
end
end
endwith
class MyUploader < Shrine
class UploadedFile
def image?
mime_type.start_with?("image")
end
end
end