Ajax upload with XMLHttpRequest level 2 and the File API

Posted on November 17, 2010 by Phil   61 comments

I’ve put together a micro Ajax library for the XMLHttpRequest level 2 spec. You can go check it out on Github or read the related article XMLHttpRequest Level 2 Ajax library, xhr2-lib

Ajax applications are wide spread these days and one thing that has proven a pain in the arse is uploading files. I’m sure most people will have fired off a fake asynchronous upload to a hidden iframe or used a sneaky swf to handle it but now with the development of the level 2 specification for the XMLHttpRequest object some of those woes may soon (if you are ok about not supporting IE) be a thing of the past.

The FileReader API and the DataTransfer object

One of the coolest things I’ve messed about with so far in HTML5 is the FileReader and File APIs and their ability to read in files from the users computer. With this also comes the addition of the DataTransfer object which carries a reference to the files in the drag/drop events. What this means is that we can drag files from our desktop and drop them onto our web applications where we can use JavaScript to read in their data or pass them off to be uploaded. The code snippets below are taken from the FileAPI singleton used in the demo to handle the file manipulation and uploading.

this.showDroppedFiles = function (ev) {
    ev.stopPropagation();
    ev.preventDefault();
    var files = ev.dataTransfer.files;
    addFileListItems(files);
}

The above method is fired when the user drops files onto a specified element, we stop any default action for the event and stop it bubbling before accessing the dataTransfer object of the drop event to retrieve the list of files that were dropped before passing them to the addFileListItems method shown below.

var addFileListItems = function (files) {
    for (var i = 0; i < files.length; i++) {
        var fr = new FileReader();
        fr.file = files[i];
        fr.addEventListener("loadend", showFileInList, false);
        fr.readAsDataURL(files[i]);
    }
}

Here we loop over our file list and for each one we initialise a FileReader object and bind the showFileInList method to it’s loadend event which fires once our FileReader object is done reading in the file as a data URL. Note: my testing revealed that addEventListener is not supported for FileReader in Chrome. Currently Safari and IE9 do not support the file API. It is only necessary to do this to show the preview for images in the list (see demo) other files could just be added to the queue directly. In the showFileInList method (shown below) we access the file we have just read, populate a list with it’s details and push it into an Array ready to be uploaded. Also you’ll notice the addition of an upload progress indicator which we’ll look at with the uploading via Ajax.

var showFileInList = function (ev) {
    var file = ev.target.file;
    if (file) {
        var li = document.createElement("li");
        if (file.type.search(/image\/.*/) != -1) {
            var thumb = new Image();
            thumb.src = ev.target.result;
            thumb.addEventListener("mouseover", showImagePreview, false);
            thumb.addEventListener("mouseout", removePreview, false);
            li.appendChild(thumb);
        }
        var h3 = document.createElement("h3");
        var h3Text = document.createTextNode(file.name);
        h3.appendChild(h3Text);
        li.appendChild(h3)
        var p = document.createElement("p");
        var pText = document.createTextNode(
            "File type: ("
            + file.type + ") - " +
            Math.round(file.size / 1024) + "KB"
        );
        p.appendChild(pText);
        li.appendChild(p);
        var divLoader = document.createElement("div");
        divLoader.className = "loadingIndicator";
        li.appendChild(divLoader);
        fileList.appendChild(li);
        fileQueue.push({
            file : file,
            li : li
        });
    }
}

XMLHttpRequest object send() with File

In the level 2 specification the send method can accept a File object argument which allows us to stream binary chunked data to the server asynchronously. So when we have chosen the files we want to upload and have our queue all set up ready to roll we can fire each file off to our upload method to be sent to the server for saving as shown in the two methods below.

this.uploadQueue = function (ev) {
    ev.preventDefault();
    while (fileQueue.length > 0) {
        var item = fileQueue.pop();
        var p = document.createElement("p");
        p.className = "loader";
        var pText = document.createTextNode("Uploading...");
        p.appendChild(pText);
        item.li.appendChild(p);
        if (item.file.size < 1048576) {
            uploadFile(item.file, item.li);
        } else {
            p.textContent = "File to large";
            p.style["color"] = "red";
        }
    }
}

