Getting attachment_fu to play nice with acts_as_versioned

Posted by unixmonkey on February 09, 2008

If you’ve ever wanted to keep track of revisions to document files or images in your Rails app, you are likely to want to use Acts_as_versioned, which is the authority on versioning database records, and Attachment_fu, which is the authority on uploading files with Rails.

The problem is that they don’t know about each other and will step on each other’s toes without some changes. This article serves as a quick introduction to each, and shows how to make the two plugins get along like best friends.

Acts_as_versioned was written by Rails Core Team member Rick Olsen (who also wrote attachment_fu and Restful_authentication among others) that essentially makes a mirror table of the one you want to version, and keeps every version of the record you are updating.

Say I have a document table with fields like this:

id title description
1 rep08 2008 report

Acts_as_versioned will add a column “version”, and a separate table “document_versions”.

id title description version
1 rep08 2008 report 1

The document_versions table will look a bit like this

id document_id title description version
1 1 rep08 2008 report 1

Setting up acts_as_versioned is pretty simple, I got most of my introduction to it from urbanhonking.com

Now every time you update the original document, the changes are saved in your main documents table, and the version column is incremented by 1.

After a few edits of the document, you’ll see the versioning information in the Document_versions table add up.

id document_id title description version
1 1 rep08 2008 report 1
2 1 rep08 2008 report changed 2
3 1 rep08 chgd 2008 report changed 3

Great! We can now use some of acts_as_versioned’s built-in methods for determining if there are older versions, and be able to view or even revert to them.

Now lets add the ability to upload a file to attach to a document record with attachment_fu.

Attachment_fu is another plugin that makes uploading files and keeping track of them in the database relatively simple.

A good intro to attachment_fu can be found on Mike Clark’s blog

Attachment_fu would require a few changes to our documents table:

id title description version filename content_type size
1 rep08 2008 report 1 rep08.jpg image/jpeg 2854

Don’t forget to add the same fields to your documents_versions table, too.

Once we’ve added the right file fields to the new and edit forms, and image_tag or download link on the show view, we’ve got working file uploads. Nice.

Try to edit a record by attaching a new file, the new file is displayed and the record is preserved as an older version in the versioned table. But if you try to view the old version…wait a minute? Where did my version 1 file go!

That’s right, attachment_fu deletes the old file when you add a new one (as it should if you aren’t versioning your data). Attachment_fu’s rename_file method is the one responsible for deleting (or renaming) the old file when a new one is added, so lets monkeypatch that in our model to not do anything.

def rename_file
end

Now, it will only overwrite the file if the filename is the same. Lets store each version in its own folder to keep them from clobbering each other by monkey-patching the path files get written to in our model also:

def attachment_path_id
  "/#{id}/v#{version}/"
end
def partitioned_path(*args)
  attachment_path_id + args.to_s
end

This changes the public path from /0000/0001/rep08.jpg to /1/v1/rep08.jpg

Now, if we want to display the image, we cannot use the ‘public_filename’ method, because it is only given to the Document model, and not the Document_Version model.

That’s okay, because with our new path arrangement, we can reliably predict where the old versions of the files will be kept. You can show them with some code similar to this in your views:

<% for version in @document.versions %>
 
  Version <%= version.version %>
  <%= image_tag("/documents/#{@document.id}/v#{version.version/" + version.filename) %>
  <hr />
 
<% end %>

Now, when we delete a record, attachment_fu only knows about the current document, and will leave behind orphaned files and folders from the old versions. Lets fix that by having it get rid of the document id folder.

Rails reserves some special methods (callbacks) for performing actions before or after other major actions, lets tap into that by defining a method that will magically get called every time we delete a record.

def after_destroy
  FileUtils.rm_rf(RAILS_ROOT + "/public/documents/#{id}/")
end

This translates into the shell command rm -rf and deletes our ID directory and everything inside it.

Hooray!

As a wrap up, lets look at our complete Document model:

class Document < ActiveRecord::Base
  acts_as_versioned
  has_attachment :storage => :file_system
 
  def rename_file
  end
 
  def attachment_path_id
    "/#{id}/v#{version}/"
  end
 
  def partitioned_path(*args)
    attachment_path_id + args.to_s
  end
 
  def after_destroy
    FileUtils.rm_rf(RAILS_ROOT + "/public/documents/#{id}/") if id
  end
 
end

I’ve whipped up a sample Rails app demonstrating the points and code in this article. It uses Rails 2.0.2 with the sqlite3 database.

Download it here: Attachments_versioned (240kb .zip)

I hope this saves some work for someone who wants to leverage these two excellent plugins by Rick Olsen (technoweenie) on the same model without having them fight too much.