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 < ActiveRecord::Base
  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

  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.)

Extra credit

If your attachments belong to another model (like a user), you'll need to use a before filter to associate the parent model id with the thumbnail you wish to store.

before_thumbnail_saved do |record, thumbnail|
  thumbnail.user_id = record.user_id
end

You'll also need an extra migration to store the parent id. (Don't use the parent_id field... that's reserved by attachment_fu.)

Extra, extra credit

What if you want three sizes? Large (original image), medium, and small?

has_attachment :content_type => :image,
               :storage => :file_system,
               :max_size => 500.kilobytes,
               :thumbnails => { :small => '100x100>', :medium => '200x200>' },
               :path_prefix => 'public/images/mugshots'
validates_as_attachment

def full_filename(thumbnail = nil)
  file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix]
  case self.thumbnail
  when "small"
    File.join(RAILS_ROOT, file_system_path, 'small', thumbnail_name_for(thumbnail, self.parent_id))
  when "medium"
    File.join(RAILS_ROOT, file_system_path, 'medium', thumbnail_name_for(thumbnail, self.parent_id))
  else
    File.join(RAILS_ROOT, file_system_path, 'large', thumbnail_name_for(thumbnail, self.id))
  end
end
August 21, 2007

Comments

Leonardo Faria Coelho August 21, 2007

It's works with acts_as_attachment?

Patrick Crowley August 21, 2007

Not to my knowledge, Leonardo.

attachment_fu is a significant rewrite of acts_as_attachment, so a lot of the codebase has changed. You should consider upgrading your application, if possible.

Paul August 24, 2007

If you would like to resize images based on whatever, (here the params[:adtype].keys of the AdsController having a certain value) you can do this:

1. Add a custom version of this to your model

#...
include ObjectSpace
# change :ad to whatever model your image belongs to.
belongs_to :ad

def self.find_size
# Change AdsContoller to the controller of your model and params[adtype].keys to<br/>
# whatever value you want to test for.<br/>
if adtype = ObjectSpace.each_object(AdsController) {|a| p a.params[:adtype].keys unless a.params[:adtype] == nil}
    @size = case adtype
    # customize your sizes and conditions
      when 1
         '150x100'
       when 2
         '200x150'
       when 3
         '250x200'
       else
         '250x250'
    end
  end
  return @size || false
end

#modify this as you normally would, but without the :resize_to parameter
has_attachment :content_type => :image,
               :storage => :file_system,
               :max_size => 500.kilobytes,
               :path_prefix => "public/images/"
validates_as_attachment

2. Add this to attachment-fu.rb (after the definition for "has_attachment"

# ...
# this allows for dynamic resizing written in the model itself - PS
   options[:resize_to] = self.find_size if self.respond_to?(find_size) and find_size != false
# ...
mla September 22, 2007

This wasn't working for me. Calling public_filename(:thumb) was always returning the full version since full_filename() is no longer honoring the thumbnail parameter.

This does seem to work for me, however.

def full_filename(thumbnail = nil)
  file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix]
  thumbnail ||= self.thumbnail
  subdir = thumbnail ? thumbnail.to_s : 'full'
  File.join(RAILS_ROOT, file_system_path, subdir, thumbnail_name_for)
end

def thumbnail_name_for(thumbnail = nil)
  extension = filename.scan(/\.\w+$/)
  "#{attachment_path_id.to_s}#{extension}"
end
Markus October 25, 2007

Hi!

I think that customizing filenames should be in attacment_fu by default, but it seems that the people at TechnoWeenie no longer develop this plugin (at least for the seven past months). I had made my approach to this topic in a different way than you.

Basically, I hook the filename itself before the whole attachment being saved. This is made with a simple method and before callback. Works well when we are keepping the filename bound to its owner model's name or whatever.

For creations:

def before_create
self.filename = "some-name-taken-out-from-othe-model-or-filename-normalization" # The file isn't yet save so we change the filename itself without touching any other thing. The thumbnails will be saved using this filename as well.
end

The example above could be a before_add statement in a model association, which is the case I are actually using in muy application. Remember that in Rails versions prior to 2.0 the before_add statement only works if you do model.association << object

For updates:

def filename!(new_filename)
self.temp_path = full_filename # This tells attachment_fu where the old file is and that in the save method it has to rename it.
serf.filename = new_filename # This simply override the actual filename to, when attachment_fu saves the record, rewrite all files, including thumbnails, using this filename.
end

And that's all. Enjoy it!

Steven G November 29, 2007

Great article!

I am struggling a bit with custom directories though. My assets are called "mugshots" and they belong_to :artist. I am trying to hook this up so that the shots get saved in a folder with the artists name.

http://pastie.caboo.se/123257

I can save the main image successfully in the artist folder, but oddly enough the thumbnails are still defaulting to numbered folders.

Any ideas?

John Murch December 05, 2007

Hey,

Thanks for the post about the extra credit: like if your attachments belong to another model (like a user) and you need to store the photo_id but I had a question about this that I can not seem to find the answer to.

