m1es

Migrating from Shrine to ActiveStorage

I recently worked on a migration from Shrine to ActiveStorage. The combination of Shrine and Uppy worked for a while, but using just ActiveStorage and a default file field turns out cleaner and more maintainable.

This post lays out the few steps involved.

Installing ActiveStorage

The first step was to install ActiveStorage.

bin/rails active_storage:install
bin/rails db:migrate

Model changes

The Photo model was stripped of the Shrine functionality by removing:

include ImageUploader::Attachment(:image)

In return it got:

has_one_attached :image

Changes in the views

All occurrences of image_url() where replaced with image.variant().

# before
image_tag @photo.image_url(:small)

# after
image_tag @photo.image.variant(:small)

The variants are defined in the model like this:

has_one_attached :image do |attachable|
  attachable.variant :small, resize_to_limit: [300, 300]
  attachable.variant :medium, resize_to_limit: [600, 600]
  # ... etcetera
end

Migrating the image files

Shrine saves meta information of an uploaded file in the image_data field. By using this information the file can be opened. It’s then passed to the ActiveStorage attach method that takes care of the actual upload (and post-processing).

The job was implemented as follows. By checking for already attached images the job is idempotent and can be run multiple times in case of any unforeseen exceptions.

class ShrineMigrationJob < ActiveJob::Base
  def perform
    Photo.where.not(image_data: nil).find_each do |photo|
      unless photo.image.attached?
        data = JSON.parse(photo.image_data)
        path = 'storage/'+data['id']
        filename = data['metadata']['filename']
        file = File.open(path)
        file.rewind
        photo.image.attach(io: file, filename:)
        file.close
      end
    end
  end
end

All with all the migration from Shrine to ActiveStorage turned out to be a walk in the park.