Working with attachment_fu
March 25th, 2007 by TrevorNow 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.
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: [...]
Thank you very much for this tutorial, made my weekend a little less stressed ;)
[...] Almost Effortless’ Fu-tutorial [...]
Cheers for the bit of deploy code, was wondering how to move my uploads folder on each deploy! nice one.
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 [...]
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 [...]
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.
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 [...]