AJAX file uploads in Rails using attachment_fu and responds_
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 pluginSure,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:
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:
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_fuThe installation process is quite long for the image processors,so I’ve just linked to them here:
Step 4. Add uploading to your codeI’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 ( 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 The options available are:
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:
To enable multipart file uploads,we need to set <%= 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 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 itLet’s try and AJAX our file uploads. The current user flow is:
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 <% remote_form_for(:asset,:html => { :multipart => true }) do |f| %> Add 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 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 page.remove "asset_#{@asset.id}" In the controller you also need to add Keep our form views DRY (optional) We should also make the file upload form contents into a partial and use it in _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 <% 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_parentTo 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 <% 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 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 NOW you should be able to get your index page and upload a file the AJAX way! Step 7. Make it production readyThere are some more changes you need to make it production ready:
Step 8. Bonus: making a file download by clicking on a linkJust add the following action to your assets controller; don’t forget to add the route to your 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). (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |