Working with attachment_fu

Posted by Trevor in Ruby/Rails on March 25, 2007

Now that the fantastically easy-to-use file-managing plugin for Rails attachment_fu Rails plugin is production-ready, Mike Clark has published a great tutorial on how to use it.

After following Mike's instructions, I started poking around in my app and discovered that attachment_fu saves uploads in subdirectories based upon the file id. That makes sense in a lot of ways, but it's not compatible with my app's current structure. What I need is to save the uploaded files in a single directory. So, if I have a Model that has_attachment called "uploads", I'd like all of the uploaded files to be stored in the "uploads" directory - sans subdirectories.

Rick offers a hint at a solution, but for those of us who aren't quite so technically-inclined, I figured a quick code snippet might help. Here's my /app/models/upload.rb:

class Upload < ActiveRecord::Base
 
  has_attachment :storage => :file_system, :path_prefix => 'public/uploads'
  validates_as_attachment
  validates_uniqueness_of :filename
 
  # Update: Consider using the new partition option instead http://github.com/technoweenie/attachment_fu/commit/8a1e64
  def full_filename(thumbnail = nil)
    file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix].to_s
    File.join(RAILS_ROOT, file_system_path, thumbnail_name_for(thumbnail))
  end
 
end

That's all you need to get your uploads uploaded to public/uploads :) We're overwriting the full_filename bit to remove one tiny little bit that specifies the subdirectory. We're also using "validates_uniqueness_of" to ensure that the filename is unique and avoid overwriting existing files with the same name.

Now, that's only half the battle if you're working with an app in production. I'm using Capistrano, as I'm sure you are. The way Capistrano works, anything that gets uploaded into your production app is going to be "lost" when you deploy a new release. In reality, your files are left alone, it's just that they're stuck in an old "release" directory and are basically useless there.

So, how do you upload files to your production app? First, you'll need a directory for your uploads in your shared/system folder. This shared/system folder is something that Capistrano sets up to hold files that you'll want to keep as you deploy new releases. It's the perfect place to store uploaded content. The result will look something like this:

appname => shared => system => uploads

Second, you'll need a symlink that points to this shared directory. This symlink will allow your app to pretend as though it's working within public/uploads, when it's actually saving everything to shared/system/uploads. That way, everything will work in the same way between your development and production environments, and you won't lose uploaded files in your production app.

Luckily, this can all be accomplished by adding a few lines to your deploy.rb file. Here's the snippet:

task :after_update_code, :roles => :app do
  %w{uploads whatever somethingelse}.each do |share|
    run "rm -rf #{release_path}/public/#{share}"
    run "mkdir -p #{shared_path}/system/#{share}"
    run "ln -nfs #{shared_path}/system/#{share} #{release_path}/public/#{share}"
  end
end

Unfortunately, I can't remember where I got this tip from, but it's a great bit of DRY code. Basically, this will allow you to add as many different upload types as you like to your app, with very little code. If you're only using "uploads" then you can delete the "whatever" and "somethingelse" parts, but I figured I should leave them in there to illustrate the point.

So, that's just about it. One last thing is to ignore the files that you upload into your development app. There's no point in putting those into version control, and it's easy to ignore them with Subversion. Here's the code snippet that I needed to run from my app's "trunk" directory:

svn propset svn:ignore "*" public/uploads/

That'll ignore all files and subfolders of your public/uploads directory.

I think that's all there is to it! Thanks very much to Mike Clark for his excellent tutorial, and (of course) to Rick Olson for his totally awesome plugin!

Update: I'm going to follow up on this when I have more information, but it seems that there's a problem where Apache automatically appends trailing slashes in cases where you've got a directory/symlink in your public folder that matches a named route. This can cause strange routing issues that are difficult to track down, but there appears to be an easy fix. Adding this:

DirectorySlash Off

...to your .htaccess file will disable Apache's addition of the trailing slash, and you should be good to go.

16 Comments

 Tiago Serafim

Thank you very much!

Added you to my feed reader.

[...] So after gleaning from “almost effortless’” Working with attachment_fu article this is what I did: [...]

 Thomas Sinclair

Thank you very much for this tutorial, made my weekend a little less stressed ;)

 Rob

Cheers for the bit of deploy code, was wondering how to move my uploads folder on each deploy! nice one.

 Conroy

Thanks so much for this. New to capistrano and was unsure where my uploads went. Your deploy addition worked perfectly!

[...] was frustrated with the amount of duplicated code sprinkled in my models that are using the attachment_fu plugin to deal with file uploads. It’s not like I need to write a plugin or anything fancy – I just [...]

[...] in the old release suddenly becomes unavailible! And that includes the files by attachment_fu. And article on Almost Effortless pointed this out several months ago and posted a solution in the form of a [...]

 Travis

Thanks so much! Quick and simple solution to my problem!

[...] Also, the folder will get removed on deployment. Here’s a tutorial to help solve that problem [...]

 Danny

Just to make it clear why attachment fu probably does this directory structure based on the id. The users of one of the larger applications I have worked on where able to upload photo’s and documents (resumes in this case). Since we had about 300.000 users and growing, most of them uploading these things we soon found out that on linux environments (in our case ubuntu) there is a limit to the amount of items each one directory can contain. I believe it is somewhere around 30.000 files in one directory, or something in the likes. We had been trying to figure out why all of a sudden all uploading functionality crashed without any reason. The reason was the linux filesystem :p We have actually hacked file_column (which we used and doesn’t have this subdirectory by id thing) to store files the same way as attachment_fu. So be advised I would say ;)

Greetings.

 Trevor

Danny, thanks for that tip. For those who are interested in reading more, this is the technique attachment_fu uses:

http://www.37signals.com/svn/archives2/id_partitioning.php

Thank you very much for this nice post!

[...] file uploads with attachment_fu is super easy, but I’ve found that users sometimes want to be able to “upload” a file directly from a URL. This [...]

[...] Working with attachment_fu [...]

Thanks for this post! I found it very useful.

Leave a comment

WP_Big_City