In this method we loop through our file list and send each one in turn, I only have the file size check at 1MB for the demo – can be higher. I set up some loading text here to let the user know what’s going on, we also have a progress bar which was set up earlier that we will update as the file uploads (explained below).

var uploadFile = function (file, li) {
    if (li && file) {
        var xhr = new XMLHttpRequest(),
            upload = xhr.upload;
        upload.addEventListener("progress", function (ev) {
            if (ev.lengthComputable) {
                var loader = li.getElementsByTagName("div")[0];
                loader.style["width"] = (ev.loaded / ev.total) * 100 + "%";
            }
        }, false);
        upload.addEventListener("load", function (ev) {
            var ps = li.getElementsByTagName("p");
            var div = li.getElementsByTagName("div")[0];
            div.style["width"] = "100%";
            div.style["backgroundColor"] = "#0f0";
            for (var i = 0; i < ps.length; i++) {
                if (ps[i].className == "loader") {
                    ps[i].textContent = "Upload complete";
                    ps[i].style["color"] = "#3DD13F";
                    break;
                }
            }
        }, false);
        upload.addEventListener("error", function (ev) {console.log(ev);}, false);
        xhr.open(
            "POST",
            "upload.php"
        );
        xhr.setRequestHeader("Cache-Control", "no-cache");
        xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
        xhr.setRequestHeader("X-File-Name", file.name);
        xhr.send(file);
    }
}

With the level 2 implementation of XMLHttpRequest we have access to it’s associated XMLHttpRequestUpload object with it’s very handy progress event handler with which we can create progress indicators. The progress event handler presents the lengthComputable attribute which if true enables us to calculate the progress by taking what is loaded against the total. All we need do is set the relevant http headers, including a custom header which we need to use to pass our file name to the server to save the file, and call the send method with the file object as the argument.

Summary

Although the code is somewhat experimental I think it is something that I will most definitely use in an application in the not too distant future, Firefox and Chrome are at the time of writing the only browsers with support for FileReader and File APIs and I should imagine Safari is not far behind. As for Internet Explorer 9…. who knows?

References

  1. XMLHttpRequest Level 2 specification.
  2. File API specification

Related Posts

No related posts.

