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
andactive_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 methodurl_for(profile.profile_picture)
. - 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
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')
enddef 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)
enddef 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.