Basically I am building a social network where you have to upload images and want to store the photo_id of the image into different models. For example, a Users Profile would be one, a Group would be another, and possible even a blog could have its own image. I was looking for a DRY method that would allow me to easily store these photo_id once they upload the photo and update the model with these photo_id.

So you said you can use:

before_thumbnail_saved do |record, thumbnail|
  thumbnail.user_id = record.user_id
end

But how does this work if you have 3 models you need to store the id of the photo you just uploaded? Or if anyone else has any ideas, they would be greatly appreciated.

Omarvelou February 10, 2008

John,

Polymorphic Associations is what you'd need. Look into that.

a February 25, 2008

Hey, Thanks for the post about the extra credit: like if your attachments belong to another model (like a user) and you need to store the photo_id but I had a question about this that I can not seem to find the answer to. Basically I am building a social network where you have to upload images and want to store the photo_id of the image into different models. For example, a Users Profile would be one, a Group would be another, and possible even a blog could have its own image. I was looking for a DRY method that would allow me to easily store these photo_id once they upload the photo and update the model with these photo_id. So you said you can use before_thumbnail_saved do |record, thumbnail| thumbnail.user_id = record.user_id end But how does this work if you have 3 models you need to store the id of the photo you just uploaded? Or if anyone else has any ideas, they would be greatly appreciated.

buddhi March 04, 2008

I wanted to save all files in a single directory yet change the names.. but gave me so many errors since thumbnail does not exist on creation time.. and we have to use self.thumbnail. did some mods to the code and hope this would be the best place to post the changes (i also got the inspiration to modify the code from here).. thanks for great tips.

modified code will hopefully work for any thumbnail. doesnt have to specify "thumb1", "thumb2" trying to minimize the code size and keeping it generic as possible.

class Photo < ActiveRecord::Base
  has_attachment :content_type => :image, 
                 :storage => :file_system,
                 :size => 1.kilobyte .. 10.megabytes,
                 :resize_to => '500>',
                 :thumbnails => { :thumb => '125' },
                 :path_prefix => 'public/images/product_pics',
                 :processor => 'MiniMagick'
  validates_as_attachment
  belongs_to  :product

  def full_filename(thumbnail = nil)
    file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix]
    @thumb = self.thumbnail unless (@thumb = thumbnail) #checks if thumbnails info already exisit
    File.join(RAILS_ROOT, file_system_path, self.product.code, thumbnail_name_view(@thumb, self.product.code))
  end

  def thumbnail_name_view(thumbnail = nil, asset = nil)
    suffix = "_#{thumbnail}" unless thumbnail.blank?  #sets suffix by the name of the thumbnail set in has_attachment options
    extension = filename.scan(/\.\w+$/) # extracts extension
    return "#{asset}#{suffix}#{extension}"
  end
  
  before_thumbnail_saved do |record, thumbnail|
      thumbnail.product_id = record.product_id
  end

end
nicolas b June 06, 2008

I'd been playing with this for a while. I needed unique filenames so that i could use paranoid delete and based on your example i chose this naming scheme: public/images/photos/small/parent_id.extension. One of my goals was to make image tags easy within the app. I wanted to be able to write something like image_tag("photos/large#{photo.id}#{photo.extension}").This works for me:

def full_filename(thumbnail = nil)
  file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix]
  #stole this out of the attachment fu controller.. greek to me
  ext = nil
  basename = self.filename.gsub /\.\w+$/ do |s|
    ext = s; ''
  end
  case self.thumbnail
  when "medium"
    File.join(RAILS_ROOT, file_system_path, 'medium', "#{self.parent_id.to_s}#{ext}")
  when "small"
    File.join(RAILS_ROOT, file_system_path, 'small', "#{self.parent_id.to_s}#{ext}")
  else
    File.join(RAILS_ROOT, file_system_path, 'large', "#{self.parent_id.to_s}#{ext}")
  end
end

I hope this is helpful to anyone searching for a way to make unique filenames with attachment_fu

nicolas b June 06, 2008

overwrite thumbnail_name_for(thumbnail = nil) in attachment_fu.rb to change the name stored in the database. plugins are loaded when the server starts, so don't make the mistake i did and spend an hour wondering why your changes aren't working: restart the server.

dan August 12, 2008

nicolas b if this works for you it should work for me right I've got a few errors here m8!

Anyway if you could pasting the model file I would like so greatfull!

Ben Pellow November 05, 2008

Despite correctly renaming the file itself and saving it in the correct location, the name of the file in the 'filename' field in the database doesn't get updated...it stays the same as the name of the file itself instead of getting renamed.

Any idea how to get the filename field to update?

For reference, here's my model:

class DevicePicture < ActiveRecord::Base
  belongs_to :manufacturer_model  
  
  has_attachment :content_type => :image,
                 :storage => :file_system,
                 :max_size => 500.kilobytes,
                 :resize_to => '384x256>', 
                 :thumbnails => { :thumb => '98>' },
                 :path_prefix => 'public/device_pictures/'

  validates_as_attachment
  
  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

New comments are disabled right now. Once we finish migrating this blog, we'll restore them.