lib/saviour/life_cycle.rb
module Saviour
class LifeCycle
SHOULD_USE_INTERLOCK = defined?(Rails) && !Rails.env.test?
class FileCreator
def initialize(current_path, file, column, connection)
@file = file
@column = column
@current_path = current_path
@connection = connection
end
def upload
@new_path = @file.write
return unless @new_path
DbHelpers.run_after_rollback(@connection) do
uploader.storage.delete(@new_path)
end
[@column, @new_path]
end
def uploader
@file.uploader
end
end
class FileUpdater
def initialize(current_path, file, column, connection)
@file = file
@column = column
@current_path = current_path
@connection = connection
end
def upload
dup_temp_path = SecureRandom.hex
dup_file = proc do
uploader.storage.cp @current_path, dup_temp_path
DbHelpers.run_after_commit(@connection) do
uploader.storage.delete dup_temp_path
end
DbHelpers.run_after_rollback(@connection) do
uploader.storage.mv dup_temp_path, @current_path
end
end
@new_path = @file.write(
before_write: ->(path) { dup_file.call if @current_path == path }
)
return unless @new_path
if @current_path && @current_path != @new_path
DbHelpers.run_after_commit(@connection) do
uploader.storage.delete(@current_path)
end
end
# Delete the newly uploaded file only if it's an update in a different path
if @current_path.nil? || @current_path != @new_path
DbHelpers.run_after_rollback(@connection) do
uploader.storage.delete(@new_path)
end
end
[@column, @new_path]
end
def uploader
@file.uploader
end
end
def initialize(model, persistence_klass)
raise ConfigurationError, "Please provide an object compatible with Saviour." unless model.class.respond_to?(:attached_files)
@persistence_klass = persistence_klass
@model = model
end
def delete!
DbHelpers.run_after_commit do
pool = Concurrent::Throttle.new Saviour::Config.concurrent_workers
futures = attached_files.map do |column|
pool.future(@model.send(column)) do |file|
path = file.persisted_path
file.uploader.storage.delete(path) if path
file.delete
end
end
futures.each(&:value!)
end
end
def create!
process_upload(FileCreator)
end
def update!
process_upload(FileUpdater, touch: true)
end
private
def process_upload(klass, touch: false)
persistence_layer = @persistence_klass.new(@model)
uploaders = attached_files.map do |column|
next unless @model.send(column).changed?
klass.new(
persistence_layer.read(column),
@model.send(column),
column,
ActiveRecord::Base.connection
)
end.compact
pool = Concurrent::Throttle.new Saviour::Config.concurrent_workers
futures = uploaders.map { |uploader|
pool.future(uploader) { |given_uploader|
if SHOULD_USE_INTERLOCK
Rails.application.executor.wrap { given_uploader.upload }
else
given_uploader.upload
end
}
}
work = -> { futures.map(&:value!).compact }
result = if SHOULD_USE_INTERLOCK
ActiveSupport::Dependencies.interlock.permit_concurrent_loads(&work)
else
work.call
end
attrs = result.to_h
uploaders.map(&:uploader).select { |x| x.class.after_upload_hooks.any? }.each do |uploader|
uploader.class.after_upload_hooks.each do |hook|
uploader.instance_exec(uploader.stashed, &hook)
end
end
if attrs.length > 0 && touch && @model.class.record_timestamps
touches = @model.class.send(:timestamp_attributes_for_update_in_model).map { |x| [x, Time.current] }.to_h
attrs.merge!(touches)
end
persistence_layer.write_attrs(attrs) if attrs.length > 0
end
def attached_files
@model.class.attached_files
end
end
end