Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
1 contributor

Users who have contributed to this file

665 lines (516 sloc) 16.3 KB
id title
upgrading-to-3
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
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
and
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
and

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:

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
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
class ImageUploader < Shrine
  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),
    }
  end
end
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:

class Shrine::Attacher
  def promote(*)
    create_derivatives
    super
  end
end

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_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 beeen 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_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
  end
end
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
gem "concurrent-ruby"
require "concurrent"

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!

Default URL

The derivatives plugin integrates with the default_url plugin:

Attacher.default_url do |derivative: nil, **|
  "https://my-app.com/fallbacks/#{derivative}.jpg" if derivative
end

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:

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_processor do |original|
    magick = ImageProcessing::MiniMagick.source(original)

    { normalized: magick.resize_to_limit!(1600, 1600) }
  end
end

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.file
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
You can’t perform that action at this time.