From CarrierWave to Active Storage
By: Huy Hoang
Tags: Active Storage, Livewire, Carrierwave, Ruby on Rails, Migration

At Livewire Markets, a practice we’re following is to keep upgrading Ruby, Rails, Ruby gems to the latest possible versions, and exploit as much as we can built-in features in Rails.

Attachment handling is a popular and important feature in the Livewire web application. We use it to upload, display contributors’ profile pictures,

attachments and embedded images in wires …

In the old days, we used CarrierWave to manage attachment upload and quite happy with it until Active Storage was born. In this article, I will walk you through how we migrated attachment handling from CarrierWave to Active Storage.

Key differences

Active Storage uses two polymorphic tables active_storage_blobs and active_storage_attachments to store all types of attachments, so we don't need to create a database migration whenever we have a new type of attachment as we need with CarrierWave.

and to store all types of attachments, so we don't need to create a database migration whenever we have a new type of attachment as we need with CarrierWave. Active Storage can do image processing such as resizing at runtime.

CarrierWave uses such a method profile_picture_url to get the URL of the attachment while Active Storage use a helper method url_for(profile.profile_picture) .

to get the URL of the attachment while Active Storage use a helper method . Active Storage does not have built-in validation helper as we have in CarrierWave. Fortunately, we can use the active_storage_validations gem for validation.

Installation

Add the following line into the config/application.rb file

require "active_storage/engine"

Then run

bundle exec rails active_storage:install

It will generate config/storage.yml file for storages configuration, and the database migration to create two tables active_storage_blobs and active_storage_attachments .

Run the migration: bundle exec rake db:migrate

Configuration

Following is our config/storage.yml but you can change it to suit yours.

local:

service: Disk

root: <%= Rails.root.join('storage') %>



amazon:

service: S3

bucket: <%= ENV['AWS_S3_BUCKET'] %>

region: <%= ENV['AWS_REGION'] %>

upload:

cache_control: <%= "public, max-age=#{365.days.to_i}" %>

We use local config for the development and test environments

# Active Storage

config.active_storage.service = :local

and amazon config for production .

# Active Storage

config.active_storage.service = :amazon

Zero downtime migration

We want to keep the business running as usual while we are in the process of the migration. Therefore, we will store attachments in both systems.

mount_uploader :profile_picture, ProfilePictureUploader

has_one_attached :as_profile_picture

Then we implement an Active Record callback to make sure whenever the attachment maintained by CarrierWave changed then the one maintained by ActiveStorage is also updated respectively.

after_commit do

update_active_storage if previous_changes.keys.include?('profile_picture')

end def update_active_storage

self.as_profile_picture.purge if self.as_profile_picture.attached?

sync_profile_picture if self.profile_picture.present?

rescue StandardError -> error

Log.error(error)

end def sync_profile_picture

picture = self.profile_picture

picture.cache_stored_file!

file = picture.sanitized_file.file

content_type = picture.content_type

self.as_profile_picture.attach(io: file, content_type: content_type, filename: self.attributes['profile_picture'])

end

With the above code in place, all newly uploaded attachments will be stored and synced in both systems. Now we can write a rake task to migrate the existing attachments uploaded by CarrierWave to be uploaded and managed by Active Storage as well.

namespace :active_storage do

desc "Migrate profile pictures to use Active Storage"

task migrate_profile_pictures: :environment do

puts '*' * 50

puts "Start migrating #{Profile.count} profiles..."



Profile.find_each do |profile|

next if !profile.profile_picture.present? || profile.as_profile_picture.attached?



profile.sync_profile_picture

end



puts "Completed migrating #{Profile.count} profiles..."

puts '*' * 50

end desc 'Rename as_profile_picture to profile_picture'

task rename_as_profile_picture_to_profile_picture: :environment do

sql = <<-SQL

UPDATE active_storage_attachments

SET name = 'profile_picture'

WHERE name = 'as_profile_picture';

SQL



ActiveRecord::Base.connection.execute(sql)

end

end

We run the first rake task to migrate all existing attachments in CarrierWave to Active Storage.

bundle exec rake active_storage:migrate_profile_pictures

Then we can rename the has_one_attached attribute, remove the CarrierWave uploader, and ActiveRecord hook.

has_one_attached :profile_picture

Finally, we can run the second rake task to update existing records in the active_storage_attachments table to use the new name profile_picture .

bundle exec rake active_storage:rename_as_profile_picture_to_profile_picture

We implement a utility class to process image resizing and return the URL of a given file.

class ActiveStorageUtils

def self.image_url(image, size=nil)

url_for(image, size)

end



def self.file_url(file)

url_for(file)

end



private



def self.url_for(file, size=nil)

return unless file && file.attached?

url_helpers = Rails.application.routes.url_helpers



if size && file.variable?

url_helpers.rails_representation_url(file.variant(resize_to_fill: size).processed, only_path: true)

else

url_helpers.rails_blob_path(file, only_path: true)

end

rescue StandardError => error

Log.error(error)

nil

end

end

Then use it in the model class

# Active Storage

PICTURE_SIZES = {

thumbnail: [20, 20],

medium: [50, 50],

wire: [65, 65],

large: [200, 200]

}.freeze



def profile_picture_url(size=nil)

ActiveStorageUtils.image_url(self.profile_picture, PICTURE_SIZES.fetch(size, nil))

end

Remove CarrierWave

Finally, we can remove the ProfilePictureUploader class, the profile_picture column and the carrierwave gem.

Voila! Now all profile pictures are uploaded and maintained by Active Storage.