61 Responses So Far

  1. gnorc December 1, 2010 at 9:49 pm

    nice demo. I’ve worked with the ZIP; but I’m missing the “Streamer.php” file that implements the File_Streamer class. with php://input i’ve reconstructed some of that but I’m wondering if I’m missing out on something. Is that class avaliable somewhere?

    • Phil December 1, 2010 at 10:59 pm

      Strange, I’ll have to update the zip. Basically just a wrapper for what you have done so no you aren’t missing anything there. The upload can be pretty much handled with:

      file_put_contents($filepath, file_get_contents(“php://input”));

      $filepath would be your upload folder location with the filename which can get back from $_SERVER['HTTP_X_FILE_NAME']. I’ll dig it out and update the download now.

      • Eduardo July 19, 2011 at 12:07 pm

        How can we use de $filepath? Because I need to say in what folder the file will be upload. Can you post exactly where we put the parameters? I tried to do this but don’t work

        var params = "destPath=" + document.getElementById("destPath").value;

        xhr.open(
        "POST",
        "upload.php", true
        );
        xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        xhr.setRequestHeader("Cache-Control", "no-cache");
        xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
        xhr.setRequestHeader("X-File-Name", file.name);
        xhr.setRequestHeader("Content-length", params.length);
        xhr.send(file);

        If I put xhr.send(params), then value of destPath is write in the file!

        Tks!

      • Phil July 19, 2011 at 12:36 pm

        You want to set the file path on the server side. If you download the .zip it is all in there.

  2. Bob Robinson January 19, 2011 at 3:24 pm

    Brilliant demo. This is the best one I have seen for using both the FileAPI and XMLHttpRequest Level 2 together for an upload with progress, client side. Thanks for posting this!

  3. Graphics and Web Design Firm April 6, 2011 at 9:56 am

    Wow! nice demo very helpful and informative thanks for sharing such a good work.

  4. Shane May 24, 2011 at 4:19 am

    Thanks for the demo. However, for some reason, the files don’t always upload. I’m not sure why this is, but maybe you have an explanation. It seems to have an upload progress for each file, but on my server, it doesn’t show all files as being uploaded.

  5. pierlo July 30, 2011 at 12:38 pm

    Great tutorial. One question though, why did you choose not to implement this using jQuery? At least when it comes to DOM manipulation like this

    var ps = li.getElementsByTagName("p");
    var div = li.getElementsByTagName("div")[0];
    div.style["width"] = "100%";
    div.style["backgroundColor"] = "#0f0";

    etc…

    (just for my curiosity)

    • Phil July 30, 2011 at 8:40 pm

      Thanks Pierlo,

      No reason not to use jQuery or any other library for that matter. This was just something I was experimenting with to get a use with new File APIs. Most of us depend on libraries day to day (for very good reasons) but I find it nice to use what is at the core of the language sometimes.

      • pierlo January 10, 2012 at 5:32 pm

        sorry i forgot to check back!!! I wanted to say thanks again, really good article.

        Anyway I wanted to ask for help: I’m at a loss as to why I’m not able to get responses (in the form of text) back from the php script. I’m guessing it has to do with XSS protections, could it be?

        Basically whenever i post back something (with a simple echo) from the server back to the js, i’m not able to access the xhr object’s responseText property.

        I’ve tried, for example, to do this

        upload.addEventListener("load", function (ev) {
        console.log(xhr.responseText);
        (...)
        }

        but it logs ‘undefined’ no matter what php echoes.
        However, if I were to log the whole xhr object, Firebug would show me the xhr.responseText value highlighted in red…

        So the question is: how can i possibly get the php and js in your example to talk to each other nice and easy?

        I think this could be terribly useful for error handling, etc…
        no?
        Thanks!

      • Phil January 10, 2012 at 8:02 pm

        You want to listen for the onreadystatechange event on xhr to handle data returned from the server. Check out this little lib I have put together https://github.com/p-m-p/xhr2-lib

      • pierlo January 11, 2012 at 8:19 am

        omg… THANK YOU SO MUCH!

  6. Pingback: Removing Flash from OS X: My experience, frustrations & some HTML5 alternatives » Joe Lambert

  7. farouknl October 2, 2011 at 11:25 pm

    A best script. thank you.

    I want to use it in php and I want this form to send a text ID to the database.

    Example

    form action=”" method=”post” enctype=”multipart/form-data”>
    input type=”file” id=”fileField” name=”fileField” multiple />
    input type=\”hidden\” name=\”text_id\” value=\”$text_id\”>

    I am new to JavaScript and do not know how to do this.
    Who will help me?

    thanks

  8. Kenaniah November 3, 2011 at 11:23 pm

    Instead of loading the image inline as base64 content, try using the createObjectURL() method by changing line 92 of FileAPI.js to:

    thumb.src = window.URL ? window.URL.createObjectURL(file) : window.webkitURL.createObjectURL(file);

    See https://developer.mozilla.org/en/DOM/window.URL.createObjectURL for more info

    • Phil November 4, 2011 at 6:10 am

      That’s a good call, wrote this a while back and don’t remember seeing that in the spec at the time. I may do a refresh of this article soon and will definitely factor that in.

  9. gerteb December 18, 2011 at 8:06 pm

    Great tutorial and sample code. Is there any limitations on usage of the code. Wold like to integrate it with modifications in a prorietary project, i a have been working on for some years now.

  10. gerteb December 19, 2011 at 11:29 am

    After testing the script in FF and Chrome i discovered that dropping large amount of 3-5Mb pictures made the browsers crash! Opera 11.6 doesn’t accept dropping files.

    The reason i believe is memory caching of “readAsDataURL”, and too many working threads. Rewrote the function addFileListItems listed below, so it is recursive instead of itearative. Now only one picture is delt with at file read from filesystem, and the image object is cleared from memory. The function showImagePreview has to be rewritten to read the image file or omitted.

    At test after this rewrite FF stops after 34 file reads, and Chrome stops after 334 file reads. That is why there is a second condition i the for statment in the first line of the function.

    Hope it is of any use.


    var addFileListItems = function (files,i) {
    if(i<files.length&&i<25){
    var file = files[i];
    if (file) {
    var li = document.createElement("li");
    if (file.type.search(/image\/.*/) != -1) {
    window.URL=window.webkitURL || window.URL; // Vendor prefixed in Chrome.
    var thumb = document.createElement('img');
    thumb.onload = function(e) {
    window.URL.revokeObjectURL(thumb.src); // Clean up after yourself.
    i++;
    addFileListItems(files,i);// Recursive call
    };
    thumb.src = window.URL.createObjectURL(file);
    thumb.addEventListener("mouseover", showImagePreview, false);
    thumb.addEventListener("mouseout", removePreview, false);
    li.appendChild(thumb);
    }
    var h3 = document.createElement("h3");
    var h3Text = document.createTextNode(file.name);
    h3.appendChild(h3Text);
    li.appendChild(h3)
    var p = document.createElement("p");
    var pText = document.createTextNode("File type: ("+file.name+")-"+Math.round(file.size / 1024) + "KB");
    p.appendChild(pText);
    li.appendChild(p);
    var divLoader = document.createElement("div");
    divLoader.className = "loadingIndicator";
    li.appendChild(divLoader);
    fileList.appendChild(li);
    fileQueue.push({
    file : file,
    li : li
    });
    }
    }
    }

  11. Pingback: Gock's Blog » 略谈浏览器Upload File (上传文件)

  12. Matteo January 11, 2012 at 11:33 am

    Hi,
    Great tutorial. I would ask you if can i check the height and width of the file before to upload it.

  13. Pingback: XMLHttpRequest Level 2 Ajax library, xhr2-lib | Phil Parsons

  14. Craig February 20, 2012 at 10:58 pm

    Great write up! I’ve been playing with this myself lately and can’t wait for IE to support it (v10!).

    One thing that I can’t find is the ability to iterate through a formData object to remove a file(s) a user may have accidentally uploaded. Say they drag a group of 5 files on to a dropzone, and then decide they want to remove one, before submitting. I can’t see a way to do that, short of clearing the whole object – any ideas?

    • Phil February 21, 2012 at 8:04 pm

      You just need to keep a reference to the files you do want and build the formdata object from those once you are ready to send them.

      • Craig February 21, 2012 at 11:23 pm

        Yeah, someone suggested that on StackExchange. Seems like a pretty glaring oversight from W3C to not be able to loop through formData and remove elements.

  15. jay March 27, 2012 at 7:56 am

    Well the progress sorta works for me in FF it goes 100% then 20s later says complete (for large files, small ones go too quick to test)

    Chrome just goes from 0 to 50 to 100 in less than a second, then says complete never know when it’s really done

    Like the demo though thanks :P

  16. Frederik Braun March 28, 2012 at 12:12 pm

    This line

    $this->fileName = $_SERVER['HTTP_X_FILE_NAME'];

    Might pose a serious security flaw.
    Imagine someone doing a HTTP request with the filename ‘../index.php’.
    People will be able to overwrite your web application and execute arbitrary code on your server.

    Try $this->fileName = basename($_SERVER['HTTP_X_FILE_NAME']);
    I know this is a demo and you said you’re not using it in production, but people often use demos to built something serious with it.

  17. Matteo March 30, 2012 at 4:49 pm

    Hi all, i tried this tutorial on opera, but seems doesnt work. Any ideas or suggests ?

    • Phil March 31, 2012 at 11:34 am

      Opera doesn’t support the File APIs yet, this stuff is all experimental still although you’ll be fine in any webkit or Gecko based browser

  18. Pingback: 拖拽并自动上传至服务器(译) 》 html5jscss

  19. Ankkit April 20, 2012 at 6:14 am

    I just wanted to know what I need to do to get the “downloaded files” working the same way your demo works. Also how do you specify the file path, can I send the same files to the public folder of my dropbox account?

  20. Ankkit April 20, 2012 at 6:16 am

    Also I just read Frederik’s comment, very true. That is exactly what I am doing. I landed here from (http://html5doctor.com/drag-and-drop-to-server/) html5doctor.

  21. Ankkit April 20, 2012 at 8:17 am

    i ment is there a tutorial on how to upload files to my dropbox account using this method. And dropbox has a similar working method on their site. but if i want to upload to my dropbox account from another site.

    • Phil April 20, 2012 at 12:30 pm

      I’ve not used the Dropbox API but if they support Cross Origin Resource Sharing then you can use FormData and post the file. I doubt they do so you may have to upload the file to your server and post it from there.

      • jon July 19, 2012 at 10:41 am

        Hey Phil, great tutorial and code… I was wandering if you could help me with allowing cross domain upload… I have added the following
        header(“Access-Control-Allow-Origin: *”);
        to my upload.php file on server B… however it is not working.. is there anything else I need to do?.. kind regards J

      • Phil July 19, 2012 at 6:41 pm

        With regard the cross origin request you’ll need to set the allow method also, you can read about that in a post I wrote not so long ago.

        This article is fairly old now and the File APIs have come on quite a bit. You should use the FormData interface which is just like posting a regular form.

  22. Michele May 2, 2012 at 4:41 pm

    Pay attention: clear list link only flush visible list! Remember to add something like fileQueue = new Array() into function clearList

    • Phil May 3, 2012 at 10:37 am

      Ha, yeah you’re right. I really must do an up to date version of this article!

      • karthikeyan April 4, 2013 at 4:58 am

        Hi phill
        Hey Phil, great tutorial and code,i am using many project this multiple file upload API,
        one small bug:
        when I upload 3 images and do clearlist and again i upload 2 images finally I click that upload button,5 requests are going to server,we wand only 2 request when we added last two images,I found that solution,the fileQueue array length should be empty.like this

        this.clearList = function (ev) {
        ev.preventDefault();
        while (fileList.childNodes.length > 0) {
        fileList.removeChild(
        fileList.childNodes[fileList.childNodes.length - 1]
        );
        }
        fileQueue.length = 0; //array length must be zero
        }

  23. Alexander May 8, 2012 at 3:54 pm

    Hey Phil,

    Thanks for this explanation.

    I see a strange behaviour in Google Chrome with FileReader.

    The “loadend” handler fires up, but e.target has no “file” property attached to it. I only have :

    error: null
    onabort: null
    onerror: null
    onload: null
    onloadend: null
    onloadstart: null
    onprogress: null
    readyState: 2
    result: “data:image/png;base64,…”

    Do you an idea of what is going on?

    • Phil May 10, 2012 at 10:40 am

      file is not an attribute of the fileReader object you want to use the result attribute if using readAsDataURL, in this code I just set a reference to the file on the fileReader instance so that I can use it in the showFileInList function.

  24. Alexander May 8, 2012 at 4:15 pm

    The strangiest thing is that your demo actually works.

    I tried to upload my script on a remote testing server without success (Google Chrome has some strange security restrictions that I disabled in command line using –allow-file-access-from-files to make it work on a file://… URL).

    And as for Firefox 12, it doesn’t even work. The image is simply loaded instead of being handled by JS. But your demo works.

  25. Alexander May 8, 2012 at 4:46 pm

    You can see a simple script here:

    Ok, I managed to make it work in Firefox but event.target.file is still undefined.

    May it be related to the fact that I am using Mac OS X Lion ?

    I’ve put a brief demo here:

    http://94.23.52.56/

    Every event is logged in the browser console.

  26. Karthikeyan May 11, 2012 at 5:11 am

    Hi phil,
    This is nice tutorial for multiple file upload,But I have one doubt,I wand to upload images with some image format like GIF|PNG|JPEG.,Thanks

  27. Vernard May 27, 2012 at 6:34 am

    Hello,
    I have tried using this to my project but i have a problem… After selecting the files ready for upload, there is a ‘clear list’ function but i don’t know how to remove a file individually. I have appended an “X” button but I don’t know how to add that function.

    Please reply ASAP, thanks. Thumbs up for sharing this code. It’s very flexible.

  28. jon July 19, 2012 at 4:41 pm

    Hey Phil, me again… Aswell as wanting to send the upload cross domain, I was wandering how to add other variables, for example a category_id or something that you want to also send to the php upload script. Thanks in advance… J

  29. Joey July 21, 2012 at 1:04 pm

    Thanks for this great example! Straight to the point; that’s how I like it :)

    However a little comment: It took me a little longer to figure out that the files do not end up in $_FILES.

    Maybe you can add a little example PHP file:

    $allowed_extensions = array('jpg','jpeg','png','gif'); // At least make sure no one is allowed to upload .php files!
    $destination_folder = 'files/uploads'; // No trailing slash
    $file_name = $_SERVER['HTTP_X_FILE_NAME'];

    if( !in_array( get_extension( $file_name ), $allowed_ext ) ){
    die( 'invalid extension: should be jpg, jpeg, png or gif' );
    }

    file_put_contents( $destination_folder . '/' . $file_name , file_get_contents( 'php://input' ) );

    Eventually I found out that in the ZIP file this example can be found, but then again, why download samples if the example is so clear?

  30. Tobias Launer August 6, 2012 at 8:34 am

    Hi Phil,
    this tutorial is really great and useful. But I have one question:
    How can I reset the fileList on fileField.change? I don´t want to add files to the list, when there are already files in the list. The problem is, if I want to have only one file for upload and I have no “multiple” in input, I can add a second file to the list thereafter. Do you have a solution for that?

    Thank you very much, Tobias

  31. Baurzhan Batykov October 10, 2012 at 4:50 pm

    When I test it at IE9 I get this (screenshot):

    http://funkyimg.com/u2/2195/993/113289error.png

    For fix height you can fix showFileInList:


    if (file.type.search(/image\/.*/) != -1) {
    var thumb = new Image();
    thumb.src = ev.target.result;
    thumb.addEventListener("mouseover", showImagePreview, false);
    thumb.addEventListener("mouseout", removePreview, false);
    thumb.load;
    thumb.height = thumb.height / (thumb.width / 120);
    thumb.width = 120;
    li.appendChild(thumb);
    }

    120 is a desirable width of your thumb.

  32. Baurzhan Batykov October 10, 2012 at 4:51 pm

    I hope you can understand my russian-english. :\

  33. kartoshin October 31, 2012 at 7:05 am

    And after you make backend for receiving and processing these files don’t forget to test it, because there are some default parameters like php’s max_file_uploads (which is 20 by default) and post_max_size (8M by default) that can give you unexpected results if you forget about them.

  34. Pingback: File API和xhr 2实现图片上传 | zp

  35. Jose D April 9, 2013 at 12:49 am

    Any way to make it so that when two files with identical names are uploaded, the first one is NOT overwritten. Currently same files names+extensions get overwritten

  36. Matty May 11, 2013 at 11:08 pm

    Thank you for this eye-opening demo!

    (Aside: I got around the same-filename-overwriting problem mentioned by Jose above by appending randomly-generated characters to each filename in the php)

    I’ve been playing around to see how browser support has come along since the time of writing, and discovered something odd: This works a treat in FF, Chrome, Opera, IE10. It also works in Safari…. with the exception that files over ~1MB in size do not get posted to the PHP. (I changed the in-code limit to 5MB). From iPhone, iPad and Desktop Safari, if any files larger than 1MB or thereabouts are included in the list, those particular files seem to be totally neglected when it comes to the sending to the PHP, although the upload progress bars behave as normal.

    After lots of googling I’m still at a loss for why this might be occurring. I’d understand if the limit was purely browser-specific or purely size-limited, but the combination of Safari and file size causing the problem seems rather odd. My heartiest thanks and congratulations to anyone who can shed light on this mystery!

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Recommended reading

  • Ben Nadel Articles on “obsessively thorough web development” with Coldfusion and jQuery
  • Christian Heimann's blog – Wait till I come!! Articles on web development
  • David Walsh Articles around PHP, CSS, MooTools, jQuery and just about everything else
  • John Resig Javascript programmer and the creator of the jQuery library
  • Mary Lou (Codrops) Stunning designs with lots of cool jQuery effects

Photostream

  • Loading photostream from Flickr...