From CarrierWave to Active Storage

Huy Hoang
4 min readDec 17, 2020

--

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.
  • 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) .
  • Active Storage does not have built-in validation helper as we have in CarrierWave. Fortunately, we can use theactive_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 testenvironments

# Active Storage
config.active_storage.service = :local

andamazon 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, theprofile_picture column and the carrierwave gem.

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

--

--

Huy Hoang
Huy Hoang

Responses (2)