Topic: Beginners: File uploads and rendering images to the database

We'll start by creating a simple application to upload photos to a database. The steps we'll go through are as follows:

- Create a model for the photographs, we'll call this Photo
- Use a database migration to create a table in the database to hold the photographs
- Create a multi-part form and corresponding action for users to upload their photograph files   
- Create an action to pull and render the image back out of the database
- Building the foundations

First things first: let's create the photograph model from the command line:

ruby script/generate model photo

Using migrations, we'll create a simple table containing fields to hold a description of the image, along with a binary field that will hold the binary image data itself. Open the 001_create_photos.rb migration in the db/migrate directory, and edit it as follows: 

 
class CreatePhotos < ActiveRecord::Migration
  def self.up
   create_table :photos do |t|
      t.column :description, :string
      t.column :content_type, :string
      t.column :filename, :string
      t.column :binary_data, :binary
   end

We can then run the migration from the command line:

rake db:migrate

If all has gone well, you should now have a table named photos in your database with all the fields you need to hold your image data! The next step is in the controller. Let's create a photo_admin controller to take care of uploading, editing and deleting photos:

ruby script/generate controller photo_admin

We'll also use Rails' scaffolding generator to create some basic actions and views that we can edit and work with:

ruby script/generate scaffold photo photo_admin

You might be asked by the generator script if it's okay to overwrite some of the existing controller files. Seeing as we haven't edited them even once, you can safely answer yes.

Creating a new photo

Excellent! Now we have the foundations laid, it's time to handle uploading a new photograph to the database. As it happens, Rails' scaffolding generator has already created the new and create actions in the photo_admin controller we just generated, so we don't even need to touch it yet! Instead we'll concentrate on the view for the new action.

To start with, we need to create a multipart form in the new.rhtml view template. This allows the browser to send image files (or any other kind of files) to the server. Let's change the existing form_tag helper to a form_for helper that will wrap our photo model object:

<% form_for(:photo, @photo, :url=>{:action=>'create'}, :html=>{:multipart=>true}) do |form| %>

Notice the :multipart => true option at the end of the tag. It's always good to use the brackets and braces for clarity when using form_for with a multipart option. So many errors will happen if the form isn't rendered correctly using :multipart => true, so it's a time saver to always make sure you get that bit right from the beginning!

Next, let's remove the scaffolded entry for <%= render :partial => 'form' %> and instead add two form fields: one to handle the uploading of the photo, and the other to type in a description of the photo.

<%= form.file_field :image_file %>
<%= form.text_field :description %>

To create a field in the form which allows users to browse for and upload their files, we use Rails' specific [colo=red]file_field[/color] helper. It takes the same options as the text_field, the object assigned to the template (in this case, form) and the method for accessing a specific attribute (in this case image_file) Have you spotted the first anomaly yet? No? Well let's see: we've used :description in the text_field to enter our photo's description, and that's great, because we have a corresponding description field in our database table. But we've used image_file to upload our image data, and there's no corresponding image_file field in our photos table. So what's going on?

It turns out that when uploading a file, you don't get receive just one string, such as the binary image data, but a more complex object containing not just the file's binary data but its filename and content-type (such as image/jpg or image/gif). These are all stored together in :image_file, so what we'll need to do is extract the data and assign it the correct model attributes. The best place to do that is in the photo model, in app/models/photo.rb

class Photo < ActiveRecord::Base 
  def image_file=(input_data)
    self.filename = input_data.original_filename
    self.content_type = input_data.content_type.chomp
    self.binary_data = input_data.read
  end
end

Here, we take the contents of image_file and we use three methods to extract the data and assign it to the model attributes that match our database table: the methods are original_filename, content_type and read.

original_filename gives you surprisingly enough the original filename of the file
content_type provides you with the content-type of the data, such as whether it is an image/gif or an audio/wav which is useful for validation
read lets you actually get at the binary data

The chomp method at the end of content_type simply removes any extraneous newline characters, to make it neater.

Now, if you go to yourapp/photo_admin/new you should be able to upload a file. Try a small one at first (under 10k would be good) You'll know if it works because you'll be returned to the /photo_admin/list action, and you'll see the details of your file, along with a lot of gibberish under the binary column. That's because Rails is rendering your binary image data as text. Instead, we want a way to get the binary image data back out of the database and display it as an actual image...

Rendering an image stored in the database

To do this, we'll create a new action in our photo_admin controller. We'll call it code_image

def code_image 
end

In this action, we'll get the id (primary key) of the image file we want to display, we'll pull it out of the database and then we'll send it to the browser and tell it to render it correctly. Let's expand:

def code_image 
  @image_data = Photo.find(params[:id])
  @image = @image_data.binary_data
  send_data (@image, :type     => @image_data.content_type,
                     :filename => @image_data.filename,
                     :disposition => 'inline')
end

Easy! In the above action we have:

