Building an HTML5 Drag & Drop File Uploader Using Sinatra and jQuery: Part 1

Upon setting out to build a Drag & Drop uploader, I first trawled the web for prior art. Blog posts, examples, any information that would lead me down a well-trodden path. As it turns out, such a path doesn’t really exist yet.

FileReader: A Dead End

Any cursory research into HTML5 file handling will undoubtedly turn up mention of the FileReader class which has made an appearance in the most recent versions of Chrome and Firefox. FileReader is useful, in that it allows a browser to read data from files, either all at once, or in chunks, however it quickly becomes unworkable when you try to use it to read large files for uploading to a server. This is due to the fact that you need to read the entire file in one fell swoop to pass off to an XMLHttpRequest, and buffering this much data (this much being 50MB or more) will cause every browser I tested to become completely unresponsive.

XMLHttpRequest Level 2: The Missing Piece

Why haven’t more drag and drop multi-file uploaders shown up on the web before now? Certainly lack of support for gathering file information from drop events was one reason, but another was the fact that until recently, XMLHttpRequest didn’t provide any mechanism for sending file data to the server. Some clever hacks abound, such as using an IFrame or Flash, but none of these would really work for our purposes.

It just so happens that the XMLHttpRequest spec has recently been revised by the W3C: enter XMLHttpRequest Level 2. Here’s the description from the aforelinked working draft:

The XMLHttpRequest Level 2 specification enhances the XMLHttpRequest object with new features, such as cross-origin requests, progress events, and the handling of byte streams for both sending and receiving.

Wow, that sounds perfect! So in supported browsers, an XHR will have an upload attribute, which is an instance of XMLHttpRequestUpload. You can even get progress events from this object, allowing you to build progress tracking into your uploader. This appears to be supported in Firefox 3.5+, Safari 5+, and recent versions of Chrome stable (6.0.472.63 as of writing, but it’s probably supported in earlier versions).

There is one caveat to this approach: there’s currently no good way to perform your XHR file upload as a multipart form post. Experimental support for this has been added in recent builds of Firefox 4, using the FormData interface, however currently the only way to reliably submit a file in a multipart form using XHR in existing browsers is to use FileReader, read the entire thing into memory, then manually build a multipart post. So in other words, it’s a non-starter. Instead, a XMLHttpRequestUpload can be sent with a reference to a file, which the browser will stream from the filesystem as the body of the request. This may require some extra work on the server side, if you want to avoid buffering the entire file inside the process handling the request. For our part, we’ve made some modifications to the venerable nginx upload module, allowing it to also accept PUT requests with raw file contents in the body.

Drag & Drop: Just Enough to Get By

People with far more knowledge and tenacity than I have written eloquent sonnets declaring their undying love for the HTML5 drag & drop API. Unfortunately, it’s the only game in town if you want to do interesting things with files dropped on the browser window. For the remainder of this post, and in subsequent posts, I’ll be building an uploader from the ground up, adding features and complexity as I go. For now, the simplest thing that can possibly work: a drop target on the page, which listens for drop events and, if any files are dropped, creates XMLHttpRequests to send the files to a URL. Things get a lot more dicey when you want to make a complex element (like a table) into a drop target, highlight the drop area, or show information underneath the mouse, so we’ll go for progressive enhancement and add that stuff in later.

Where The Rubber Meets Brass Tacks

This example micro-app will use Sinatra, and have two endpoints, one that displays a page with a drop target, and one that accepts a file upload. Follow the code on github. We’re implementing the front-end code as a jQuery plugin, but most of the concepts can be adapted to Prototype or even plain Javascript.

First, we create a simple page with a drop target div on it. We also include a CSS file and links to both jQuery and our uploader Javascript file. Lastly, we attach an uploader to the drop target:

  <html>
    <head>
      <title>Drag &amp; Drop Tacos</title>
      <link rel="stylesheet" href="/css/master.css" type="text/css" media="screen" title="no title" charset="utf-8">
    </head>

    <body>
      <div id="drop_target">
      </div>
    </body>

    <script type="text/javascript" charset="utf-8" src="/javascripts/jquery-1.4.3.js"></script>
    <script type="text/javascript" charset="utf-8" src="/javascripts/jquery.dnduploader.js"></script>
    <script type="text/javascript" charset="utf-8">
      $("#drop_target").dndUploader({
        url : "/"
      });
    </script>
  </html>

