Sorry this page looks weird. It was automatically migrated from my old blog, which had a different layout and different CSS.

How To Store Thumbnails and Full Size Images In Different Directories With attachment_fu

Catchy title!

When you want to upload and resize images in Rails, attachment_fu is your best bet. Mike Clark wrote a good tutorial explaining how to get started.

If you are storing the images on the file system, attachment_fu puts any given full size image and its thumbnails in the same directory (with different filenames). For example:

photos/0000/0001/piccie.jpg
photos/0000/0001/piccie_small.jpg

You can change the path and customise the filename by overriding the full_filename method in your model. The original implementation is:

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, *partitioned_path(thumbnail_name_for(thumbnail)))
end

This is how it works:

The file\system\path is set to your model’s (or your thumbnail model’s) pathprefix. By default this is the model’s table name under public. So if your model is Photo, file\system\_path becomes public/photos.

The following line is more involved.

thumbnail\name\for(thumbnail) returns the image’s filename. If thumbnail is nil, the original filename is returned — e.g. piccie.jpg. If a value is given, such as small, it returns the thumbnail’s filename — e.g. piccie_small.jpg.

The partitioned_path method returns a path based on the image’s database ids such that your file system won’t be brought to its knees by overflowing directories.

The File.join line concatenates these components to give a path like /path/to/your/app/public/photos/0000/0001/piccie.jpg.

So far, so good. You can easily change the paths to store your images wherever you like, bearing in mind that a full size image and its thumbnails will be stored in the same directory.

But what happens if you want to store full size images and their thumbnails in different directories? It’s not quite as easy as it first seems.

You might think you could branch on the thumbnail argument like this:

def full_filename(thumbnail = nil)
  file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix].to_s
  if thumbnail
    File.join(RAILS_ROOT, file_system_path, 'thumbnails', *partitioned_path(thumbnail_name_for(thumbnail)))
  else
    File.join(RAILS_ROOT, file_system_path, 'fullsizes', *partitioned_path(thumbnail_name_for(thumbnail)))
  end
end

But it turns out that the other parts of attachment_fu don’t pass in thumbnail when you might expect and all the images end up in the fullsizes directory path.

This, however, does the trick:

def full_filename(thumbnail = nil)
  file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix].to_s
  if thumbnail.blank? && self.thumbnail.blank?
    File.join(RAILS_ROOT, file_system_path, 'fullsize', *partitioned_path(thumbnail_name_for(thumbnail)))
  else
    File.join(RAILS_ROOT, file_system_path, 'thumb', *partitioned_path(thumbnail_name_for(thumbnail)))
  end
end

The point to notice is that thumbnail is an argument to the method while self.thumbnail is an attribute of your model. They are not the same.

It reminds me of that baffling joke told by ontologists: “What’s the difference between a duck? One of its legs are both the same.” Except the other way around.

Comments

Hey great work man. I was encountering the same issue and your post did the trick. Thanks a ton man. Keep up the good work. Great code.

dubaimacha • 21 August 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
# whatever value you want to test for.
  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

#...
  1. 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 
 # ...
Paul • 24 August 2007

Subclassing does the trick too :


class Asset < ActiveRecord::Base
  has_attachment :thumbnail_class=>Image,
    :file_system_path => '/downloads',
    :content_type=>:image, 
    :thumbnails=>{:thumb=>'80', :big=>'600x480'},
    :max_size => 10.megabyte
end

class Image < Asset
  has_attachment :storage => :file_system,
    :processor=>MiniMagick,
    :content_type=>:image,
    :file_system_path => 'public/images/photos'

end

chears

charly • 12 September 2007

Hi there,

I see this post is quite old, but i’m new to ruby on rails now. I used your post to learn how to change the full_filename, and i did with success BUT there a major thing that you didn’t mention in your post and i only realized later:

it changes the filename, but it doesn’t change the filename on the database!

the file in the filesystem is correct, but the database still carries the old file name

do you know what do i have to do to correct such issue?

i’m really strugling here

i already tried adding after File.join:

write_attribute :filename, my_nem_filename

with no success!

thanks.

Marco • 9 May 2008

Marco, the article describes how to change the path to your file, but not the file’s name. I realise now I didn’t make that clear, so sorry for the confusion.

You can set the actual filename by overriding the uploaded_data method in your model, like this:

def uploaded_data=(file_data)
  super
  self.filename = 'your_filename_here'
end

Hope that helps.

Andy Stewart • 12 May 2008

Andrew Stewart • 20 June 2007 • Rails
You can reach me by email or on Twitter.