lib/datashift/loaders/paperclip/attachment_loader.rb
# Copyright:: (c) Autotelik Media Ltd 2012
# Author :: Tom Statter
# Date :: Sept 2012
# License:: MIT. Free, Open Source.
#
# => Provides facilities for bulk uploading/exporting attachments provided by PaperClip gem
#
require 'loader_base'
require 'datashift_paperclip'
module DataShift
module Paperclip
class AttachmentLoader < LoaderBase
include DataShift::Paperclip
attr_accessor :attach_to_klass, :attach_to_find_by_field, :attach_to_field
# We try splitting up inbound file_names in various ways looking for the attachment Owner
attr_accessor :split_file_name_on
attr_reader :loading_files_cache, :missing_records
def initialize
super
@attach_to_klass = nil
@attach_to_find_by_field = nil
@attach_to_field = nil
@split_file_name_on = Regexp.new(/\s+/)
@missing_records = []
end
# => :attach_to_klass
# A class that has a relationship with the attachment (has_many, has_one or belongs_to etc)
# The instance of :attach_to_klass can be searched for and the new attachment assigned.
#
# Examples
# Owner has_many pdfs and mp3 files as Digitals .... :attach_to_klass = Owner
# User has a single image used as an avatar ... :attach_to_klass = User
#
# => :attach_to_find_by_field
# Field on the :attach_to_klass, this is the field used to search for the
# object (class == attach_to_klass) to assign the new attachment to.
#
# Examples
# Owner has a unique 'name' field ... :attach_to_find_by_field = :name
# User has a unique 'login' field ... :attach_to_klass = :login
#
# => :attach_to_field
# Attribute/association to assign attachment to on :attach_to_klass.
# Examples
#
# :attach_to_field => digitals : Owner.digitals = attachment
# :attach_to_field => avatar : User.avatar = attachment
#
def init(attach_to_klass, attach_to_find_by_field, attach_to_field, options = {})
@attach_to_klass = MapperUtils.ensure_class( attach_to_klass )
ModelMethods::Manager.catalog_class(@attach_to_klass, reload: options[:reload], instance_methods: true)
@attach_to_find_by_field = attach_to_find_by_field
@attach_to_field = attach_to_field
end
def init_from_options(options)
init(options[:attach_to_klass], options[:attach_to_find_by_field], options[:attach_to_field])
@split_file_name_on = options[:split_file_name_on] if(options[:split_file_name_on])
end
def find_bindings
# Map field to a suitable call on the Active Record Owner class e.g Owner.digitals
bindings = begin
logger.info("Finding matching field/association [#{attach_to_field}] on class [#{attach_to_klass}]")
binder.map_inbound_fields(attach_to_klass, attach_to_field)
rescue StandardError => e
logger.error("Failed to map #{attach_to_field} to database operator : #{e.inspect}")
logger.error( e.backtrace )
raise MappingDefinitionError, 'Failed to map #{attach_to_field} to database operator'
end
bindings
end
def find_owner(in_file_name, options = {} )
search_term = File.basename(in_file_name, '.*')
search_term.strip!
logger.info("Attempting to find attachment owner (#{attach_to_klass} for [#{search_term}]")
owner_record = get_record_by(attach_to_klass, attach_to_find_by_field, search_term, split_file_name_on, options)
if owner_record
logger.info("#{owner_record.class} (id : #{owner_record.id}) found with matching :#{attach_to_find_by_field} ")
else
message = "No matching owner found for file name : #{search_term}"
failed = FailureData.new(nil, doc_context.node_context, message)
doc_context.progress_monitor.failure(failed)
missing_records << in_file_name
end
owner_record
end
# This version creates attachments and also attaches them to instances of :attach_to_klazz
#
# Each file found in PATH will be processed - it's file_name being used to scan for
# a matching record to attach the file to.
#
# Options
# :add_prefix
#
def perform_load(options = {} )
raise 'The class that attachments belongs to has not been set (:attach_to_klass)' unless @attach_to_klass
raise "The field to search for attachment's owner has not been set (:attach_to_find_by_field)" unless @attach_to_find_by_field
is_dummy_run = DataShift::Configuration.call.dummy_run
@missing_records = []
@load_object = options[:attachment] if options[:attachment]
# Support both directory and file
@loading_files_cache = DataShift::Paperclip.get_files(file_name, options)
logger.info("Found #{loading_files_cache.size} files - splitting names on delimiter [#{split_file_name_on}]")
# Map field to a suitable call on the Active Record Owner class e.g Owner.digitals
bindings = find_bindings
attach_to_method_binding = if bindings.size != 1
logger.warn("Failed to map #{attach_to_field} to database operator")
nil
else
bindings[0]
end
# Iterate through all the files creating an attachment per file
loading_files_cache.each do |in_file_name|
attachment_name = File.basename(in_file_name)
logger.info "Processing attachment file #{attachment_name} "
owner_record = find_owner(in_file_name, options)
# Don't actually create/upload to DB if we are doing dummy run
# Don't create the Attachment if the Owner was not found
next if(is_dummy_run || owner_record.nil?)
attachment = create_paperclip_attachment(load_object_class, in_file_name, options)
# Check if attachment must have an associated owner_record
next unless attachment && owner_record && attach_to_method_binding
reset
# TOFIX - what about has_one etc ? - indicates that Context etc are still too complex and Excel/CSV focused
owner_record.send(attach_to_method_binding.operator) << attachment
logger.info "Added Attachment to #{owner_record.class} (id : #{owner_record.id})"
end
process_missing_records( missing_records )
created = loading_files_cache.size - missing_records.size
puts "Created #{created} / #{loading_files_cache.size} attachments of type #{load_object_class} attached to #{@attach_to_klass}"
puts 'Dummy Run Complete - if happy run with full commit' if(is_dummy_run)
end
end
def process_missing_records( missing_records )
unless missing_records.empty?
FileUtils.mkdir_p('MissingAttachmentRecords') unless File.directory?('MissingAttachmentRecords')
puts "WARNING : #{missing_records.size} of #{loading_files_cache.size} files could not be attached to a #{load_object_class}"
puts "For your convenience copying files with MISSING #{attach_to_klass} to : MissingAttachmentRecords"
missing_records.each do |i|
logger.info("Copying #{i} to MissingAttachmentRecords folder")
FileUtils.cp( i, 'MissingAttachmentRecords') unless(DataShift::Configuration.call.dummy_run)
end
end
end
end
end