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:
1. Update the location generation
Since Shrine generates the location only once during upload, it is safe to change
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.
# change location generationendend
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
:cache storage will be uploaded to the new location on promotion.
Photo.find_each do |photo|attacher = photo.image_attachernext unless attacher.stored? # move only attachments uploaded to permanent storageold_attacher = attacher.dupattacher.set attacher.upload(attacher.file) # reupload fileattacher.set_derivatives attacher.upload_derivatives(attacher.derivatives) # reupload derivatives if you have derivativesbeginattacher.atomic_persist # persist changes if attachment has not changed in the meantimeold_attacher.destroy_attached # delete files on old locationrescue Shrine::AttachmentChanged, # attachment has changed during reuploadingActiveRecord::RecordNotFound # record has been deleted during reuploadingattacher.destroy_attached # delete now orphaned filesendend
Now all your existing attachments should be happily living on new locations.
For faster migration, we can also delay moving files into a background job:
Photo.find_each do |photo|attacher = photo.image_attachernext unless attacher.stored? # move only attachments uploaded to permanent storageMoveFilesJob.perform_async(attacher.class.name,attacher.record.class.name,attacher.record.id,attacher.name,attacher.file_data,)end
include Sidekiq::Workerattacher_class = Object.const_get(attacher_class)record = Object.const_get(record_class).find(record_id) # if using Active Recordattacher = attacher_class.retrieve(model: record, name: name, file: file_data)old_attacher = attacher.dupattacher.set attacher.upload(attacher.file)attacher.set_derivatives attacher.upload_derivatives(attacher.derivatives)attacher.atomic_persistold_attacher.destroy_attachedrescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFoundattacher&.destroy_attachedendend