Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clone record and copy remote files to new location?

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
like image 395
David Tuite Avatar asked Oct 07 '22 18:10

David Tuite


1 Answers

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
like image 95
David Tuite Avatar answered Oct 10 '22 03:10

David Tuite