Created
October 12, 2019 15:45
-
-
Save ndbroadbent/ab4f08c4dff55665681317f2a034bff8 to your computer and use it in GitHub Desktop.
Shrine file migration Rake task
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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