Skip to main content

7 How to Migrate from Paperclip to Rails ActiveStorage

Episode 243 · May 8, 2018

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

Gems ActiveStorage File Uploading


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

Transcripts

No transcripts available. Earn a free month

Discussion


Gravatar
This should return the path to file in this format: /users/avatar/xxx/xx/original/filename
instance.send(attachment).path
Also, in my case i had to use something like:
file_path = k.send(value).path.split('/system', 2).second
because it exists locally and path had system as a prefix.

Gravatar
For Digital Ocean Spaces, could you check the ETag of an object for the checksum rather than downloading the entire thing? Their docs say the ETag is "an MD5 hash of the object".

Gravatar

When I run the migration (with the provided code) I get:

ArgumentError: wrong number of arguments (given 1, expected 2..3)
/Users/nicknoble/products/Lnky/db/migrate/20180531013954_convert_to_active_storage.rb:14:in prepare'
/Users/nicknoble/products/Lnky/db/migrate/20180531013954_convert_to_active_storage.rb:14:in
up'

Gravatar
When running migration:

ArgumentError: You tried to define an enum named "industry" on the model "Company", but this will generate a instance method "horeca?", which is already defined by another enum.  


Gravatar
I think this is because of the first line, when we require all the models. I just commented that line out and worked without that kind of error.

Gravatar
When can we actually remove paperclip?

Gravatar

No (offical) way to use validations (https://github.com/thoughtbot/paperclip#validations) for Active Storage yet. Mayor bummer …


Login or create an account to join the conversation.