Rails, Attachment_fu - deep copy of database storage attachments
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_fi开发者_如何学运维le_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!!
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
?
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
精彩评论