- taken the id of the Photo from the params supplied by the form, and retrieved it from the database into the @image_data object
- extracted the binary data out of the binary_data field
- used Rails' send_data method to render the binary image to the browser.

The send_data method can take several options:

:filename - suggests a filename for the browser to use.
:type - specifies an HTTP content type. Defaults to application/octet-stream.[/coplor] We've used the existing [color=red]content_type information that was stored in the database when we saved the image
:disposition - specifies whether the file will be shown inline or downloaded. Valid values are inline and attachment (default). We want the image displayed in the browser, so we've used inline.
:status - specifies the status code to send with the response. Defaults to 200 OK. We don't really need to worry about this today.

Test it out: go to yourapp/photo_admin/code_image/1 (or whatever the id is of the image you want to display - make sure it is in the database!). If all is well, your image should be rendered in the browser.

Displaying the image inline

Let's change the show.rhtml view for the photo_admin controller, so that instead of displaying a lot of gibberish, we can display the image itself under the "binary" column. If we open the show.rhtml view we should see the following:

<% for column in Photo.content_columns %>
<p>
  <b><%= column.human_name %>:</b> <%=h @photo.send(column.name) %>
</p>
<% end %>

This code simply gets all of the column names from the database and their content and displays them automatically. This is fine for the text-based fields, but not for the binary image field. So we'll use an if condition to display the binary_data column differently:

<% for column in Photo.content_columns %>
<p>
<b><%= column.human_name %>:</b>
<% if column.name == "binary_data" %>
  <%= image_tag("/photo_admin/code_image/#{@photo.id}", :alt => "Image") %>
   <% else %>
    <%=h @photo.send(column.name) %>
<% end %>
</p>
<% end %>

Try it! Go to yourapp/photo_admin/show/1 (or whatever the image id is you want to display) and your image should be displayed. In the above code, we're doing the following:

- looping through each column, and checking to see if the name of the column (column.name) is set to binary_data
- if it is, then we use the image_tag command to render an image. The URL of the image is simply the action that we just created in the photo_admin controller to encode and render the image: photo_admin/code_image/id where id is the id of the image, in this case stored in the @photo.id instance variable
- if the column name isn't binary_data, we just display the standard content of the column as text

So now you have the code_image method in your photo_admin controller, whenever you want to encode and render an image inline, you just call photo_admin/code_image/id as the image URL. Simple!

Troubleshooting Tips

If you're getting NoMethod or other strange errors when uploading a file, try viewing the HTML of the new form after it has been rendered by your browser and before submitting it. Then you can make sure Rails is rendering the form correctly with the multipart entry. If you don't see multipart in the <form> tag, then that's your problem.

http://dizzy.co.uk/portfolio/show - Making business beautiful.

Re: Beginners: File uploads and rendering images to the database

Is it worth using a database for storing files? I have learned that the file system is always a better option.
I may be using this sort of thing very soon, but I am reluctant to use a database when I can use the file system.

Re: Beginners: File uploads and rendering images to the database

There's an interesting post on Sitepoint talking exactly about this issue, you may want to have a read of the arguments for and against:

Binaries Belong in the Database Too

http://dizzy.co.uk/portfolio/show - Making business beautiful.

Re: Beginners: File uploads and rendering images to the database

Hi I found this helpful and i have added it to one of my exciting models with other fields however i sometimes dont have an image to upload and there is an error with this if you dont select an image is there a way to get around this?

Re: Beginners: File uploads and rendering images to the database

You could check to see if the contents of the image_file field are blank. If the contents are blank (and therefore the user has not browsed for a file), just ignore that field in the action. For example:

unless photo[:image_file].blank?
         @photo.update_attributes(params[:photo])
end

http://dizzy.co.uk/portfolio/show - Making business beautiful.

Re: Beginners: File uploads and rendering images to the database

A nice tutorial, Mr. Dizzy.

As an enhancement I'd like to suggest to expand it for the usual resize/recode with RMagick.

Re: Beginners: File uploads and rendering images to the database

Hi the code still does not work because it has a nil object where its in the model

class Photo < ActiveRecord::Base
  def image_file=(input_data)
    self.filename = input_data.original_filename
    self.content_type = input_data.content_type.chomp
    self.binary_data = input_data.read
  end
end

Re: Beginners: File uploads and rendering images to the database

Sorry, that's my mistake. You have to check the incoming params object:

unless params[:photo].blank?
  @photo.update_attributes(params[:photo])
end

That should work.

http://dizzy.co.uk/portfolio/show - Making business beautiful.

Re: Beginners: File uploads and rendering images to the database

Sorry to be a newbie, but I'm getting the following routing error when trying the access any of the photo_admin functions (i.e. myapp/photo_admin/edit);

no route found to match "/images/photo_admin/edit" with {:method=>:get}

I can imagine that this is something silly, so I apologise in advance!

Re: Beginners: File uploads and rendering images to the database

Neil wrote:

