Rails Illustrated

Rails, Web Design and the User Experience

Screencast : How to Create a File Upload Progress Bar in Rails, Passenger, Prototype and Low Pro


How to Create a File Upload Progress Bar in Rails, Passenger, Prototype and Low Pro from Erik Andrejko on Vimeo.

An upload progress bar is one of the best ways to improve the usability of file uploads in your application. This screencast will show how to create a file upload progress bar using Rails, Passenger, Low Pro and the upload progress bar apache module.

Screen Cast

Required

Optional

1. Install Apache Module

A module must be installed for Apache to respond to requests for the upload progress. The instructions for installing the Apache module can be found at Drogomir's blog.

Installing under Intel and Mac OS X

The default compile of the apache module will not work under Leopard. Apache will generate this error on startup:

httpd: Syntax error on line 489 of /private/etc/apache2/httpd.conf: Cannot load /usr/libexec/apache2/mod_upload_progress.so into server: dlopen(/usr/libexec/apache2/mod_upload_progress.so, 10): no suitable image found.  
Did find: /usr/libexec/apache2/mod_upload_progress.so: mach-o, but wrong architecture 

Use this command to install under Mac OS X

git clone git://github.com/drogus/apache-upload-progress-module.git
cd apache-upload-progress-module
sudo apxs -c -Wc,-arch -Wc,x86_64 -Wl,-arch -Wl,x86_64 -i -a mod_upload_progress.c 

2. Configure Apache/Rails

In the httpd.conf located at /private/etc/apache2/httpd.conf:

LoadModule upload_progress_module libexec/apache2/mod_upload_progress.so

In the vhost config:

<VirtualHost *:80>
  ServerName file-upload-progress.local
  DocumentRoot "/Users/andrejko/Documents/Projects/web/ri/posts/code/file_upload_progress/public"
  RailsEnv development
  RailsAllowModRewrite off

  <directory "/Users/andrejko/Documents/Projects/web/ri/posts/code/file_upload_progress/public">
    Order allow,deny
    Allow from all
  </directory>


    # needed for tracking upload progess
    <Location />
        # enable tracking uploads in /
        TrackUploads On
    </Location>

    <Location /progress>
        # enable upload progress reports in /progress
        ReportUploads On
    </Location>
</VirtualHost>

Important

Make sure to configure the environment.rb file so that Paperclip will work under Passenger.

# location of the Image Magick command files
Paperclip.options[:command_path] = "/usr/local/bin"

3. Model

The model image.rb has the standard Paperclip options

class Image < ActiveRecord::Base
  has_attached_file :photo, 
                    :styles => { :medium => "300x300>",
                                 :thumb => "100x100#" }

  validates_attachment_presence :photo
end

4. Controller

The controller is a standard restful controller:

class ImagesController < ApplicationController

  def index
    @images = Image.find(:all)
    # generate a unique id for the upload
    @uuid = (0..29).to_a.map {|x| rand(10)}
  end

  def create
    @image = Image.new(params[:image])
    respond_to do |wants|
      if @image.save
        flash[:notice] = 'Image was successfully created.'
        wants.html { redirect_to(:action => 'index') }
      else
        wants.html { redirect_to(:action => 'index') }
      end
    end
  end

end

5. View

<% content_for :scripts do %>
    <%= javascript_include_tag 'upload' %>
<% end %>

<div class='images'>
<%= render :partial => 'image', :collection => @images %>
</div>

<div id='progress' style='display: none;'>
    File upload in progress
    <div id='bar' style='width: 0%;'>
        0%
    </div>
</div>

<% form_for :image, :url => "/images?X-Progress-ID=#{@uuid}", :html => { :multipart => true } do |form| %>
    <%= hidden_field_tag 'X-Progress-ID', @uuid %>
    <%= form.label :photo %>
  <%= form.file_field :photo %>

    <%= form.submit 'upload image', :class => 'submit' %>
<% end %>

6. Unobstrusive Javascript

The javascript is all contained in the javascsripts/upload.js file

// if this is the iframe
// reload the parent
Event.observe(window, 'load',
  function() {
    try
    {
    if (self.parent.frames.length != 0)
    self.parent.location=document.location;
    }
    catch (Exception) {}
  }
);

Event.addBehavior({
  "input.submit:click" : function () {
    $('progress').show();

    //add iframe and set form target to this iframe
    $$("body").first().insert({bottom: "<iframe name='progressFrame' style='display:none; width:0; height:0; position: absolute; top:30000px;'></iframe>"});    
    $(this).up('form').writeAttribute("target", "progressFrame");

    $(this).up('form').submit();

    //update the progress bar
    var uuid = $('X-Progress-ID').value;
    new PeriodicalExecuter(
      function(){
        if(Ajax.activeRequestCount == 0){
          new Ajax.Request("/progress",{
            method: 'get',
            parameters: 'X-Progress-ID=' + uuid,
            onSuccess: function(xhr){
              var upload = xhr.responseText.evalJSON();
              if(upload.state == 'uploading'){
                upload.percent = Math.floor((upload.received / upload.size) * 100);
                $('bar').setStyle({width: upload.percent + "%"});
                $('bar').update(upload.percent + "%");
              }
            }
          })
        }
      },2);

    return false; 
  }
})

