Thursday, April 26, 2007

Getting Your attachment_fu Back Out Of The Database

I have been busy the last few nights incorporating the brilliant Rails plugin called attachment_fu from Rick Olson aka technoweenie. If you do not know about attachment_fu, it is a complete solution to the problem of uploading image files attachments to include with your models.

First, I had to get past the initial hurdle of getting ImageScience and FreeImage correctly installed and working on my Mac. Despite the claims that Locomotive should have already had these in the bundle, I had to go thru a small process to get the prerequisites going.

I was then able to easily follow the careful instructions of Mike Clark, who has blogged at some length of how to get started with attachment_fu. In very short order, I had a working upload page, and another page displaying the automatically generated thumbnails. I was happily bragging to myself that "my attachment_fu has grown powerful, ha ha ha".

Then it occurred to me that using the :file_system option which stores uploaded files and thumbnails onto the web server's drive, typically into a special part of the "public" directory, was quick and easy, but not very scalable. Suddenly my attachment_fu is wimpy.

Luckily, attachment_fu supports the idea of "backends", which are code modules designed to communicate with the backend storage to be used for the images and thumbnails. The three provided backends are the file system, database, and Amazon's S3 storage. I decided that switching to the database storage was the best for my particular application. I changed the configuration option as described by Mike Clark, and ran my app expecting everything to just work...

Boom! Errors up the wazoo. OK, I wade in and discover that there is plenty of hidden away functionality in that plugin. First, the database storage backend requires an additional table called "db_files" not mentioned in mike clark's blog. The required additional migration is as follows:


t.column :db_file_id, :integer

create_table :db_files do |t|
t.column :data, :binary
end


You will need to add the "db_file_id" column to the table that stores your attachments. Then you need to create the "db_files" table to hold the actual binary data for the images and thumbnails.

I made the changes, ran the migration, and then I was able to upload the image. Almost there... However, I was shocked to see an error on the page that was supposed to display said image. Uh-oh!

It turns out that although the mechanism for displaying images which are stored in the file system or in S3 are exposed so they are web server accessible, the database storage engine is not yet fully implemented. Most importantly, the "public_filename" method is not there. So images get into the database, but there is no convenient way to display them.

At first I thought that I could use the "create_temp_file" method. However, a Ruby Tempfile deletes itself when it goes out of scope. Not the right thing, unless you don't mind images that disappear before they are actually displayed in the browser.

Anyhow, I went and after studying the code a bit, made a few changes to attachment_fu to provide better support. My solution was to add a method to the database backend to render the binary image data on the fly whenever it is requested.


def image_data(thumbnail = nil)
if thumbnail.nil?
current_data
else
thumbnails.find_by_thumbnail(thumbnail.to_s).current_data
end
end


To keep things nice and restful, I added support to my User controller to render the image when image format is requested:


def show
@user = current_user

respond_to do |format|
format.html # show.rhtml
format.xml { render :xml => @user.to_xml }
format.jpg do
picture = current_user.picture
image = picture.image_data(:thumb)
send_data (image, :type => 'image/jpeg',
:filename => picture.filename,
:disposition => 'inline')
end
end
end


Once this is all completed, displaying the image within a view is simple with a helper:


def user_thumbnail_tag(user)
image_tag("#{user_path(user)}.jpg")
end


And there you have it. The nice thing about this solution is you can deploy on multiple web servers behind a load balancer without concern. Thanks to the awesome power of attachment_fu...you have been warned.

10 comments:

Luis said...

Nice workaround, but that should be handled by AttachmentFu and not you.

You're hardcoding .jpg as format, so any other attached image (gif, png?) will require evaluate you the attachment format/extension. Not DRY :-P

Regarding TempFile, I have a few issues with it on Windows, still patching, but attachment_fu worth the effort ;-)

Regards,

L.

Ron Evans said...

You are correct, Luis, that attachment_fu should probably already handle this. I will try to submit a patch to technoweenie this weekend.

With regards to the hardcoding for JPG format, I had actually already changed my controller implementation as follows:

format.jpg do
image = @picture.image_data(:thumb)
send_data (image, :type => @picture.content_type,
:filename => @picture.filename,
:disposition => 'inline')
end

This will return the correct content type for the image that was stored. If you do not like asking for a JPG and getting back an PNG, you could always create your own custom MIME type like "IMAGE" for something like that.

Roger said...

Ron,

Do you recall what the "small process" you had to go through to get FreeImage and ImageScience working was? Ryan Raaum is off the grid lately, and I can't find any help for this. Much appreciated.

Nilesh said...

Ron, I think you also need to mention this:
for 'format.jpg' to work in the RESTful controller, you have to register the MIME type in environment.rb:

Mime::Type.register "image/jpeg", :jpg

Mark A. Richman said...

I can upload the image to MySQL just fine; I have two tables: photos and db_files, where I see the binary data.

I'm now trying to render the thumbnails as links to the original images, and I'm stuck.

Is there a complete sample somewhere that shows how to do this? I'm lost when it comes to getting the images back out of the db.

Phil Smy said...

I found that I needed to do a definition of the db_files table like this:

create_table :db_files do |t|
end
execute 'ALTER TABLE db_files ADD COLUMN data LONGBLOB'

otherwise the column maxes out at 65535

ricky said...

Hi,
Can you please explain in-detail how to retrieve the BLOB data from db_files table?

ricky said...
This comment has been removed by the author.
Ron Evans said...

ActiveRecord is already able to retrieve the BLOB data from database just by referring to the currect attribute. Attachment_fu is doing some little bit of magic as far as deciding which record to grab the data from (based on the requested size) but AR 'just works' at grabbing the binary data. Hence the example shows the image_data method using the 'current_data' method of the attachment object, instead to going directly to the db_files data field.

The other part is making your application serve up the correct mime type and return the binary data. You need a separate controller action or else use 'respond_to' to determine what type to return to the browser.

ricky said...

Thx for your response. This time I tried by adding this method in my model,

def image_data(thumbnail = nil)
if thumbnail.nil?
current_data
else
thumbnails.find_by_thumbnail(thumbnail.to_s).current_data
end
end

and in controller I tried to access like,

def show
@photo = Photo.find(params[:id])

respond_to do |format|
format.html { render :action => 'show', :layout => false }
format.png { send_data(@photo.image_data(:thumb),
:type => @photo.content_type,
:filename => @photo.filename,
:disposition => 'inline') }

format.jpg { send_data(@photo.image_data(show_thumbnail),
:type => 'image/jpeg',
:filename => @photo.filename,
:disposition => 'inline') }
format.gif { send_data(@photo.image_data(:thumb),
:type => 'image/gif',
:filename => @photo.filename ,
:disposition => 'inline') }
end
#rescue
# flash[:warning] = 'Could not find image.'
# redirect_to '/'
end


Images are getting stored in the db without any issues. But when I try to view the image like this,

http://localhost/photos/3.gif (or)1.png

I get empty images.

when I access the corresponding 'thumb' image (2.png), I see,

nil.current_data Error.

Please help.