Tuesday, August 21, 2007

How to customize attachment_fu file names

For uploading files, it's pretty hard to beat attachment_fu. But it can be overkill for smaller projects.

One issue is that attachment_fu uses id partioning. This is a great way to overcome native file system limitations when you have more than 32,000 attachments. By segmenting files into different directories, you can have millions of attachments, if necessary. Empahsis on "if necessary". It usually isn't.

Also, attachment_fu preserves original filenames. While this make sense for many projects, sometimes you need to have control over the naming of attachments.

Since a lot of people use Mike Clark's excellent File Upload Fu tutorial, let's use that as our starting point for customizing file names.

If we complete the tutorial, here's how attachment_fu will store our first image upload:

public/mugshots/0000/0001/chunkybacon.png
public/mugshots/0000/0001/chunkybacon_thumb.png

Hmmm, not bad. But I'd like to customize things:

* Images should be stored in public/images/
* Thumbnails should be organized by size
* ID partioning (0000/0001/) should be disabled
* Images should be renamed with the Mugshot id

So let's open up our Mugshot model and tweak it a bit.

  • class Mugshot <>
  • has_attachment :content_type => :image,
        • :storage => :file_system,
        • :max_size => 500.kilobytes,
        • :resize_to => '320x200>',
        • :thumbnails => { :thumb => '100x100>' },
        • :path_prefix => 'public/images/mugshots'
        • validates_as_attachment # To validate the size of the file being uploaded

  • def full_filename(thumbnail = nil)
    • file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix]
    • case self.thumbnail
      • when "thumb"
        • File.join(RAILS_ROOT, file_system_path, 'thumb', thumbnail_name_for(thumbnail, self.parent_id))
      • else
        • File.join(RAILS_ROOT, file_system_path, 'fullsize', thumbnail_name_for(thumbnail, self.id))
    • end
  • end

  • def thumbnail_name_for(thumbnail = nil, asset = nil)
    • extension = filename.scan(/\.\w+$/)
    • return "#{asset}#{extension}"
  • end
  • end

Now, when we upload an image, it will be stored like so:

public/images/mugshots/fullsize/2.png
public/images/mugshots/thumb/2.png

How does this work?

Well, first, we customize the :path_prefix value in has_attachment to set the base location of our files.

Second, we override the full_filename method to force attachment_fu to save each thumbnail type into its own directory. This way, all large thumbnails are stored in images/mugshots/fullsize and all small thumbnails are stored in images/mugshots/thumb. (By default, attachment_fu stores all thumbnail sizes for an object in a single directory.)

Lastly, we override the thumbnail_name_for method to customize the filename to our liking... in this case, the file name will consist of the parent mugshot id, plus the original file's file extension.

That's all we need to do... now our files are stored exactly where we want them!

(Thanks to AirBlade Software for showing the way.)

Important Note :---

In order to make use of the functionality provided by attachment_fu you need to create an ActiveRecord model with at least the following attributes:

  • content_type: what sort of content you are storing. This is used by web browsers to know how to present this information to users (open an external application, show embedded using a plugin, etc).
  • filename: a pointer to the image location
  • size: the size in bytes of the attachment

4 comments:

Anonymous said...

Thanks for the attribution! I'm glad to have helped out.

Eddie said...

Your method allows images to be saved to custom directories, but what happens when you want to retrieve an image? Attachment_fu allows you to get the location of an image through an instance of its associated model via:

@somemugshot.public_filename() or
@somemugshot.public_filename(:thumb) to get a thumbnail called :thumb.

These methods don't work anymore with your method (they return a partion-style address that doesn't know about the new naming convention). For instance:
@somemughsot.public_filename() -->
"/mugshots/0000/0473/ned.jpg"
@somemughsot.public_filename(:thumb) -->
"/mugshots/0000/0473/ned_thumb.jpg"

Eddie said...

actually, the methods don't return a partition based address, but they return the same address for images and their thumbs..

@somemughsot.public_filename() -->
"/fullsize/473.jpg"
@somemughsot.public_filename(:thumb) -->
"/fullsize/473.jpg" /* same as above - incorrect */

Anonymous said...

Was there a answer for using :thumb to access the image?