Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Shrine file migration Rake task
# frozen_string_literal: true
# Run this with: rake storage:migrate
class ShrineFileMigrationService
def self.migrate_files!(options = {})
dry_run = options[:dry_run]
verbose = options[:verbose]
unless Shrine.version.to_s == '2.19.3'
raise 'This code is only tested to work with Shrine 2.19.3! ' \
'You probably need to update the plugin injection/restoration code ' \
'for backgrounding and keep_files.'
end
# Need to load all model classes for development
# (This is already called for production and test.)
Rails.application.eager_load! if Rails.env.development?
ApplicationRecord.subclasses.flat_map do |model_class|
model_class.ancestors.each do |attachment_class|
next unless attachment_class.is_a? Shrine::Attachment
uploader_class = attachment_class.shrine_class
attachment_name = attachment_class.attachment_name
if verbose
puts "\nMigrating #{model_class}##{attachment_name} (#{uploader_class})"
puts((['-'] * 60).join)
end
# Inject the keep_files plugin so that nothing is deleted automatically
uploader_class.class_eval do
plugin :keep_files, destroyed: true, replaced: true
end
# Unregister any background blocks so that promote and delete is synchronous
backgrounding_promote = uploader_class.opts[:backgrounding_promote]
backgrounding_delete = uploader_class.opts[:backgrounding_delete]
uploader_class.opts.delete :backgrounding_promote
uploader_class.opts.delete :backgrounding_delete
attachment_attribute = "#{attachment_name}_data"
attachment_attacher_name = "#{attachment_name}_attacher"
store_uploader = uploader_class.new(:store)
records = model_class.where.not(attachment_attribute => nil)
records.each do |record|
attacher = record.send(attachment_attacher_name)
current_uploaded_file = record.send(attachment_name)
# Consistent hash for versions / no versions
all_versions = if current_uploaded_file.is_a?(Hash)
current_uploaded_file
else
{ nil => current_uploaded_file }
end
# Only migrate stored files
storages = all_versions.map do |_, file|
file.data['storage']
end
all_store = true
storages.each do |storage|
unless %w[store cache].include?(storage)
raise "Internal error: '#{storage}' is not a valid storage. " \
'Must be one of: store, cache'
end
all_store = false if storage != 'store'
end
# Only migrate files for :store
unless all_store
if verbose
puts "[#{record.class} #{record.id}] " \
"Skipping cache file for #{attachment_name}:"
all_versions.each do |version, file|
puts "=> #{version || 'Location'}: #{file.id}"
end
end
next
end
locations = {}
locations_changed = false
all_versions.each do |version, file|
new_location = store_uploader.generate_location(
nil,
action: :store,
name: attachment_name,
record: record,
version: version,
metadata: file.metadata
)
locations[version] = [file.id, new_location]
locations_changed = true if new_location != file.id
end
unless locations_changed
if verbose
puts "[#{record.class} #{record.id}] " \
"#{attachment_name} location is already up-to-date: "
locations.each do |version, (old_location, _)|
puts "=> #{version || 'Location'}: #{old_location}"
end
end
next
end
if verbose
puts "[#{record.class} #{record.id}] " \
"Migrating #{attachment_name}:"
locations.each do |version, (old_location, new_location)|
puts "=> #{version}: " if locations.count > 1
puts "====> Old: #{old_location}"
puts "====> New: #{new_location}"
end
end
next if dry_run
attacher.copy(attacher)
record.save
# Background deletion code from:
# https://github.com/shrinerb/shrine/blob/v2.19.3/lib/shrine/plugins/backgrounding.rb#L110-L116
data = attacher.class.dump(attacher).merge(
'attachment' => current_uploaded_file.to_json,
'action' => 'replace',
'phase' => 'replace'
)
ShrineJob.perform_at(7.days.from_now, :delete, data)
end
# Clear the keep_files plugin options to restore default behavior.
# See: https://github.com/shrinerb/shrine/blob/v2.19.3/lib/shrine/plugins/keep_files.rb#L10-L12
uploader_class.opts[:keep_files] = []
# Restore any original blocks for the backgrounding plugin
if backgrounding_promote
uploader_class.opts[:backgrounding_promote] = backgrounding_promote
end
if backgrounding_delete
uploader_class.opts[:backgrounding_delete] = backgrounding_delete
end
end
end
end
end
namespace :storage do
desc 'Copy all uploaded files into new generated paths. ' \
'Delete all the original files after 7 days.'
task migrate: :environment do
ShrineFileMigrationService.migrate_files!(
verbose: ENV['VERBOSE'] || ENV['DRY_RUN'],
dry_run: ENV['DRY_RUN']
)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.