Previously I experimented with drag and drop file upload with Google Gears. Recently FireFox 3.6 (codenamed Namoroka) was the first to implement File API. It enables JavaScript to interact with local files.

Correction: Ionut G. Stan pointed out that File API was actually available already in FireFox 3.0. What Namoroka introduced is the drag and drop interface for the files. Sorry for the confusion.

Update: Apparently something has changed with FireFox 3.6b3 and has broken the code. Demo works with older alpha versions. Will debug and update the demo soon.

Here is how you can implement drag and drop multiple file upload with native JavaScript. No plugins needed. Just plain old new HTML5. Again there is a working demo. You will need FireFox 3.6 to test it. Full source code can be at GitHub.

PHP Upload Script

PHP script for receiving the files is the same as last time. It accesses uploaded files using $_FILES superglobal. For this demo we assume users upload only image files.

<?php

$error_message[0] = "Unknown problem with upload.";
$error_message[1] = "Uploaded file too large (load_max_filesize).";
$error_message[2] = "Uploaded file too large (MAX_FILE_SIZE).";
$error_message[3] = "File was only partially uploaded.";
$error_message[4] = "Choose a file to upload.";

$upload_dir  = './tmp/';
$num_files = count($_FILES['user_file']['name']);

for ($i=0; $i < $num_files; $i++) {
    $upload_file = $upload_dir . basename($_FILES['user_file']['name'][$i]);

    if (!preg_match("/(gif|jpg|jpeg|png)$/",$_FILES['user_file']['name'][$i])) {
        print "I asked for an image...";
    } else {
        if (is_uploaded_file($_FILES['user_file']['tmp_name'][$i])) {
            if (move_uploaded_file($_FILES['user_file']['tmp_name'][$i], 
                $upload_file)) {
                /* Great success... */
            } else {
                print $error_message[$_FILES['user_file']['error'][$i]];
            }
        } else {
            print $error_message[$_FILES['user_file']['error'][$i]];
        }    
    }
}
?>

Setup Drop Target

Also JavaScript code is almost identical with the Google Gears version. Since only FireFox is supported we can leave Mozilla and IE initialization out. We only need to attach drop event to div with id #dropzone. Event will trigger function called upload().