Sorry to be a newbie, but I'm getting the following routing error when trying the access any of the photo_admin functions (i.e. myapp/photo_admin/edit);

no route found to match "/images/photo_admin/edit" with {:method=>:get}

I can imagine that this is something silly, so I apologise in advance!

Do you have an edit action in your photo_admin controller?

Re: Beginners: File uploads and rendering images to the database

Yes;

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

Is that causing problems?

Re: Beginners: File uploads and rendering images to the database

No sorry, I just noticed the "images" in your path.  Im not completely sure but i think the path should be more like /photo_admin/edit/7, or /images/edit/7.

The error your getting just means there is no such thing as /images/photo_admin/edit.  If "images" is a controller? Rails wants looks for controller/action.

Maybe you could try moving the photo methods into your images controller.

Sorry i couldn't be more help to you, I'm a bit of a hack myself smile However i did manage to get image uploads working with file_column plugin and i'd be really happy if i could help you out.
Give me more information and ill give it a shot if you still need help.

Re: Beginners: File uploads and rendering images to the database

For some daft reason I called the database 'images' - it was late at night!

My problem was the tutorial advising me to go to http://localhost:3000/myapp/photo_admin/new, when really I just wanted to go to http://localhost:3000/photo_admin/new. I can now see the controller, but unfortunately I'm not getting the option of uploading a photo. Not to worry - I've started working my way through Agile Web Development with Rails, so I'll come back to this tutorial once I'm more clued up, and less tired!

Re: Beginners: File uploads and rendering images to the database

I know what you mean, sometimes you just have to refresh your mind!

I have that book and Rails Recipes, both great books!

Re: Beginners: File uploads and rendering images to the database

I am a newbie to RoR, but doesn't running rake mean having a database at the backend? Whenever I run rake db:migrate I get #42000 error.

Re: Beginners: File uploads and rendering images to the database

Very impressive! I've been using Rails for about a year now(yet I still think I'm a newb). And I've been having quite some trouble trying to find a good way to upload photos. I've tried two plugins at the moment which is File_column and Attachment_fu. So far I've had trouble configuring both! :-( thank goodness for people like you!

Re: Beginners: File uploads and rendering images to the database

Hi and thanks for the tutorial. I'm working through it and have run across a problem - the show method adds ".png" to the id, so there's a routing error when the browser tries to find the image in the database (where the id is a number without a ".png" extension). Searching ".png" in my project seems to show that this comes from rails (see below). I'm running rails 1.2.3 in locomotive 2.0.8 on Mac  OS 10.4.9.
Is there a simple way to fix this without upgrading my rails installation?

Thanks for any suggestions.
Charles
- here's the message which I think explains where the ".png" addition is coming from.
DEPRECATION WARNING: You've called image_path with a source that doesn't include an extension. In Rails 2.0, that will not result in .png automatically being appended. So you should call image_path('/map_admin/code_image/6.png') instead  See http://www.rubyonrails.org/deprecation for details. (called from image_tag at /Applications/Locomotive2/Bundles/rmagickRailsMar2007_i386.locobundle/framework/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_view/helpers/asset_tag_helper.rb:188)

Re: Beginners: File uploads and rendering images to the database

i had the same problem.

make a named route ie.

map.photo 'portfolio/photo/:id/:size/image.jpg' , :controller => 'portfolio' , :action => 'photo'

Use it like:
<%= image_tag photo_url(:id=> photo.id,:size=>640) %>

i think...

Last edited by dylanfm (2007-05-13 07:10:45)

Re: Beginners: File uploads and rendering images to the database

Thanks Dylanfm
  it looks like I'd better start looking learning about routes, I tried simplifying things by just adding a "filename" column and calling the image tag with an url for an image file on the disk, which works fine (I store the images in a folder called "plasmid_maps" in the "public/images" folder of the project. This of course means losing out on the convenience of having everything in the MySQL database, but will do until I have a bit more time on my hands.

It looks like this in the show view, for those interested:

  <b><%= column.human_name %>:</b>
        <% if column.name == "filename" %>
            <% if @plasmid.filename %>
                <%= image_tag( 'plasmid_maps/' + @plasmid.filename,
                :alt => "Image") %>
            <% end %>
           <% else %>
            <%=h @plasmid.send(column.name) %>
        <% end %>

regards, Charles

Re: Beginners: File uploads and rendering images to the database

Hello,

I was wondering if you could offer any insigth into validating the size of the images uploaded.

I keep receiving an error on images that are around 1mb in size:

Mysql::Error: #08S01Got a packet bigger than 'max_allowed_packet' bytes: INSERT INTO photos (`content_type`, `binary_data`, `description`, `filename`) VALUES('image/jpeg',(this is followed by all the binary data for the image)'blah', 'SaltyDogsMarchofDimes 006.jpg')



Any information regarding this error would be very helpful thank you.

Pete

Last edited by coolbox (2007-05-16 19:57:01)