In this walkthrough,I go through the available options and an example using attachment_fu to handle file uploads and image thumbnailing,and responds_to_parent to implement the iframe remoting pattern to work around javascript’s security restrictions on file system access.
You can also download the complete example.
This is an outdated article. I will be updating with a new article soon.
Step 1. Choose a file upload plugin
Sure,you can write one yourself (or bake the code directly into your app),but unless you have specific requirements you should take a look at what’s available. Even if you do have a good excuse,you can learn from the existing plugins or extend them. The three that I’ve used over the past two years are:
- file_column – the first file upload plugin available for Rails that I know of,it handles saving files to the filesystem,resizing of images,creation of thumbnails,and integration with rmagick; however it doesn’t seem to be in active development.
- acts_as_attachment – written by Rick Olson,it does everything that file_column can,but with a cleaner and extensible code base.
- attachment_fu – is a rewrite of acts_as_attachment adding a plugin architecture to extend it to add different image processors (image_science,mini_magick and rmagick are provided) and storage backends (database,file system,and Amazon S3). The only problem is you need Rails 1.2+
Recommendation: attachment_fu if you are using Rails 1.2+,otherwise acts_as_attachment.
Step 2. Determine which Image Processor you want to use.
attachment_fu supports three processors out of the Box:
- image_science – a light ruby wrapper around the FreeImage library,it can only be used to resize images. It used to have problems with image quality of thumbnails and PNG color profiles but these have recently been fixed.
- RMagick – a ruby wrapper around the ImageMagick/GraphicsMagick libraries,it provides a lot of advanced image processing features. It’s memory hungry though,and can max resource limits on some shared hosts causing your app to fail; it’s happened to me a few times on large images.
- minimagick – another wrapper around ImageMagick,however it resizes images using imagemagick’s mogrify command. If you are hitting resource limits on your host,minimagick is preferred over rmagick.
Recommendation: image_science if you only need image resizing and can handle the slightly inferior thumbnail quality,minimagick otherwise.
Step 3. Install image processors and attachment_fu
The installation process is quite long for the image processors,so I’ve just linked to them here:
- RMagick/ImageMagick: Mac OS X, Linux,and Windows
- FreeImage/image_science: Mac OS X,Linux
- To install minimagick:
sudo gem install mini_magick
- To install attachment_fu:
script/plugin install http://svn.techno-weenie.net/projects/plugins/attachment_fu/
Step 4. Add uploading to your code
I’ll use a restful model for our file uploads since it’s all the rage (here’s a good introduction). You can create a restful scaffold using the following command:
ruby script/generate scaffold_resource asset filename:string content_type:string size:integer width:integer height:integer parent_id:integer thumbnail:string created_at:datetime
This will create the controllers,models,views and a migration. I’ve included support for saving image properties (width
and height
attributes) and thumbnailing (parent_id
and thumbnail
attributes).
Here is the resulting migration if you want to do it manually:
class CreateAssets < ActiveRecord::Migration def self.up create_table :assets do |t| t.column :filename,:string t.column :content_type,:string t.column :size,:integer t.column :width,:integer t.column :height,:integer t.column :parent_id,:integer t.column :thumbnail,:string t.column :created_at,:datetime end end def self.down drop_table :assets end end
In the model,it’s really a one liner to add file upload features.
class Asset < ActiveRecord::Base has_attachment :storage => :file_system,:max_size => 1.megabytes,:thumbnails => { :thumb => '80x80>',:tiny => '40x40>' },:processor => :MiniMagick # attachment_fu looks in this order: ImageScience,Rmagick,MiniMagick validates_as_attachment # ok two lines if you want to do validation,and why wouldn't you? end
The has_attachment
(or acts_as_attachment
method for those not using attachment_fu) adds a lot of useful methods such as image?
to determine if the file is an image,and public_filename(thumbnail=nil)
to retrieve the filename for the original or thumbnail. I usually add methods to determine other file types such as movies,music,and documents.
The options available are:
content_type
– Allowed content types. Allows all by default. Use:image
to allow all standard image types.min_size
– Minimum size allowed. 1 byte is the default.max_size
– Maximum size allowed. 1.megabyte is the default.size
– Range of sizes allowed. (1..1.megabyte
) is the default. This overrides the:min_size
and:max_size
options.resize_to
– Used by RMagick to resize images. Pass either an array of width/height,or a geometry string.thumbnails
– Specifies a set of thumbnails to generate. This accepts a hash of filename suffixes and RMagick resizing options.thumbnail_class
– Set what class to use for thumbnails. This attachment class is used by default.path_prefix
– path to store the uploaded files. Usespublic/#{table_name}
by default for the filesystem,and just#{table_name}
for the S3 backend. Setting this sets the:storage
to:file_system
.storage
– Use:file_system
to specify the attachment data is stored with the file system. Defaults to:db_system
.
In the above we’re storing the files in the file system and are adding two thumbnails if it’s an image: one called ‘thumb’ no bigger than 80×80 pixels,and the other called ‘tiny’. By default,these will be stored in the same directory as the original: /public/assets/nnnn/mmmm/ with their thumbnail name as a suffix. To show them in the view,we just do the following: <%= image_tag(image.public_filename(:thumb)) %>
validates_as_attachment
ensures that size
,content_type
and filename
are present and checks against the options given to has_attachment
; in our case the original should be no larger than 1 megabyte.
To enable multipart file uploads,we need to set multipart => true
as a form option in new.rhtml
. The uploaded_data
file input field is used by attachment_fu to store the file contents in an attribute so that attachment_fu can do its magic when the uploaded_data=
method is called.
<%= error_messages_for :asset %> <% form_for(:asset,:url => assets_path,:html => { :multipart => true }) do |form| %> <p> <label for="uploaded_data">Upload a file:</label> <%= form.file_field :uploaded_data %> </p> <p> <%= submit_tag "Create" %> </p> <% end %>
We’ll also pretty up the index code. We want to show a thumbnail if the file is an image,otherwise just the name:
<h1>Listing assets</h1> <ul id="assets"> <% @assets.each do |asset| %> <li id="asset_<%= asset.id %>"> <% if asset.image? %> <%= link_to(image_tag(asset.public_filename(:thumb))) %><br /> <% end %> <%= link_to(asset.filename,asset_path(asset)) %> (<%= link_to "Delete",asset_path(asset),:method => :delete,:confirm => "are you sure?"%>) </li> <% end %> </ul><br /><%= link_to 'New asset',new_asset_path %>
Don’t forget to do a rake db:migrate
to add the assets table. At this stage you can start your server and go to http://localhost:3000/assets/new to add a new file. After being redirected back to the index page you’ll notice that thumbnails are showing in our index with the originals. To get rid of this,we can modify assets_controller
to only display originals by checking if the parent_id
attribute is nil
. attachment_fu also allows you to store thumbnails into a different model,which would make this step unnecessary.
def index @assets = Asset.find(:all,:conditions => {:parent_id => nil},:order => 'created_at DESC') respond_to do |format| format.html # index.rhtml format.xml { render :xml => @assets.to_xml } end end
Step 5. AJAX it
Let’s try and AJAX our file uploads. The current user flow is:
- go to index page
- click on “new file” link
- choose a file and submit the form
- get redirected to index.
What we want to happen is to have all that occur on the index page,with no page refreshes. Normally you would do the following:
Add the Javascript prototype/scriptaculous libraries into your layout.
<%= javascript_include_tag :defaults %>
Change the form_for
tag to a remote_form_for
<% remote_form_for(:asset,:html => { :multipart => true }) do |f| %>
Add format.js
to the create
action in the controller to handle AJAX requests:
def create @asset = Asset.new(params[:asset]) respond_to do |format| if @asset.save flash[:notice] = 'Asset was successfully created.' format.html { redirect_to asset_url(@asset) } format.xml { head :created,:location => asset_url(@asset) } format.js else format.html { render :action => "new" } format.xml { render :xml => @asset.errors.to_xml } format.js end end end
Make a create.rjs
file to insert the asset at the bottom of your list:
page.insert_html :bottom,"assets",:partial => 'assets/list_item',:object => @asset page.visual_effect :highlight,"asset_#{@asset.id}"
Create a partial to show the image in the list
<li id="asset_<%= list_item.id %>"> <% if list_item.image? %> <%= link_to(image_tag(list_item.public_filename(:thumb))) %><br /> <% end %> <%= link_to(list_item.filename,asset_path(list_item))%> (<%= link_to_remote("Delete",{:url => asset_path(list_item),:confirm => "are you sure?"}) %>) </li>
Add AJAX deletion (optional)
If you’ve noticed the changes in the prevIoUs code,I’ve added AJAX deletion of files as well. To enable this on the server we add a destroy.rjs
file to remove the deleted file form the list.
page.remove "asset_#{@asset.id}"
In the controller you also need to add format.js
to the delete
action.
Keep our form views DRY (optional)
We should also make the file upload form contents into a partial and use it in new.rhtml
as well as index.rhtml
.
_form.rhtml
<p> <label for="uploaded_data">Upload a file:</label> <%= form.file_field :uploaded_data %> </p> <p> <%= submit_tag "Create" %> </p>
new.rhtml
<% form_for(:asset,:html => { :multipart => true }) do |form| %> <%= render(:partial => '/assets/form',:object => form)%> <% end %>
Add the form to index.rhtml
<% remote_form_for(:asset,:object => form) %> <% end %>
Now that we have all our code in place,go back to the index page where you should be able to upload a new file using AJAX.
Unfortunately there is one problem. A security restriction with javascript prevents access to the filesystem. If you used validations for your asset model you would have gotten an error complaining about missing attributes. This is because only the filename is sent to the server,not the file itself. How can we solve this issue?
Step 6. Using iframes and responds_to_parent
To get around the AJAX/file upload problem we make use of the iframe remoting pattern. We need a hidden iframe and target our form’s action to that iframe. First,we change the index.rhtml
to use a form_for
tag. To get rails to process our action like an AJAX request we simply add a ”.js” extension to the form’s action. We then set the iframe to a 1×1 sized pixel so it doesn’t get shown. Don’t use display:none
or your iframe will be hidden from your form and depending on your browser you will end up opening a new window,load the response in the main window,or download the server response.
<% form_for(:asset,:url =>formatted_assets_path(:format => 'js'),:html => { :multipart => true,:target => 'upload_frame'}) do |form| %> <%= render(:partial => '/assets/form',:object => form) %> <% end %> <iframe id='upload_frame' name="upload_frame" style="width:1px;height:1px;border:0px" src="about:blank"></iframe>
To handle the form on the server,we can use Sean Treadway’s responds_to_parent plugin.
script/plugin install http://responds-to-parent.googlecode.com/svn/trunk/
This plugin makes it dead simple to send javascript back to the parent window,not the iframe itself. Add the following to your create
action:
def create @asset = Asset.new(params[:asset]) respond_to do |format| if @asset.save flash[:notice] = 'Asset was successfully created.' format.html { redirect_to asset_url(@asset) } format.xml { head :created,:location => asset_url(@asset) } format.js do responds_to_parent do render :update do |page| page.insert_html :bottom,:object => @asset page.visual_effect :highlight,"asset_#{@asset.id}" end end end else format.html { render :action => "new" } format.xml { render :xml => @asset.errors.to_xml } format.js do responds_to_parent do render :update do |page| # update the page with an error message end end end end end end
At this point you no longer need the create.rjs
file.
NOW you should be able to get your index page and upload a file the AJAX way!
Step 7. Make it production ready
There are some more changes you need to make it production ready:
- handling errors,
- displaying error messages when uploading fails,
- showing some Feedback to the user while the file is uploading or being deleted
Step 8. Bonus: making a file download by clicking on a link
Just add the following action to your assets controller; don’t forget to add the route to your routes.rb
file.
def download @asset = Asset.find(params[:id]) send_file("#{RAILS_ROOT}/public"+@asset.public_filename,:disposition => 'attachment',:encoding => 'utf8',:type => @asset.content_type,:filename => URI.encode(@asset.filename)) end
Update: 2007/05/23 Thanks to Geoff Buesing for pointing out that we can use formatted_routes.
Update: 2007/05/26 Updated a bug in the initial index.html example (thanks Benedikt!) and added a download link to the final example (see the first paragraph).