How to Migrate from Paperclip to Rails ActiveStorage

May 8, 2018

Track your progress

Sign in to track your progress and access subscription-only lessons.

Log In

Your Teacher

Hi, I'm Chris. I'm the creator of GoRails, Hatchbox.io and Jumpstart. I spend my time creating tutorials and tools to help Ruby on Rails developers build apps better and faster.

About This Episode

Now that the Paperclip gem has been deprecated, it's recommended that you migrate your apps to ActiveStorage

Notes

Resources

db/migrate/convert_to_active_storage.rb

Dir[Rails.root.join("app/models/**/*.rb")].sort.each { |file| require file }

class ConvertToActiveStorage < ActiveRecord::Migration[5.2]
  require 'open-uri'

  def up
    # postgres
    # get_blob_id = 'LASTVAL()'
    # mysql / mariadb
    # get_blob_id = 'LAST_INSERT_ID()'
    # sqlite
    get_blob_id = 'LAST_INSERT_ROWID()'

    active_storage_blob_statement = ActiveRecord::Base.connection.raw_connection.prepare(<<-SQL)
      INSERT INTO active_storage_blobs (
        key, filename, content_type, metadata, byte_size,
        checksum, created_at
      ) VALUES (?, ?, ?, '{}', ?, ?, ?)
    SQL

    active_storage_attachment_statement = ActiveRecord::Base.connection.raw_connection.prepare(<<-SQL)
      INSERT INTO active_storage_attachments (
        name, record_type, record_id, blob_id, created_at
      ) VALUES (?, ?, ?, #{get_blob_id}, ?)
    SQL

    models = ActiveRecord::Base.descendants.reject(&:abstract_class?)

    transaction do
      models.each do |model|
        attachments = model.column_names.map do |c|
          if c =~ /(.+)_file_name$/
            $1
          end
        end.compact

        model.find_each.each do |instance|
          attachments.each do |attachment|
            if instance.send(attachment).exists?
              active_storage_blob_statement.execute(
                key(instance, attachment),
                instance.send("#{attachment}_file_name"),
                instance.send("#{attachment}_content_type"),
                instance.send("#{attachment}_file_size"),
                checksum(instance.send(attachment)),
                instance.updated_at.iso8601
              )

              active_storage_attachment_statement.
                execute(attachment, model.name, instance.id, instance.updated_at.iso8601)
            end
          end
        end
      end
    end

    active_storage_attachment_statement.close
    active_storage_blob_statement.close
  end

  def down
    raise ActiveRecord::IrreversibleMigration
  end

  private

  def key(instance, attachment)
    # SecureRandom.uuid
    # Alternatively:
    filename = instance.send("#{attachment}_file_name")
    klass = instance.class.table_name
    id = instance.id
    id_partition = ("%09d".freeze % id).scan(/\d{3}/).join("/".freeze)

    "#{klass}/#{attachment.pluralize}/#{id_partition}/original/#{filename}"
  end

  def checksum(attachment)
    # local files stored on disk:
    #url = attachment.path
    #Digest::MD5.base64digest(File.read(url))

    # remote files stored on another person's computer:
    url = attachment.url
    Digest::MD5.base64digest(Net::HTTP.get(URI(url)))
  end
end

lib/tasks/paperclip.rake

namespace :paperclip do
  task migrate: :environment do
    klass = User
    attachment = 'avatar'
    name_field = :"#{attachment}_file_name"

    klass.where.not(name_field => nil).find_each do |instance|
      # This step helps us catch any attachments we might have uploaded that
      # don't have an explicit file extension in the filename

      filename = instance.send("#{attachment}_file_name")

      next if filename.blank?

      id = instance.id
      id_partition = ("%09d".freeze % id).scan(/\d{3}/).join("/".freeze)

      url = "https://nyc3.digitaloceanspaces.com/gorails/#{klass.table_name}/#{attachment.pluralize}/#{id_partition}/original/#{filename}"

      instance.send(attachment.to_sym).attach(
        io: open(url),
        filename: instance.send(name_field),
        content_type: instance.send(:"#{attachment}_content_type")
      )
    end
  end
end

Want to stay up-to-date with Ruby on Rails?

Join 86,796+ developers who get early access to new tutorials, screencasts, articles, and more.

    We care about the protection of your data. Read our Privacy Policy.