Skip to main content

Migrating File Storage

This guides shows how to move file attachments to a different storage in production, with zero downtime.

Let's assume we have a Photo model with an image file attachment stored in AWS S3 storage:

Shrine.storages = {
  cache: Shrine::Storage::S3.new(...),
  store: Shrine::Storage::S3.new(...),
}

Shrine.plugin :activerecord
class ImageUploader < Shrine
  # ...
end
class Photo < ActiveRecord::Base
  include ImageUploader::Attachment(:image)
end

Let's also assume that we're migrating from AWS S3 to Google Cloud Storage, and we've added the new storage to Shrine.storages:

Shrine.storages = {
  ...
  store: Shrine::Storage::S3.new(...),
  gcs:   Shrine::Storage::GoogleCloudStorage.new(...),
}

1. Mirror upload and delete operations

The first step is to start mirroring uploads and deletes made on your current storage to the new storage. We can do this by loading the mirroring plugin:

Shrine.plugin :mirroring, mirror: { store: :gcs }

Put the above code in an initializer and deploy it.

You can additionally delay the mirroring into a background job for better performance.

2. Copy the files

Next step is to copy all remaining files from current storage into the new storage using the following script. It fetches the photos in batches, downloads the image, and re-uploads it to the new storage.

Photo.find_each do |photo|
  attacher = photo.image_attacher

  next unless attacher.stored?

  attacher.file.trigger_mirror_upload

  # if using derivatives
  attacher.map_derivative(attacher.derivatives) do |_, derivative|
    derivative.trigger_mirror_upload
  end
end

Now the new storage should have all files the current storage has, and new uploads will continue being mirrored to the new storage.

3. Update storage

Once all the files are copied over to the new storage, everything should be ready for us to update the storage in the Shrine configuration. We can keep mirroring, in case the change would need to reverted.

Shrine.storages = {
  ...
  store: Shrine::Storage::GoogleCloudStorage.new(...),
  s3:    Shrine::Storage::S3.new(...),
}

Shrine.plugin :mirroring, mirror: { store: :s3 } # mirror to :s3 storage

4. Remove mirroring

Once everything is looking good, we can remove the mirroring:

Shrine.storages = {
  ...
  store: Shrine::Storage::GoogleCloudStorage.new(...),
- s3:    Shrine::Storage::S3.new(...),
}

- Shrine.plugin :mirroring, mirror: { store: :s3 } # mirror to :s3 storage