Reputation: 368
Starting with ActiveStorage you can know define mirrors for storing your files.
local:
service: Disk
root: <%= Rails.root.join("storage") %>
amazon:
service: S3
access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
region: us-east-1
bucket: mybucket
mirror:
service: Mirror
primary: local
mirrors:
- amazon
- another_mirror
If you add a mirror after a certain point of time you have to take care about copying all files e.g. from "local" to "amazon" or "another_mirror".
Upvotes: 8
Views: 3310
Reputation: 7032
(03-11-2021) On Rails > 6.1.4.1, using active_storage > 6.1.4.1 and within:
Gemfile:
gem 'azure-storage-blob', github: 'Azure/azure-storage-ruby'
config/environments/production.rb
# Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :mirror #:microsoft or #:amazon
config/storage.yml:
amazon:
service: S3
access_key_id: XXX
secret_access_key: XXX
region: XXX
bucket: XXX
microsoft:
service: AzureStorage
storage_account_name: YYY
storage_access_key: YYY
container: YYY
mirror:
service: Mirror
primary: amazon
mirrors: [ microsoft ]
This does NOT work:
ActiveStorage::Blob.find_each do |blob|
blob.mirror_later
end && puts("Mirroring done!")
What DID work is:
ActiveStorage::Blob.find_each do |blob|
ActiveStorage::Blob.service.try(:mirror, blob.key, checksum: blob.checksum)
end && puts("Mirroring done!")
Not sure why that is, maybe future versions of Rails support it, or it needs additional background job setup, or it would have happened eventually (which never happened for me).
TL;DR
If you need to do mirroring for your entire storage immediately, add this rake task and execute it on your given environment with bundle exec rails active_storage:mirror_all
:
lib/tasks/active_storage.rake
namespace :active_storage do
desc 'Ensures all files are mirrored'
task mirror_all: [:environment] do
ActiveStorage::Blob.find_each do |blob|
ActiveStorage::Blob.service.try(:mirror, blob.key, checksum: blob.checksum)
end && puts("Mirroring done!")
end
end
Optional:
Once you mirrored all the blobs, then you probably want to change all their service names if you want them to actually get served from the right storage:
namespace :active_storage do
desc 'Change each blob service name to microsoft'
task switch_to_microsoft: [:environment] do
ActiveStorage::Blob.find_each do |blob|
blob.service_name = 'microsoft'
blob.save
end && puts("All blobs will now be served from microsoft!")
end
end
Finally, change: config.active_storage.service=
in production.rb or make the primary mirror to be the one you want future uploads to go to.
Upvotes: 7
Reputation: 311
I have a couple of solutions that might work for you, one for Rails <= 6.0 and one for Rails >= 6.1:
Firstly, you need to iterate through your ActiveStorage blobs:
ActiveStorage::Blob.all.each do |blob|
# work with blob
end
then...
Rails <= 6.0
You will need the blob's key, checksum, and the local file on disk.
local_file = ActiveStorage::Blob.service.primary.path_for blob.key
# I'm picking the first mirror as an example,
# but you can select a specific mirror if you want
mirror = blob.service.mirrors.first
mirror.upload blob.key, File.open(local_file), checksum: blob.checksum
You may also want to avoid uploading a file if it already exists on the mirror. You can do that by doing this:
mirror = blob.service.mirrors.first
# If the file doesn't exist on the mirror, upload it
unless mirror.exist? blob.key
# Upload file to mirror
end
Putting it together, a rake task might look like:
# lib/tasks/active_storage.rake
namespace :active_storage do
desc 'Ensures all files are mirrored'
task mirror_all: [:environment] do
# Iterate through each blob
ActiveStorage::Blob.all.each do |blob|
# We assume the primary storage is local
local_file = ActiveStorage::Blob.service.primary.path_for blob.key
# Iterate through each mirror
blob.service.mirrors.each do |mirror|
# If the file doesn't exist on the mirror, upload it
mirror.upload(blob.key, File.open(local_file), checksum: blob.checksum) unless mirror.exist? blob.key
end
end
end
end
You may run into a situation like @Rystraum mentioned where you might need to mirror from somewhere other than the local disk. In this case, the rake task could look like this:
# lib/tasks/active_storage.rake
namespace :active_storage do
desc 'Ensures all files are mirrored'
task mirror_all: [:environment] do
# All services in our rails configuration
all_services = [ActiveStorage::Blob.service.primary, *ActiveStorage::Blob.service.mirrors]
# Iterate through each blob
ActiveStorage::Blob.all.each do |blob|
# Select services where file exists
services = all_services.select { |file| file.exist? blob.key }
# Skip blob if file doesn't exist anywhere
next unless services.present?
# Select services where file doesn't exist
mirrors = all_services - services
# Open the local file (if one exists)
local_file = File.open(services.find{ |service| service.is_a? ActiveStorage::Service::DiskService }.path_for blob.key) if services.select{ |service| service.is_a? ActiveStorage::Service::DiskService }.any?
# Upload local file to mirrors (if one exists)
mirrors.each do |mirror|
mirror.upload blob.key, local_file, checksum: blob.checksum
end if local_file.present?
# If no local file exists then download a remote file and upload it to the mirrors (thanks @Rystraum)
services.first.open blob.key, checksum: blob.checksum do |temp_file|
mirrors.each do |mirror|
mirror.upload blob.key, temp_file, checksum: blob.checksum
end
end unless local_file.present?
end
end
end
While the first rake task answers the OP's question, the latter is much more versatile:
Rails > 6.1
Its super easy, just call this on each blob...
blob.mirror_later
Wrapping it up as a rake task looks like:
# lib/tasks/active_storage.rake
namespace :active_storage do
desc 'Ensures all files are mirrored'
task mirror_all: [:environment] do
ActiveStorage::Blob.all.each do |blob|
blob.mirror_later
end
end
end
Upvotes: 19
Reputation: 2025
I've worked on top of https://stackoverflow.com/a/57579839/365218 so the rake task does not assume that the file is in local.
I started with S3, and due to cost concerns, I've decided to move the files to disk and use S3 and Azure as mirrors instead.
So my situation is that for some files, my primary (disk) sometimes don't have the file and my complete repository is actually on my 1st mirror.
So, it's 2 things:
namespace :active_storage do
desc "Ensures all files are mirrored"
task mirror_all: [:environment] do
ActiveStorage::Blob.all.each do |blob|
source_mirror = if blob.service.primary.exist? blob.key
blob.service.primary
else
blob.service.mirrors.find { |m| m.exist? blob.key }
end
source_mirror.open(blob.key, checksum: blob.checksum) do |file|
blob.service.primary.upload(blob.key, file, checksum: blob.checksum) unless blob.service.primary.exist? blob.key
blob.service.mirrors.each do |mirror|
next if mirror == source_mirror
mirror.upload(blob.key, file, checksum: blob.checksum) unless mirror.exist? blob.key
end
end
rescue StandardError
puts blob.key.to_s
end
end
end
Upvotes: 2
Reputation: 71
Everything is stored according to ActiveStorage's keys, so as long as your bucket names and file names aren't changed in the transfer, you can just copy everything over to the new service. See this post for how to copy stuff over.
Upvotes: 1