upgrading_to_3.md

doc/upgrading_to_3.md
Last Update: 2023-07-30 18:26:06 +0200

— 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