The Ruby code to support this is super simple:

  require 'rubygems'
  require 'sinatra'
  require 'erb'

  get '/' do
    erb :"index.html"
  end

To run the app, make sure you have the Sinatra gem installed (gem install sinatra … depending on your system, you might have to sudo), drop into the example app directory, and type ruby app.rb. WEBrick should start up, and you should be able to access the application at localhost:4567.

Now, let’s take a look at the jQuery plugin. I’ll dispense with the boilerplate plugin code, but if you’d like a refresher, the jQuery documentation will get you up to speed. This is a first pass at the plugin– it won’t really do any uploading yet, but it’ll allow you to drag and drop files onto the drop target and prevent the browser from trying to open them:

(function( $ ){

  var methods = {
    init : function( options ) {

    return this.each(function(){

       var $this = $(this);

       $this.bind('dragenter.dndUploader', methods.dragEnter);
       $this.bind('dragover.dndUploader', methods.dragOver);
       $this.bind('drop.dndUploader', methods.drop);
     });
    },

    dragEnter : function ( event ) {
      event.stopPropagation();
      event.preventDefault();

      return false;
    },

    dragOver : function ( event ) {
      event.stopPropagation();
      event.preventDefault();

      return false;
    },

    drop : function( event ) {
      event.stopPropagation();
      event.preventDefault();

      return false;
    }
  };

  $.fn.dndUploader = function( method ) {
    if ( methods[method] ) {
      return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
    } else if ( typeof method === 'object' || ! method ) {
      return methods.init.apply( this, arguments );
    } else {
      $.error( 'Method ' +  method + ' does not exist on jQuery.dndUploader' );
    }
  };
})( jQuery );

The first order of business is to intercept three drag & drop events: dragenter, dragover, and drop. In order to define your own behavior for drag & drop, the default behavior and event bubbling must be cancelled in your event handler. This bears repeating- if you don’t cancel event propagation, the browser’s default drop handling behavior will take over, and your code won’t work at all. So, we write three identical functions to handle these events:

  dragEnter : function ( event ) {
    event.stopPropagation();
    event.preventDefault();

    return false;
  },

  dragOver : function ( event ) {
    event.stopPropagation();
    event.preventDefault();

    return false;
  },

  drop : function( event ) {
    event.stopPropagation();
    event.preventDefault();

    return false;
  },

Then, in our init function, we bind our event handlers to the appropriate events:

  init : function( options ) {

  return this.each(function() {

     var $this = $(this);

     $this.bind('dragenter', methods.dragEnter);
     $this.bind('dragover', methods.dragOver);
     $this.bind('drop', methods.drop);
   });
  }

So, with all of that, we should now have a drop target primed to accept items dragged onto it.

Getting information out of the MouseEvent

When files are dropped into our target, the event carries with it important information about what items were dropped. This information can be found in the dataTransfer property of the MouseEvent. The following modification will allow us to list the contents:

  drop : function( event ) {
    event.stopPropagation();
    event.preventDefault();

    console.log( event.originalEvent.dataTransfer.files );

    return false;
  }

If you’ve been following along, when you drop a couple of files onto the target, you should see something like this:

  FileList
    0: File
      fileName: "Scan 2.pdf"
      fileSize: 4999673
      name: "Scan 2.pdf"
      size: 4999673
      type: "application/pdf"
      webkitRelativePath: ""
      __proto__: File
    1: File
      fileName: "Scan.jpeg"
      fileSize: 943332
      name: "Scan.jpeg"
      size: 943332
      type: "image/jpeg"
      webkitRelativePath: ""
      __proto__: File
      length: 2
      __proto__: FileList

