I have a Job
model which can have many attachments. The Attachment
model has a CarrierWave uploader mounted on it.
class Job < ActiveRecord::Base
has_many :attachments
end
class Attachment < ActiveRecord::Base
mount_uploader :url, AttachmentUploader
belongs_to :job
end
Jobs can be cloned and cloning a job should create new Job and Attachment records. This part is simple.
The system then needs to copy the physical files to the upload location associated with the cloned job.
Is there a simple way to do this with CarrierWave? The solution should support both the local filesystem and AWS S3.
class ClonedJob
def self.create_from(orig_job)
@job_clone = orig_job.dup
if orig_job.attachments.any?
orig_job.attachments.each do |attach|
cloned_attactment = attach.dup
# Need to physically copy files at this point. Otherwise
# this cloned_attachment will still point to the same file
# as the original attachment.
@job_clone.attachments << cloned_attachment
end
end
end
end
I've pasted below the module I whacked together to accomplish this. It works but there is still a couple of things I would improve if it mattered enough. I just left my thoughts inline in the code.
require "fileutils"
# IDEA: I think it would make more sense to create another module
# which I could mix into Job for copying attachments. Really, the
# logic for iterating over attachments should be in Job. That way,
# this class could become a more generalized class for copying
# files whether we are on local or remote storage.
#
# The only problem with that is that I would like to not create
# a new connection to AWS every time I copy a file. If I do then
# I could be opening loads of connections if I iterate over an
# array and copy each item. Once I get that part fixed, this
# refactoring should definitely happen.
module UploadCopier
# Take a job which is a reprint (ie. it's original_id
# is set to the id of another job) and copy all of
# the original jobs remote files over for the reprint
# to use.
#
# Otherwise, if a user edits the reprints attachment
# files, the files of the original job would also be
# changed in the process.
def self.copy_attachments_for(reprint)
case storage
when :file
UploadCopier::LocalUploadCopier.copy_attachments_for(reprint)
when :fog
UploadCopier::S3UploadCopier.copy_attachments_for(reprint)
end
end
# IDEA: Create another method which takes a block. This method
# can check which storage system we're using and then call
# the block and pass in the reprint. Would DRY this up a bit more.
def self.copy(old_path, new_path)
case storage
when :file
UploadCopier::LocalUploadCopier.copy(old_path, new_path)
when :fog
UploadCopier::S3UploadCopier.copy(old_path, new_path)
end
end
def self.storage
# HACK: I should ask CarrierWave what method to use
# rather than relying on the config variable.
APP_CONFIG[:carrierwave][:storage].to_sym
end
class S3UploadCopier
# Copy the originals of a certain job's attachments over
# to a location associated with the reprint.
def self.copy_attachments_for(reprint)
reprint.attachments.each do |attachment|
orig_path = attachment.original_full_storage_path
# We can pass :fog in here without checking because
# we know it's :fog since we're in the S3UploadCopier.
new_path = attachment.full_storage_path
copy(orig_path, new_path)
end
end
# Copy a file from one place to another within a bucket.
def self.copy(old_path, new_path)
# INFO: http://goo.gl/lmgya
object_at(old_path).copy_to(new_path)
end
private
def self.object_at(path)
bucket.objects[path]
end
# IDEA: THis will be more flexible if I go through
# Fog when I open the connection to the remote storage.
# My credentials are already configured there anyway.
# Get the current s3 bucket currently in use.
def self.bucket
s3 = AWS::S3.new(access_key_id: APP_CONFIG[:aws][:access_key_id],
secret_access_key: APP_CONFIG[:aws][:secret_access_key])
s3.buckets[APP_CONFIG[:fog_directory]]
end
end
# This will only be used in development when uploads are
# stored on the local file system.
class LocalUploadCopier
# Copy the originals of a certain job's attachments over
# to a location associated with the reprint.
def self.copy_attachments_for(reprint)
reprint.attachments.each do |attachment|
# We have to pass :file in here since the default is :fog.
orig_path = attachment.original_full_storage_path
new_path = attachment.full_storage_path(:file)
copy(orig_path, new_path)
end
end
# Copy a file from one place to another within the
# local filesystem.
def self.copy(old_path, new_path)
FileUtils.mkdir_p(File.dirname(new_path))
FileUtils.cp(old_path, new_path)
end
end
end
I use it like this:
# Have to save the record first because it needs to have a DB ID.
if @cloned_job.save
UploadCopier.copy_attachments_for(@cloned_job)
end
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With