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

Andy Stewart, 20 June 2007

Posted in Rails


  1. 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
  2. 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
  3. 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
  4. 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
    09 May 2008
  5. 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

Have your say

You can use Markdown in your comments. If you want to post code, do this:

<pre><code class="ruby|javascript|css|html">your code here</code></pre>

Thanks!