DEV Community

Morten Trolle
Morten Trolle

Posted on

Migrating from Refile to ActiveStorage

Historically I've been using the Refile gem for attaching files to my Rails models. But the gem hasn't been maintained for a long time and since Rails 5.2 ActiveStorage has been my preferred way to go.

These days I've been working on upgrading some of our older Rails 5.1 applications to Rails 6.0 and now I started seeing deprecation warnings on Refile, so It's time to say goodbye to Refile and hello to ActiveStorage.

Here I'll show you how I migrated my users avatar images from Refile to ActiveStorage.

The basics

On my User model I've replaced Refile's attachment :avatar with ActiveStorage's has_one_attached :avatar

In my Users#form view I've replaced the Refile field:

<%= f.attachment_field :avatar %>
Enter fullscreen mode Exit fullscreen mode

with a regular file_field for ActiveStorage:

<%= f.file_field :image %>
Enter fullscreen mode Exit fullscreen mode

Finally in my views when I wanna show the avatar image I moved from Refile's:

<%= image_tag attachment_url(user, :avatar, :fill, 75, 75) %>
Enter fullscreen mode Exit fullscreen mode

to ActiveStorage:

<%= image_tag user.avatar.variant(resize_to_limit: [75, 75]) %>
Enter fullscreen mode Exit fullscreen mode

You should check out the Rails guides for more options on ActiveStorage.

The migration

Great, with my frontend ready to handle ActiveStorage let's move on to the actual file migrations.
I've been using AWS S3 for hosting my Refile files.

Refile relies on the AWS SDK v2. Before I ran this migration I've removed the Refile gems including the AWS SDK v2 with the following gems for ActiveStorage in my Gemfile:

gem "aws-sdk-s3", require: false
gem 'image_processing'
Enter fullscreen mode Exit fullscreen mode

Historically my User model just had a single Refile database field: avatar_id (string).
I wish I had used Refile's content_type detection as well, but I didn't, so in my migration I decided to use ImageMagick to detect the file type of my users images.

I created the following migration file in db/migrate/moving_from_refile_to_active_storage.rb:

require 'mini_magick' # included by the image_processing gem
require 'aws-sdk-s3' # included by the aws-sdk-s3 gem

class User < ActiveRecord::Base
  has_one_attached :avatar
end

class MovingFromRefileToActiveStorage < ActiveRecord::Migration[6.0]
  def up
    puts 'Connecting to AWS S3'
    s3_client = Aws::S3::Client.new(
      access_key_id: ENV['AWS_S3_ACCESS_KEY'],
      secret_access_key: ENV['AWS_S3_SECRET'],
      region: ENV['AWS_S3_REGION']
    )

    puts 'Migrating user avatar images from Refile to ActiveStorage'
    User.where.not(avatar_id: nil).find_each do |user|
      tmp_file = Tempfile.new

      # Read S3 object to our tmp_file
      s3_client.get_object(
        response_target: tmp_file.path,
        bucket: ENV['AWS_S3_BUCKET'],
        key: "store/#{user.avatar_id}"
      )

      # Find content_type of S3 file using ImageMagick
      # If you've been smart enough to save :avatar_content_type with Refile, you can use this value instead
      content_type = MiniMagick::Image.new(tmp_file.path).mime_type

      # Attach tmp file to our User as an ActiveStorage attachment
      user.avatar.attach(
        io: tmp_file,
        filename: "avatar.#{content_type.split('/').last}",
        content_type: content_type
      )

      if user.avatar.attached?
        user.save # Save our changes to the user
        puts "- migrated #{user.try(:name)}'s avatar image."
      else
        puts "- \e[31mFailed to migrate the avatar image for user ##{user.id} with Refile id #{user.avatar_id}\e[0m"
      end

      tmp_file.close
    end

    # Now remove the actual Refile column
    remove_column :users, :avatar_id, :string
    # If you've created other Refile fields like *_content_type, you can safely remove those as well
    # remove_column :users, :avatar_content_type, :string
  end

  def down
    raise ActiveRecord::IrreversibleMigration
  end
end
Enter fullscreen mode Exit fullscreen mode

Just to be safe I chose not to delete Refile files from my S3 bucket in this migration file. I could of course have done so, but I choose to delete the storage and cache folders in my bucket manually instead.

That's it. After having run this migration all my users avatar images are now handled through ActiveStorage.

Good bye Refile.

Top comments (2)

Collapse
 
ruanltbg profile image
Ruan Carlos • Edited

Thank you for sharing, my final dynamic file:

# lib/migrate_refile_to_active_storage.rb
require 'mini_magick' # included by the image_processing gem
require 'aws-sdk-s3' # included by the aws-sdk-s3 gem

class MigrateRefileToActiveStorage
  attr_accessor :model, :attribute
  def initialize(model, attribute)
    @model = model
    @attribute = attribute
  end

  def run
    puts "Migrating #{@mode} #{@attribute} file from Refile to ActiveStorage"
    @model.constantize.where.not(:"#{@attribute}_id" => nil).find_each do |instance|
      next if instance.send(@attribute).attached?

      id = file_id(instance.send("#{@attribute}_id"))
      next unless Easymovie::Aws.bucket.object(id).exists?
      tmp_file = Tempfile.new

      # Download the S3 file to our temp file
      Easymovie::Aws.s3.client.get_object(
        response_target: tmp_file.path,
        bucket: Easymovie::Aws.bucket.name,
        key: id
      )

      # Find content_type of S3 file using ImageMagick
      # Verify if the model has the content_type column otherwise infers using MiniMagick
      content_type = instance.try(:"#{@attribute}_content_type") || MiniMagick::Image.new(tmp_file.path).mime_type

      # Attach tmp file to our User as an ActiveStorage attachment
      filename = instance.try(:"#{@attribute}_filename") || "file.#{content_type.split('/').last}"
      instance.send(@attribute).attach(
        io: tmp_file,
        filename: filename,
        content_type: content_type
      )

      if instance.send(@attribute).attached?
        instance.save # Save our changes to the instance
        puts "- migrated #{@mode} #{@attribute}'s file for instance ##{instance.id}."
      else
        puts "- \e[31mFailed to migrate the file for #{@model} instance ##{instance.id} with Refile id #{instance.send("#{@attribute}_id")}\e[0m"
      end

      tmp_file.close
    end
  end

  private

  def file_id(id)
    return id if id.match('store/')
    "store/#{id}"
  end
end

# MigrateRefileToActiveStorage.new('User', 'avatar').run
Enter fullscreen mode Exit fullscreen mode
Collapse
 
alispat profile image
Alisson Patrick

Thanks for sharing this! Super useful technique!