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.
If you would like assistance with the upgrade, I'm available for consultation, you can email me at janko.marohnic@gmail.com.
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 arguments
To 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 :model
(:image_data) include Shrine::Attachment(:image)end
Data 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.reloadattacher.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 :validation
Attacher.validate do # ... endend
Setting
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 file
If 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 storage
The 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 changes
If you want the concurrenct-safe promotion with persistence, use the new
Attacher#atomic_promote
method.
attacher.atomic_promote
The 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.fileattacher.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 :backgroundingShrine::Attacher.promote_block do PromoteJob.perform_async(self.class.name, record.class.name, record.id, name, file_data)endShrine::Attacher.destroy_block do DestroyJob.perform_async(self.class.name, data)end
include Sidekiq::Worker 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 endend
include Sidekiq::Worker attacher_class = Object.const_get(attacher_class) attacher = attacher_class.from_data(data) attacher.destroy endend
Dual 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.
include Sidekiq::Worker 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 endand
include Sidekiq::Worker 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 endand
Attacher 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 registered
If 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 block
Versions
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
code:
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 endend
photo = Photo.new(photo_params) if photo.valid? photo.save # automatically calls processing block # ... else # ... end
With derivatives
, the original file is automatically downloaded and retained,
so the code is now much simpler:
Shrine.plugin :derivatives, versions_compatibility: true # handle versions column format
Attacher.derivatives_processor 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), } endend
photo = Photo.new(photo_params) if photo.valid? photo.image_derivatives! if photo.image_changed? # create derivatives photo.save # automatically calls processing block # ... else # ... end
If you have multiple places where you need to generate derivatives, and want it
to happen automatically like it did with the versions
plugin, you can
override Attacher#promote
to call Attacher#create_derivatives
before
promotion:
create_derivatives super endend
Accessing derivatives
The derivative URLs are accessed in the same way as versions:
photo.image_url(:small)
But the derivatives 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 switch existing records to the
new column format:
Photo.find_each do |photo| photo.image_attacher.write photo.image_attacher.atomic_persistend
Afterwards 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:
include Sidekiq::Worker 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 beeen deleted, nothing to do endend
Recache
If you were using the recache
plugin, you can replicate the behaviour by
creating another derivatives processor that you will trigger in the controller:
Attacher.derivatives_processor do |original| # this will be triggered in the background job end Attacher.derivatives_processor :foreground do |original| # this will be triggered in the controller endend
photo = Photo.new(photo_params) if photo.valid? photo.image_derivatives!(:foreground) if photo.image_changed? photo.save # ... else # ... end
Parallelize
The parallelize
plugin has been removed. The derivatives
plugin is
thread-safe, so you can parallelize uploading processed files manually:
# Gemfile
derivatives = attacher.process_derivatives tasks = derivatives.map do |name, file| Concurrent::Promises.future(name, file) do |name, file| attacher.add_derivative(name, file) endend Concurrent::Promises.zip(*tasks).wait!
Default URL
The derivatives
plugin integrates with the default_url
plugin:
Attacher.default_url do |derivative: nil, **| "https://my-app.com/fallbacks/ .jpg" if derivativeend
However, it doesn't implement any other URL fallbacks that the versions
plugin has for missing derivatives.
Other
Processing
The processing
plugin has been deprecated over the new
derivatives
plugin. If you were modifying the original file:
plugin :processing process(:store) do |io, context| ImageProcessing::MiniMagick .source(io.download) .resize_to_limit!(1600, 1600) endend
you should now add the processed file as a derivative:
plugin :derivatives Attacher.derivatives_processor do |original| magick = ImageProcessing::MiniMagick.source(original) {normalized: magick.resize_to_limit!(1600, 1600) } endend
Logging
The logging
plugin has been removed in favour of the
instrumentation
plugin. You can replace code like
Shrine.plugin :logging, logger: Rails.logger
with
Shrine.logger = Rails.logger Shrine.plugin :instrumentation
Backup
The backup
plugin has been removed in favour of the new
mirroring
plugin. You can replace code like
Shrine.plugin :backup, storage: :backup_store
with
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 :copy
attacher.copy(other_attacher)
with
attacher.attach other_attacher.fileattacher.add_derivatives other_attacher.derivatives # if using derivatives
Moving
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
plugin :module_include file_methods do mime_type.start_with?("image") end endend
with
mime_type.start_with?("image") end endend