We regularly copy images around on our site — for cropping, for duplicating items, and for many other purposes. For the last six months, we’ve been using the code that I found on Ruby forum. It has been passable, but as I’ve been stamping out the bottlenecks in our performance, I’ve found that image duplication has been one of our slower movers. Copying a single image with its two thumbnails was taking about 5-10 seconds per image. Given that all it should be doing, functionally, is making a file copy and a new database entry, this didn’t seem right. I did some research into it today and figured out why.
The reason is that the code on Ruby forum still relies on the main image creating its thumbnails from scratch. That is, the main loop of thumbnail creation in their method isn’t actually saving the thumbnails. The thumbnails get saved on the c.save line, via the standard attachment_fu after_save callback.
I just finished an updated monkey patch that I’m sharing here that should let you copy images without the costly thumbnail re-processing. You can grab attachment_fu mixin.rb or copy the ugly WordPress version below.
module Technoweenie # :nodoc:
module AttachmentFu # :nodoc:
module InstanceMethods
attr_writer :skip_thumbnail_processing
# Added by WBH from http://groups.google.com/group/rubyonrails-talk/browse_thread/thread/e55260596398bdb6/4f75166df026672b?lnk=gst&q=attachment_fu+copy&rnum=3#4f75166df026672b
# This is intended to let us make copies of attachment_fu objects
# Note: This makes copies of the main image AND each of its prescribed thumbnails
def create_clone
c = self.clone
self.thumbnails.each { |t|
n = t.clone
img = t.create_temp_file
n.temp_path = img #img.path -- Commented so that img wo'nt get garbage collected before c is saved, see the thread, above.
n.save_to_storage
c.thumbnails<<n
}
img = self.create_temp_file
# We'll skip processing (resizing, etc.) the thumbnails, unless the thumbnails array was empty. If the array is empty,
# it's possible that the thumbnails for the main image might have been deleted or otherwise messed up, so we want to regenerate
# them from the main image
c.skip_thumbnail_processing = !self.thumbnails.empty?
c.temp_path = img #img.path
c.save_to_storage
c.save
return c
end
protected
# Cleans up after processing. Thumbnails are created, the attachment is stored to the backend, and the temp_paths are cleared.
def after_process_attachment
if @saved_attachment
if respond_to?(:process_attachment_with_processing) && thumbnailable? && !@skip_thumbnail_processing && !attachment_options[:thumbnails].blank? && parent_id.nil?
temp_file = temp_path || create_temp_file
attachment_options[:thumbnails].each { |suffix, size| create_or_update_thumbnail(temp_file, suffix, *size) }
end
save_to_storage
@temp_paths.clear
@saved_attachment = nil
callback :after_attachment_saved
end
end
Include it in your environment.rb and you should be golden.
Notice that it is nearly identical to the Ruby forum code, except that it adds a new member variable to the object being copied so it will just save, not re-process, the thumbnails.
I’ve tested it on my dev machine for a few hours this afternoon and it all seems working, I’ll post back later if I encounter any problems with it. You can do the ame.
Great.
Thnx for this 🙂
Hi,
I tried your script but I always get undefined method temp_path on
n.temp_path = img #img.path — Commented so that img wo’nt get garbage
Any help?
Thanks,
Ah, apparently it relies on some other mixed in attachment_fu methods we’re using. See if adding this to the mixin helps:
# Adds a new temp_path to the array. This should take a string or a Tempfile. This class makes no
# attempt to remove the files, so Tempfiles should be used. Tempfiles remove themselves when they go out of scope.
# You can also use string paths for temporary files, such as those used for uploaded files in a web server.
def temp_path=(value)
temp_paths.unshift value
temp_path
end
Thank you so much! This was perfect.
Hmm, this doesn’t seem to work for db_file-based storage. It creates the appropriate copies of the attachments but it does not copy the db_files data over. The new attachment records point to the db_file_id of the old ones… What is strange is that it does a:
UPDATE db_files SET file_data = ‘hex’ WHERE id = OLD_ID_VALUE. Seems like it ought to be doing an insert…
Any thoughts? Thanks!!!
Note: if you use the Database storage model, this method will not work; it re-uses the files in the DB.
# 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
Since db_file already exists, it’s not creating a new DB file.
Thus, you have to do something like this:
def create_to_storage
if save_attachment?
db_file = DbFile.create :data => temp_data
self.class.update_all [‘db_file_id = ?’, self.db_file_id = db_file.id], [‘id = ?’, id]
end
true
end
However, that creates double entry. I think it is because the DB objects are already created/saved by themselves the Attachment#save method will try and save the associated objects, thus calling create AGAIN and multiple objects.
I fixed that with the following:
Technoweenie::AttachmentFu::Backends::DbFileBackend.module_eval do
# Saves the data to the DbFile model
def save_to_storage
if save_attachment?
# The saving of the Attachment obj also triggers a save_to_storage, even though they are already ther
# so only fire this on the initial creation, not the save.
if self.new_record?
db_file = DbFile.create :data => temp_data
self.class.update_all [‘db_file_id = ?’, self.db_file_id = db_file.id], [‘id = ?’, id]
end
end
true
end
end
However, this modification fails on creation of a new Attachment that is not being cloned. So, I’m at a loss. Pretty close, but it’s tough to try and trace the way it calls these methods over and over… Any help?
Be careful with this if you’re storing your files on S3. That call to c.save_to_storage comes before c.save. This means that if you’re partitioning your files using the file id, you’ll soon discover a proliferation of temporary files in the root of your bucket. This, of course, happens because the cloned, unsaved object doesn’t yet have an id.
Simply omitting c.save_to_storage prevents this from happening.
@Tim Lowrimore
Hi Tim, I’m using S3 and I tried commenting out the save_to_storage calls first but noticed that the images were not being saved. I added them back and didn’t notice any temp files being saved anywhere in the bucket so I don’t think they need to be commented out.
anyone know if this works with rails 3.2