Optional Enhancements

Here are few additional possible enhancements that were not shown in the screencast.

  • Show time to completion of upload.
  • Remove progress bar div from the view and insert dynamically with javascript.
  • Allow multiple simultaneous uploads.

Update

The javascript has been modified to work with Internet Explorer 7.

Credits

Intro music thanks to Courtney Williams via Podcast NYC.

Comments  

1

Nice solution. Really clean and simple, I like it!

Roy van der Meij wrote on January 9 2009
2

I am curious as to how you slowed down the bandwidth on your local machine to simulate the progress?

Cool solution!

Jeff wrote on January 10 2009
3

Jeff - To slow down your bandwidth use these instructions here: How to Simulate a Web Experience in Development. Basically you use ipfw to set a bandwidth limit to port 80.

Erik wrote on January 11 2009
4

Wow, really cool! I just did a little hack job of a write up for multiple file uploads similar to Gmail's attachment feature.

Check it out here: <http://prowestech.com/posts/4>

When I get time, maybe I'll apply your methodology to my tutorial and see where it goes.

Daniel wrote on January 14 2009
5

Great screencast, thanx for that. I implemented it into a project and it works very smooth, even with multiple files.

In IE7 (and IE6 too, but I'm not taking that beast into account) a new window opens when I start uploading. I guess IE doesn't get the concept op the dynamically created iframe?

Any suggestions on how to fix that?

wout wrote on January 26 2009
6

wout - It seems like the iframe was not correctly inserted in IE. To get it to work in IE insert the iframe with:

$$(&quot;body&quot;).first().insert({bottom: &quot;&lt;iframe name=&#39;progressFrame&#39; style=&#39;display:none; width:0; height:0; position: absolute; top:30000px;&#39;&gt;&lt;/iframe&gt;&quot;});
Erik wrote on January 27 2009
7

Wow, thanx. Escaping it does the job indeed.

In the meanwhile I noticed something else though. If a visitor stops in the middle of an upload process, the apache progress module stops working. In some way this keeps paperclip from working too. The only way to get it working again is restarting apache.

At first I thought it was a local problem, but when I deployed the app on my mediatemple DV, the same thing happened. Later I tried it on the osx server of a client but without success. I'll notify drogus about it.

wout wrote on January 31 2009
8

wout - That sounds a little odd. Are you submitting a single form with multiple file fields? You will want to have a separated submit with a separate and distinct uid for each submit. Another thing to consider to try to debug the situation if you haven't already tried this is to use the Firefox Firebug extension to watch the requests and responses from the apache module.

erik wrote on February 4 2009
9

for an odd reason i can't get the script to move on to the show action after the upload. It uploads, then the bar reaches 100% then it gets stuck there. The requests keeps on going with the state done. The show action is displayed in the iframe. I must be missing something huge. I hacked my way around it in the upload.js doing : <pre><code>if(upload.state == 'uploading'){ upload.percent = Math.floor((upload.received / upload.size) * 100); $('bar').setStyle({width: upload.percent + "%"}); $('bar').update(upload.percent + "%"); } else if (upload.state == 'done'){ redirect somewhere } </code></pre> but i'm sure there's a cleaner way to do this. plz keep in mind i'm quite noob with this rails thing. maybe i'm just asking too much to the rails magic ! :)

raphael_999 wrote on February 17 2009
10

hi,

The "/progress" url is showing me a 404 error. Can anybody help me on that.

Amit Yadav wrote on February 17 2009
11

Anyone get this working with nginx??

Roger S wrote on February 24 2009
12

Roger S: Are you using this module: http://wiki.codemongers.com/NginxHttpUploadProgressModule. Is the JSON returned by the /progress URL in the same format? It might be slightly different. If it is, you might need to modify the Javascript that calculates the progress bar size.

Amit: Are you sure that Apache is configured correctly? You might want to double check the vhost config file and make sure that the Apache Module is activated in the httpd.conf file.

raphael_999: The Javascript should be loaded by the show action inside the iframe. The Javascript detects if the content of the iframe has been reloaded and then forces the parent to reload. This should cause the whole page to reload when the upload is complete,

erik wrote on February 25 2009
13

Hmm seems like this won't work with a standard new/edit form, since it's forcing a hard redirect to the submit action URL in the onLoad. Any ideas for making it play nice with edit forms and display errors inline?

In other words, instead of redisplaying the form data at /images/new (if there was a full form for data entry) it would redirect to /images and display the listing.

nap wrote on February 27 2009
14

Hello webmaster I would like to share with you a link to your site write me here preonrelt@mail.ru

Alexwebmaster wrote on March 3 2009
15

I'm receiving the "no suitable image" error even after following your instructions on compiling the apache upload progress module for my mac os x intel arch.

http://gist.github.com/80611

After closely examining your screencast, the only difference I see in my results and yours is this line: <pre> warning: no debug symbols in executable (-arch x86_64)</pre> but its only a warning so I don't see how that could the culprit.

I'm using darwin ports apache: apache2 @2.2.11_0+darwin_9 (active)

Any suggestions?

Jonathan wrote on March 17 2009
16

Did my previous comment get eaten?

I followe your installation steps on mac os x intel arch and am still having problems.

http://gist.github.com/80611

Any suggestions?

Jonathan wrote on March 17 2009
17

Jonathan - it seems from your gist that it is still compiling a mach architecture version of mod_upload_progress.so. What version of gcc do you have installed? On my machine I have the version that was installed with XCode. Also are you using Tiger or Leopard?

Erik wrote on March 18 2009
18

my gcc version is: i686-apple-darwin9-gcc-4.0.1 (GCC) 4.0.1 (Apple Inc. build 5490)

and i'm on leopard: ProductName: Mac OS X ProductVersion: 10.5.6 BuildVersion: 9G55

Jonathan wrote on March 19 2009
19

Hi! Great and easy-to-follow video.

I installed FireBug and when I load the page I get the following error: <b>Event.addBehavior is not a function</b>

Of course the whole system does not work at all, it does not even display the hidden div when submit is clicked.

Is there any extra javascript that needs to be included? Other suggestions to try and fix this?

Thank you.

Jose wrote on March 26 2009
20

I reply to myself.

"lowpro.js" must be included as well.

Sorry for the post.

Jose wrote on March 26 2009
21

Over at Drogomir's blog in the comments, I found a apxs command that worked for compiling the apache upload progress module.

<code> sudo apxs -c -i -Wc,-arch -Wc,ppc7400 -Wl,-arch -Wl,ppc7400 -Wc,-arch -Wc,ppc64 -Wl,-arch -Wl,ppc64 -Wc,-arch -Wc,x86_64 -Wl,-arch -Wl,x86_64 -Wc,-arch -Wc,i386 -Wl,-arch -Wl,i386 mod_upload_progress.c </code>

Thanks for the tutorial!

Jonathan wrote on April 1 2009
22

Hmm.... I'm having all sorts of bother with this. Something is borked with my setup. Upload works fine (ie 100% uploaded immediately) without throttling, but no progress is shown {as expected}.

If I throttle using your instructions (ipfw), then upload takes forever {also expected?}. I confirm that I have had success twice {with firefox}, but now the progress bar updates in one jump after 15+ seconds. Same behavior with firefox 3.0.8 + Safari

Running OS 10.5.6/Apache2.2.9 with: Passenger 2.1.3. Safari 3.2.1, latest version of 'upload progress', Passenger Pref pane 1.2

I've tried Rails 2.1.0, and 2.1.2, no difference... I've upgraded to Prototype 1.6.0.3 (from 1.6.0.1) no difference

Qs: - What version of Safari are you using? - What version of Passenger are you using? - Does the <directory> directive inside the VirtualHost require a Capital 'D' ? - Have you got it to work with Passenger.pref pane?

Maybe important: ruby+gem+rails installed using MacPorts (ie in /opt/local/....)

Suggestions, anyone?

Thank you.

Grant wrote on April 15 2009
23

Good work Eric!!!

Two Things:

1) Is there a method to see if the upload_module is properly installed / configuration is correct.

2) I am not sure if I connected the javascript libraries correctly... connected prototype and lowpro in the right way. I downloaded lowpro.js and prototype.js, and included both of them in the layout declaring prototype first.

best, Leumas

Leumas wrote on July 15 2009
24

Forget to ask a question regarding my last post.

The question for number 2) is is there a method to check if they javascript files are installed correctly?

Leuams

Leumas wrote on July 15 2009
25

This is what I have

http://pastie.org/private/x4yfvdc9fpksi4lwgtylha

It seems to be the same problem than Jonathan

Someone can help?

iuser59 wrote on July 30 2009
26

This is what I have when I include lowpro.js

http://pastie.org/565370

iuser59 wrote on July 30 2009
27

Leumas,

Lowpro requires prototype. Try including that. I don't see it in your pastie.

Jonathan wrote on August 3 2009
28

Thanks, for the help. Been waiting a while and I will see if those tips work.

Best, Leumas

Leumas wrote on August 8 2009
29

Does this video really only have 10 keyframes?

EH wrote on August 18 2009
30

Anyone using Amazon S3 who is interested in uploading multiple files (with progress bars, simultaneously) directly to S3 without hitting the server might be interested in my posts here:

http://www.railstoolkit.com/posts/fancyupload-amazon-s3-uploader-with-paperclip http://www.railstoolkit.com/posts/uploading-files-directly-to-amazon-s3-using-fancyupload

Nico wrote on August 28 2009

Add Comment

(required)
(required, won't be displayed)

(Use Markdown syntax)