β id: changing-location
title: 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, eg.... [ options[:record] && options[:record].class.name.underscore, option[:record] && options[:record].id, super ].compact.join("/") end end
We can now deploy this change to production so new file uploads will be stored in the new location.
As seen above, we can call super to get the include the default location, which uses ruby SecureRandom.hex to have a unique immutable storage location. While it isnβt strictly required to have a unique immutable storage location, it makes many things work smoother when different content will get a different storage location, and is recommended. One approach is using fixed directory/prefix as above.
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. Only 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