Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Script to migrate images between providers #9117

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions lib/active_storage/migrator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
require 'yaml'
require 'erb'
require 'logger'

class ActiveStorage::Migrator
Rails.logger = Logger.new($stdout)
Rails.logger.level = Logger::DEBUG

def self.migrate(from_service_name, to_service_name)
configs = load_storage_config
# Check if services are configured correctly
if configs[from_service_name.to_s].nil? || configs[to_service_name.to_s].nil?
raise "Error: The services '#{from_service_name}' or '#{to_service_name}' are not configured correctly."
end

from_service = ActiveStorage::Service.configure(from_service_name, { from_service_name.to_sym => configs[from_service_name.to_s] })
to_service = ActiveStorage::Service.configure(to_service_name, { to_service_name.to_sym => configs[to_service_name.to_s] })

configure_blob_service(from_service)

Rails.logger.debug { "#{ActiveStorage::Blob.count} Blobs to migrate from #{from_service_name} to #{to_service_name}" }

migrate_blobs(from_service, to_service)
end

def self.load_storage_config
yaml_with_env = ERB.new(File.read('config/storage.yml')).result
YAML.load(yaml_with_env)
end

def self.configure_services(from_service_name, to_service_name, configs)
from_service = ActiveStorage::Service.configure(from_service_name, { from_service_name.to_sym => configs[from_service_name.to_s] })
to_service = ActiveStorage::Service.configure(to_service_name, { to_service_name.to_sym => configs[to_service_name.to_s] })
[from_service, to_service]
end

def self.configure_service(service_name, configs)
service_config = configs[service_name.to_s]
ActiveStorage::Service.configure(service_name, { service_name.to_sym => service_config })
end

def self.configure_blob_service(service)
ActiveStorage::Blob.service = service
end

def self.migrate_blobs(_from_service, to_service)
# Configure the blob service for the source service
ActiveStorage::Blob.find_each do |blob|
next unless blob.image?

Rails.logger.debug { '.' }

blob.open do |io|
checksum = blob.checksum
to_service.upload(blob.key, io, checksum: checksum)
end
end
Rails.logger.debug { 'Successful migration' }
end
end
11 changes: 11 additions & 0 deletions lib/tasks/storage_migrations.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace :storage do
desc 'Migrate blobs from one storage service to another'
task migrate: :environment do
from_service = ENV.fetch('FROM', nil)
to_service = ENV.fetch('TO', nil)

raise 'Missing FROM or TO argument. Usage: FROM=service_name TO=service_name rake storage:migrate' if from_service.nil? || to_service.nil?

ActiveStorage::Migrator.migrate(from_service.to_sym, to_service.to_sym)
end
end
31 changes: 31 additions & 0 deletions spec/lib/active_storage/migrator_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
require 'rails_helper'

RSpec.describe ActiveStorage::Migrator do
describe '.migrate' do
let(:from_service_stub) { instance_double(ActiveStorage::Service) }
let(:to_service_stub) { instance_double(ActiveStorage::Service) }

before do
allow(ActiveStorage::Service).to receive(:configure).with('local', any_args).and_return(from_service_stub)
allow(ActiveStorage::Service).to receive(:configure).with('amazon', any_args).and_return(to_service_stub)
end

context 'when services are configured correctly' do
it 'migrates blobs from one service to another' do
expect(ActiveStorage::Service).to receive(:configure).with('local', any_args)
expect(ActiveStorage::Service).to receive(:configure).with('amazon', any_args)
expect(described_class).to receive(:migrate_blobs).with(from_service_stub, to_service_stub)
expect { described_class.migrate('local', 'amazon') }.not_to raise_error
end
end

context 'when services are not configured correctly' do
it 'prints an error message' do
allow(ActiveStorage::Service).to receive(:configure).and_return(nil)
expect do
described_class.migrate('random', 'random')
end.to raise_error(RuntimeError, "Error: The services 'random' or 'random' are not configured correctly.")
end
end
end
end
35 changes: 35 additions & 0 deletions spec/lib/tasks/rake/task_storage_migrations_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
require 'rake'
require 'rails_helper'

RSpec.describe Rake::Task do
describe 'storage_migrations' do
describe 'rake task' do
context 'when FROM argument is missing' do
before do
ENV['FROM'] = nil
end

it 'raises an error' do
expect do
described_class['storage:migrate'].invoke
end.to raise_error(RuntimeError,
'Missing FROM or TO argument. Usage: FROM=service_name TO=service_name rake storage:migrate')
end
end

context 'when TO argument is missing' do
before do
ENV['FROM'] = 'service_name'
ENV['TO'] = nil
end

it 'raises an error' do
expect do
described_class['storage:migrate'].invoke
end.to raise_error(RuntimeError,
'Missing FROM or TO argument. Usage: FROM=service_name TO=service_name rake storage:migrate')
end
end
end
end
end
4 changes: 4 additions & 0 deletions spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
require 'pundit/rspec'
require 'sidekiq/testing'

# Load Rake tasks
require 'rake'
Rails.application.load_tasks

# test-prof helpers for tests optimization
require 'test_prof/recipes/rspec/before_all'
require 'test_prof/recipes/rspec/let_it_be'
Expand Down
Loading