$(function() {

    /* Cannot use $.bind() since jQuery does not normalize native events. */
    $('#dropzone')
        .get(0)
        .addEventListener('drop', upload, false);

    function upload(event) { 
        /* Uploading will here. */
    }

}

Upload Files With Native JavaScript

JavaScript is used to build RFC2388 string. This string is POST:ed using XMLHttpRequest to our PHP script. PHP will see the request as if it was POST:ed from normal multipart/form-data form.

There are three differences to Gears version:

  • RFC2388 string is built using JavaScript string variable instead of Gears BlobBuilder API.
  • Data is POST:ed using native XMLHttpRequest instead of Gears HttpRequest API.
  • Information on dropped files are accessed using event.dataTransfer variable instead of desktop.getDragData() function call.
function upload(event) {

    var data = event.dataTransfer;

    var boundary = '------multipartformboundary' + (new Date).getTime();
    var dashdash = '--';
    var crlf     = '\r\n';

    /* Build RFC2388 string. */
    var builder = '';

    builder += dashdash;
    builder += boundary;
    builder += crlf;

    var xhr = new XMLHttpRequest();

    /* For each dropped file. */
    for (var i = 0; i < data.files.length; i++) {
        var file = data.files[i];

        /* Generate headers. */            
        builder += 'Content-Disposition: form-data; name="user_file[]"';
        if (file.fileName) {
          builder += '; filename="' + file.fileName + '"';
        }
        builder += crlf;

        builder += 'Content-Type: application/octet-stream';
        builder += crlf;
        builder += crlf; 

        /* Append binary data. */
        builder += file.getAsBinary();
        builder += crlf;

        /* Write boundary. */
        builder += dashdash;
        builder += boundary;
        builder += crlf;
    }

    /* Mark end of the request. */
    builder += dashdash;
    builder += boundary;
    builder += dashdash;
    builder += crlf;

    xhr.open("POST", "upload.php", true);
    xhr.setRequestHeader('content-type', 'multipart/form-data; boundary=' 
        + boundary);
    xhr.sendAsBinary(builder);        

    xhr.onload = function(event) { 
        /* If we got an error display it. */
        if (xhr.responseText) {
            alert(xhr.responseText);
        }
        $("#dropzone").load("list.php?random=" +  (new Date).getTime());
    };

    /* Prevent FireFox opening the dragged file. */
    event.stopPropagation();

}

And there you have it. Native drag and drop multiple file upload.

More Reading

Ryan from CSS Ninja has similar article but with progress meter and thumbnail image. PPK got frustrated with HTML5 drag and drop. Leslie Michael has nice article about HTML5 drag and drop. Last but not least Ionut G. Stan helped me understand how to build RFC2388 strings.

Oh and almost forgot. If you need FireFox 3.6 check the nightlies folder.


15 Responses to “HTML5 Drag and Drop Multiple File Upload”

  1. Ryan says:

    Nice one, I never really delved into the handling of the binary data on the server side will be useful as reference. Thanks for posting!

  2. Mika Tuupola says:

    Ryan: Thanks! Your article helped by giving a quick introduction to File API. I did not even know it was already implemented.

  3. Ionut G. Stan says:

    Hi Mika,

    Thanks for mentioning my article and congratulations for succeeding to be more succinct than I were.

    I have one small correction tough, the File API exposed by Namoroka seems to be the same as the File API in Firefox 3.0. So the real new stuff will be the drag and drop capability. Also, the File API doesn’t seem to be completely implemented. For example the slice method, which I hope will allow us to send large files without freezing the browser.

  4. Mika Tuupola says:

    Ionut G. Stan: I stand corrected. Everything I read I understood that File API was new to Namoroka. Now when I Google some more I can too find info that it was available already in 3.0. Will update the article.

    Thanks for the heads up!

  5. Marius says:

    I couldn’t find your contact details on your website so I’m gonna contact you via this comment form.

    I made some changes on your filestyle jquery plugin and I wanna send you an updated version of it. If you can send me an email or some concact details so I can explain more would be great.

    Thanx, Marius

  6. Mika Tuupola says:

    Email at tuupola at appelsiini.net

  7. Tom says:

    For servers that are more picky about the form structure (I was using WEBrick), you might need to add a check around the last part of the for loop so you don’t get two boundary lines in a row at the end of the data.

    if (i != files.length -1){
    /* Write boundary. */
      builder += dashdash;
      builder += boundary;
      builder += crlf;
    }
    

    Thanks Mika, this helped a ton!

  8. Mika Tuupola says:

    Tom: Good catch. Thanks!

  9. qubodup says:

    Hello,

    The demo won’t work. FF will just open the photo in the current window. :(

    Best regards

  10. Mika Tuupola says:

    qubodup: Which version of FF? As the article says you need at least FF 3.6 for the demo to work.

  11. Jonny says:

    Good post, although I am very disappointed with the standards work on XHR file upload. Seriously, sending a single string as the file data is ridiculously over-simplified. The XHR file upload API is completely useless for my application, which almost always deals with files in the gigabytes.

    Why is it so hard for these guys to propose a simple XHR::writeBytes method?

  12. Mika Tuupola says:

    Jonny: You can also send data in chunks. Although then the receiving script has to combine the chunks into file(s) manually.

  13. pod°boq says:

    I’m having the same problem as @qubodup with Firefox 3.6b3.

  14. z.Yleo77 says:

    oh? it got an error that doesn’t work??

  15. Mika Tuupola says:

    Apparently something has changed with 3.6b3 and has broken the code. Will debug and update the demo soon.

Leave a Reply



(will not be published)



(you can use Textile for formatting)