So, we can easily get the filename, size, and type from the objects in the dataTransfer. Next, we’ll loop through and upload each file. Since this only works with XMLHttpRequest Level 2, we won’t bother with interfaces that abstract away the XMLHttpRequest object- we’ll create one and manipulate it directly. First though, we should make sure to store the url, and any other options, on the uploader node:

  return this.each( function () {

    var $this = $(this);

    $.each(options, function( label, setting ) {
      $this.data(label, setting);
    });

    $this.bind('dragenter.dndUploader', methods.dragEnter);

So, now that a url and method can be passed in, we’ll handle the upload once the files have been dropped:

  drop : function( event ) {
    event.stopPropagation();
    event.preventDefault();

    var $this = $(this);
    var dataTransfer = event.originalEvent.dataTransfer;

    if (dataTransfer.files.length > 0) {
      $.each(dataTransfer.files, function ( i, file ) {
        var xhr    = new XMLHttpRequest();
        var upload = xhr.upload;

        xhr.open($this.data('method') || 'POST', $this.data('url'), true);
        xhr.setRequestHeader('X-Filename', file.fileName);

        xhr.send(file);
      });
    };

    return false;
  }

Next, make sure to edit index.html so that it specifies a PUT:

  $("#drop_target").dndUploader({
    url : "/",
    method : "PUT"
  });

Finally, we’ll add rudimentary handling of the file on the server. For now, this just prints the name of the file and its length. From here, reading the contents or writing to the filesystem isn’t much of a stretch, but for the time being, I’ll leave that as an exercise to the reader.

  get '/' do
    erb :"index.html"
  end

  put '/' do
    puts "uploaded #{env['HTTP_X_FILENAME']} - #{request.body.read.size} bytes"
  end

What’s Next?

There are some nice features we should probably add to this uploader, such as the ability to show/hide an overlay when the mouse hovers over the drop target, an upload progress bar, and feedback when the files have successfully (or unsuccessfully) uploaded. In the next post in this series, I’ll work on adding these features.

18 thoughts on “Building an HTML5 Drag & Drop File Uploader Using Sinatra and jQuery: Part 1

  1. Thank you very much mate! I´m looking for a solution for may startup that could easy the data transfer from customers to us … so i had this idea of draging and droping stuff … then i see your tutorial at hacker news … amazing ! Now i can implement this solutions to my clients, even if in a beta form … waiting for the second part … keep up the good job !

  2. Thanks Ron, I'm glad you like how we've implemented this in Onehub Workspaces. We discussed drag and drop both onto folders and between folders, and have decided against it for now. Matt, our lead designer, is preparing a post that contains some insight into our thought process when designing this feature– hopefully that will shed some light on the considerations involved.

    As for IE, current versions don't have the machinery necessary to support this feature, but we aim for full support of standards compliant browsers. If a future version of IE ships that allows us to support drag and drop file upload, we'll be glad to.

  3. Awesome Janez, thanks for the heads up! Going this route limits you to supporting a more limited number of browsers of course, but for people who aren't worried about that, it could be much easier to handle on the server side.

  4. Please let us know once this becomes something simple for the 'rest of us' type webmasters. I literally searched for about 5 hours today trying to find anyone that had something complete, ready to go out of the box, and dead simple.

  5. Hi Bob,

    Sorry for the delay. I'm hard at work on the follow up… hopefully it'll be worth the wait!

  6. Hi Klancy,

    Yes, right now this is a solution more aimed at programmers who have some experience with both client and server-side development. If you'd like a service that's dead simple to use, which includes drag and drop uploads, check out Onehub Workspaces! http://onehub.com/try

  7. I am using XMLHttpRequest2 to upload files too. When I begin to optimize the server parts, I figured out that using Nginx's upload module is a good choice. But I ran in to a problem that this upload module won't work if i am using ajax to post raw file contents….

    Can you explian how to "modify nginx upload module, allowing it to also accept PUT requests with raw file contents in the body" ?

  8. Hello, you've done a great jobs ! Is your modified nginx_upload_module available somewhere ?

Comments are closed.