— id: upgrading-to-3
title: 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}[https://shrinerb.com/docs/release_notes/3.0.0].
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
class Photo < Struct.new(: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.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 :validation
class MyUploader < Shrine Attacher.validate do # ... end end
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.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) end
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.atomic_promote rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound # attachment has changed or record has been deleted, nothing to do end end
class 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 end
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.
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 end
class 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 end
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
}[https://shrinerb.com/docs/plugins/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 end
When 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 # ... end
With 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 format
class 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 end
photo = Photo.new(photo_params) if photo.valid? photo.save # creates derivatives on promotion # ... else # ... end
Accessing 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 end
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:
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 end
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:
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 end
photo = Photo.new(photo_params) if photo.valid? photo.image_derivatives!(:foreground) if photo.image_changed? photo.save # ... else # ... end
Default 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 end
Fallback 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 end
Fallback 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 end
Location¶ ↑
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 end
Overwriting 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 end
you 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 end
Parallelize¶ ↑
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.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.set nil # clear original attachment attacher.attach other_attacher.file, storage: other_attacher.file.storage_key attacher.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
class MyUploader < Shrine plugin :module_include file_methods do def image? mime_type.start_with?("image") end end end
with
class MyUploader < Shrine class UploadedFile def image? mime_type.start_with?("image") end end end