Ever wondered how to easily add image attachment support to your Rails application? Then you should definitely give attachment_fu a go, a very easy to use Rails plugin by Rick Olson.

(Note: This article would not have been possible without Mike Clark’s excellent attachment_fu tutorial.)

Step 1: Installation (on Ubuntu 6.10)

Installing the plugin is as easy as it gets:
script/plugin install http://svn.techno-weenie.net/projects/plugins/attachment_fu/

In order to do some image processing you need to install one of the following packages as well:

  • ImageScience
  • RMagick
  • minimagick

ImageScience is the simplest of all of them only allowing to resize images. It depends on FreeImage and RubyInline.
This is the one I have ended up using as it is enough for me.
It is not available on Ubuntu repositories, so I had to install it manually following the instructions in their website:

sudo gem install -y image_science

which also installs RubyInline, hoe and rubyforge gems.

Installing FreeImage required me to install cvs (to check out the sources) and g++ first:

sudo apt-get install cvs g++

cvs -z3 -d:pserver:anonymous@freeimage.cvs.sourceforge.net:/cvsroot/freeimage login (just type enter when asked for a password)
cvs -z3 -d:pserver:anonymous@freeimage.cvs.sourceforge.net:/cvsroot/freeimage co -P FreeImage
cd FreeImage
make
sudo make install

Step 2: Preparing your Rails application

In my application I have a Work model to which I want to associate images. Images are submitted by users and are associated to one single Work, a has_many / belongs_to association between a Work and the associated images. My application has also users and I want to know who added a particular image (to prevent abuse).

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

When you store images, attachment_fu makes use of some other useful fields:

  • parent_id: if you store thumbnails to associate them to the parent image (this could actually be used for other type of content as well)
  • thumbnail: as you can have more than one thumbnail, this fields contains the identifier assign to each type of thumbnail.
  • width: the width of the image.
  • heigth: the height of the image.

In my case as I have added the following attributes:

  • work_id: the work that the image is associated to.
  • user_id: the user that added the image
  • default: whether this is the default image to be used when displaying the work
  • created_at: when the image was added

Let’s create the model:

script/generate model WorksImages

My migrations file looks like this one:



class CreateWorkImages < ActiveRecord::Migration

  def self.up

    create_table :work_images, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8' do |t|
      t.column :work_id, :integer, :null => false
      t.column :user_id, :integer, :null => false
      t.column :default, :boolean, :null => false, :default => false
      t.column :created_at, :datetime, :null => false
      t.column :parent_id,  :integer, :null => true
      t.column :content_type, :string, :null => false
      t.column :filename, :string, :null => false
      t.column :thumbnail, :string, :null => true
      t.column :size, :integer, :null => false
      t.column :width, :integer, :null => true
      t.column :height, :integer, :null => true
    end
    execute "alter table work_images add constraint fk_wi_works foreign key (work_id) references works(id)"
    execute "alter table work_images add constraint fk_wi_user foreign key (user_id) references users(id)"
  end

  def self.down
    drop_table :work_images
  end
end

Let’s edit the WorksImages model to make use of the attachment_flu plugin:


class WorkImage < ActiveRecord::Base  
  has_attachment :content_type => :image,
                 :storage => :file_system,
                 :max_size => 100.kilobytes,
                 :resize_to => '200x200>',
                 :thumbnails => { :thumb => '50x50>' },
                 :processor => 'ImageScience'

validates_as_attachment

  belongs_to :work
  belongs_to :user

  #The block will be executed just before the thumbnail is saved.
  #We need to set extra values in the thumbnail class as
  #we want it to have the same extra attribute values as the original image
  #except for the default flag that is always set to false
  before_thumbnail_saved do |record, thumbnail|
    thumbnail.user_id = record.user_id
    thumbnail.work_id = record.work_id
    thumbnail.default = false
  end
  end

I wanted to be able to attach images by providing its url, rather than asking the user to download the image and upload it to the system, This can also be used when querying ecommerce apis (like the amazon one) to retrieve and store the images they return. So I enriched my WorkImage model with an extra method (which I guess would be a good feature to be added to the attachment_fu plugin)


def source_url=(url)
  return nil if not url
  http_getter = Net::HTTP
  uri = URI.parse(url)
  response = http_getter.start(uri.host, uri.port) {|http|
    http.get(uri.path)
  }
  case response
  when Net::HTTPSuccess
    file_data = response.body
    return nil if file_data.nil? || file_data.size == 0
    self.content_type = response.content_type
    self.temp_data = file_data
    self.filename = uri.path.split('/')[-1]
  else
    return nil
  end
