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
Related Posts
No related posts.
61 Responses So Far
-
-
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!
-
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.
-
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.
-
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)
-
Pingback: Removing Flash from OS X: My experience, frustrations & some HTML5 alternatives » Joe Lambert
-
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
-
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
-
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.
-
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
});
}
}
}
-
Pingback: Gock's Blog » 略谈浏览器Upload File (上传文件)
-
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. -
Pingback: XMLHttpRequest Level 2 Ajax library, xhr2-lib | Phil Parsons
-
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?
-
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
-
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. -
Matteo March 30, 2012 at 4:49 pm
Hi all, i tried this tutorial on opera, but seems doesnt work. Any ideas or suggests ?
-
Pingback: 拖拽并自动上传至服务器(译) 》 html5jscss
-
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?
-
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.
-
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.
-
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 functionclearList -
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?
-
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.
-
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:
Every event is logged in the browser console.
-
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 -
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.
-
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
-
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?
-
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
-
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.
-
Baurzhan Batykov October 10, 2012 at 4:51 pm
I hope you can understand my russian-english. :\
-
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.
-
Pingback: File API和xhr 2实现图片上传 | zp
-
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
-
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!





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?