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.

I am doing this using acts_as_attachment, I will post any results here.. but if you know what i have to be aware of… I really will appreciate if you let me know.
Regards
[...] original info from: http://unixmonkey.net/?p=18#comment-3461 [...]
This worked like a charm! Thanks an incredible amount for this tutorial!
-Nick
A helpful addenda to this article is that if you are calling the same view to view both the document and the document_version, would be in your model to do this:
then in your view you can always call
and never call @document.id, because your document_versions table will have a different value for id.
Hi, looks great. I will be sure to implement this soon. Do you think it would be possible using the S3 feature of attachment_fu. Also, i’ve heard that acts_as_revisionable is a more effective and flexible framework. What are your thoughts?
Cheers
The bundled example works fine for me, but breaks when I put it into an existing app running 2.1.1.
Everything works apart from the file updates (calling public_filename returns the correct path, but the corresponding file doesn’t get created). Working on a solution right now…
Hey David, this is great, thanks for writing it up.
@sandy, yes this works with S3. I have it working there. It will even version your thumbnails for you. Just be sure to have config/amazon_s3.yml set up correctly and change your storage call to :storage => :s3.
Mine is like this:
this article was great !
but i have some question about delete the file
in article when user delete file ALL version will be delete
for example
if i have filename a.txt with version 1,2,3,4,5
when i delete version 5 , version 1,2,3,4 will also delete
so could anyone have solution to delete ONLY SELECTED version
like i choose to delete version 5
it will delete only version 5 not include 1,2,3,4
thanks