Migrating File Locations
This guide shows how to migrate the location of uploaded files on the same storage in production, with zero downtime.
Let's assume we have a Photo
model with an image
file attachment:
Shrine.plugin :activerecord
class ImageUploader < Shrine
# ...
end
class Photo < ActiveRecord::Base
include ImageUploader::Attachment(:image)
end
1. Update the location generation
Since Shrine generates the location only once during upload, it is safe to change
the Shrine#generate_location
method. All the existing files will still continue
to work with the previously stored urls because the files have not been migrated.
class ImageUploader < Shrine
def generate_location(io, **options)
# change location generation
end
end
We can now deploy this change to production so new file uploads will be stored in the new location.
2. Move existing files
To move existing files to new location, run the following script. It fetches
the photos in batches, downloads the image, and re-uploads it to the new location.
We only need to migrate the files in :store
storage need to be migrated as the files
in :cache
storage will be uploaded to the new location on promotion.
Photo.find_each do |photo|
attacher = photo.image_attacher
next unless attacher.stored? # move only attachments uploaded to permanent storage
old_attacher = attacher.dup
current_file = old_attacher.file
attacher.set attacher.upload(attacher.file) # reupload file
attacher.set_derivatives attacher.upload_derivatives(attacher.derivatives) # reupload derivatives if you have derivatives
begin
attacher.atomic_persist(current_file) # persist changes if attachment has not changed in the meantime
old_attacher.destroy_attached # delete files on old location
rescue Shrine::AttachmentChanged, # attachment has changed during reuploading
ActiveRecord::RecordNotFound # record has been deleted during reuploading
attacher.destroy_attached # delete now orphaned files
end
end
Now all your existing attachments should be happily living on new locations.
Backgrounding
For faster migration, we can also delay moving files into a background job:
Photo.find_each do |photo|
attacher = photo.image_attacher
next unless attacher.stored? # move only attachments uploaded to permanent storage
MoveFilesJob.perform_async(
attacher.class.name,
attacher.record.class.name,
attacher.record.id,
attacher.name,
attacher.file_data,
)
end
class MoveFilesJob
include Sidekiq::Worker
def perform(attacher_class, record_class, record_id, name, file_data)
attacher_class = Object.const_get(attacher_class)
record = Object.const_get(record_class).find(record_id) # if using Active Record
attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
old_attacher = attacher.dup
current_file = old_attacher.file
attacher.set attacher.upload(attacher.file)
attacher.set_derivatives attacher.upload_derivatives(attacher.derivatives)
attacher.atomic_persist(current_file)
old_attacher.destroy_attached
rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
attacher&.destroy_attached
end
end