views:

296

answers:

2

I have a model, let's say Attachments, that uses attachment_fu to accept file uploads from the user. I want to "deep copy" (or in Ruby-ese, deep clone) an Attachment, thus creating a completely new binary object in the "db_files" table.

I've found that it is not quite a solved problem yet. This blog posting: http://www.williambharding.com/blog/rails/rails-faster-clonecopy-of-attachment_fu-images/

Shows a method that allegedly works for filesystem based storage. For db-based stores, the "deep copy" fails. A new "Attachment" is created but it uses the pre-existing db_file_id, thus performing a shallow copy.

Inside attachment_fu's db_file_backend.rb I see the save method:

      # Saves the data to the DbFile model
      def save_to_storage
        if save_attachment?
          (db_file || build_db_file).data = temp_data
          db_file.save!
          self.class.update_all ['db_file_id = ?', self.db_file_id = db_file.id], ['id = ?', id]
        end
        true
      end

So, I am trying to decipher this and I believe "build_db_file" is some Ruby metaprogramming magic shorthand for DbFile.new although I cannot confirm this (grepping the source shows no mention of this, nor can I find it on google).

I'm not quite sure what it is doing, but my theory is that the db_file is being copied from the source obj as part of the "Deep copy" attempt (in the linked code) thus it is simply triggering a save instead of a create.

My initial theory was that the parent (Attachment) object would be set to "new" upon a deep copy attempt, thus I did something like:

 def save_to_storage
    if save_attachment?
      if self.new_record?
        db_file = DbFile.new :data => temp_data
        self.class.update_all ['db_file_id = ?', self.db_file_id = db_file.id], ['id = ?', id]
      end
    end
    true
  end

This actually works fine for cloned objects but unfortunately all the tests for regular, non cloned file uploads fail. The Attachment object is created but no data is written to db_file. Theory is that the parent object is saved first, then the db_file stuff is written later, thus new_record? returns false.

So, as an experiment I decided to try:

  def save_to_storage
    if save_attachment?
      if self.new_record?
        db_file = DbFile.new :data => temp_data
        self.class.update_all ['db_file_id = ?', self.db_file_id = db_file.id], ['id = ?', id]
      else
        (db_file || build_db_file).data = temp_data
        db_file.save!
        self.class.update_all ['db_file_id = ?', self.db_file_id = db_file.id], ['id = ?', id]
      #end
    end
    true
  end

That works partially - the db_file is populated but then I get an error on db_file.save! - saying that db_file is nil.

So, I'm sort of stymied. I can do some further trial and error but at this point I've hit my limited understanding of how this plugin works. I really didn't expect or want to spend this much time on it so I am reluctant to try and explore attachment_fu any further, but I'm afraid I'm going to have to go down the rabbit hole to figure it out. Any ideas or thoughts?

Thanks!!

+1  A: 

This is just a partial response explaining the build_db_file call

As you suspected, the build_db_file call executes a method generated by creating a belongs_to association. The association is created here:

def self.included(base) #:nodoc:
   Object.const_set(:DbFile, Class.new(ActiveRecord::Base)) unless Object.const_defined?(:DbFile)
   base.belongs_to  :db_file, :class_name => '::DbFile', :foreign_key => 'db_file_id'
end

So the (db_file || build_db_file) statement takes an existing associated DbFile object, or creates a new one if it's nil, and assigns the temp_data to its binary field data. The temp_data is probably the byte array with the data from the form.

And I have one question (I can't comment on your question) - why don't you call db_file.save! after creating it with

db_file = DbFile.new :data => temp_data

?

Matt
Good question. That's a bug on my part, although it doesn't fix the problem :) Thanks!
Matt Rogish
A: 

Okay, so instead of figuring out how to create a new db_file (which is wasteful in our particular case), I just monkey-patched destroy_file to only delete the db_file if there are no more attachment records pointing to it. This may not be appropriate if you allow someone to "modify" an attachment db_file in situ but since we don't, this works great.

Technoweenie::AttachmentFu::Backends::DbFileBackend.module_eval do
  protected
  def destroy_file
    if db_file && self.class.count( :conditions =>["id <> ? AND db_file_id = ?", self.id, db_file.id] ) == 0
      db_file.destroy 
    end
  end
end
Matt Rogish