end

I also enrich my Work model to easily retrieve associated images. You can easily add new relationships for easy access to thumbnails.


class Work < ActiveRecord::Base
...
  has_many :images, :class_name => 'WorkImage', :conditions => ["work_images.parent_id is null"] #The condition avoids retrieving thumbnails
  #Easily retrieve the default image
  has_one  :default_image, :class_name => 'WorkImage', :conditions => ["work_images.default"]
...
end 

Step 3: Make use of the new model in the controller and view

In my controller, when I want to add an image to a model I do something like the following:


def add_image
...
  #Store the image if any
  if params[:image_source_url]
    image = WorkImage.new(:source_url => params[:image_source_url])
    image.work_id = @work.id
    image.user_id = self.current_user.id
    image.default = true if params[:is_default_image]
    image.save!
  end
...
end

Images will be saved in public/work_images using something that Jaimis buck from 37signals called id partitioning.
That way you can theoretically store 9999 * 10000 attachments (thumbnails are not counted as attachments), which for standard purposes is enough. Anyway, this can easily be changed to support more files if you need it. Look for a method named partitioned_path in vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/file_system_backend.rb.

In order to display the default image in a view I just need to do the following:

<%= image_tag(@work.default_image.public_filename()) %>

If what you want to display is the thumbnail, just pass the thumbnail identifier (in our case :thumb) to the file:

<%= image_tag(@work.default_image.public_filename(:thumb)) %>

And that should be it really. If you have questions, leave a comment.

Note:
I found a small bug in the plugin. It was not storing resized image sizes properly. I had to add edit the vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/image_science_processor.rb file and set the correct size just after the image is saved in the resize_image method:


...
img.save self.temp_path
self.size = File.size(self.temp_path)
...

I also noticed that for images that do not need to be resized, something is done as the size of the images changes, although the dimensions remain the same. I have a file of 5KB that has a size of 12 KB after the resizing process!!! The size of the image is the same and it should have not been modified. Not sure what is going on here but I guess this is an ImageScience issue.

Advertisements

If you want to a validation rule to be applied only when the corresponding attribute has a value, you can use the :allow_nil => true parameter. I don’t know if this works on any Rails model validation rule, but it might well be. The Rails documentation at http://api.rubyonrails.org/ is not very clear in that respect. For each validation rule it lists the parameters it accepts and for the validates_format rule the :allow_nil parameter was not listed. It is listed at the begining as a default parameter for all validation rules though.

In my case I wanted a column in the table to either be null or have a value that matched a particular regular expression, so I ended up with something like

validates_format_of       :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/, :message => ‘Invalid Email address’, :allow_nil => true

Have you ever had the need to access an internal CVS repository from the office and from home or any other place outside your organization’s intranet? This is something I have been doing for quite a while now as I work from home from time to time.

We don’t use any kind of VPN as we don’t have the necessary hardware to set it up properly but I have access to an internal machine from home through ssh that then allows me to connect to the CVS machine.

The tip is to create a SSH tunnel to connect to the CVS machine (I use ssh to access CVS). That way, your CVS repository is always accessed through a port opened in your localhost and so it looks the same to applications regardless of how you connect to the CVS repository.

Imagine the following scenario:

My laptop hostname is laptop.domain, the CVS server is cvs.domain and the machine I use to connect to the intranet from home is gateway.domain.

When I am at home I create the following tunnel (replace username with your login name in gateway.domain):
ssh -L2222:cvs.domain:22 username@gateway.domain

When I am in the office I create the following tunnel:
ssh -L2222:cvs.domain:22 laptop.domain

Note that you MUST NOT replace laptop.domain with localhost in the second ssh tunnel. If you do that, the SSH key associated to localhost will be your laptop’s one and this will prevent you from making a ssh connection to cvs.domain through the tunnel because the SSH host key that will be reported will be the cvs.domain’s one that will conflict with the the SSH key already registered (you laptop’s one) and therefore the client will refuse to open the connection.

To make things easier I have created two aliases that I add to my .bashrc or .alias or .bash_aliases depending on your GNU/Linux distribution:

alias cvs_home=’ssh -L2222:cvs.domain:22 username@gateway.domain’
alias cvs_office=’ssh -L2222:cvs.domain:22 laptop.domain’

When you configure the repository in your CVS client specify:

  • host: localhost
  • port: 2222
  • connection protocol: ext:ssh or ssh