Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML5

AJAX file uploader with progress notification in pure HTML5

4.87/5 (16 votes)
4 Dec 2013CPOL7 min read 55.3K   1.4K  
An AJAX file uploader with progress notification (percentage completed, upload speed, estimated time remaining, remaining bytes to speed, total bytes already sent, and total bytes to be sent) in pure HTML5 (no iframe, no external library)

Image 1

Introduction

Since the introduction of AJAX technology into the internet app development scene which allowed sending of requests to the server and updating part of a webpage without reloading the whole part, developers have taken advantage of it to write great apps.  

But as great as this technology is, it had a major limitation of not being able to upload files not until HTML5 with the introduction of FileReader, FormData, and ArrayBuffer. Before HTML5, developers had to fake AJAX file uploads using various methods. One of the most popular being the use of iFrame embedded into the document and a JSONP response from the server loaded back into the iFrame to fake the onreadystatechange method. 

With FileReader, FormData and ArrayBuffer of HTML5, AJAX now has the capacity to upload files and this article will show: 

  1. How to upload files with AJAX using various methods
  2. How to get the progress notification of upload
  3. Some of the drawback of these methods and when to and not to use a particle method

Assumptions 

  1. You are familiar with HTML5 already 
  2. You are familiar with the basic concept of AJAX already
  3. (Not an assumption, just a fact) This article uses PHP on the server side but any server can be used be it ASP, ASP.NET, ColdFusion, whatever-server-that-is-worth-it-salt

Uploading Files 

Various ways can be used to upload files in AJAX depending on which DOMElement is available for use. For instance, you might have the DOMElement of a single <code>File object ("<input type='file' …/>") or that of an HTMLFormElement ("<form></form>") containing various File object.

Using readAsDataURL 

The member readAsDataURL of the FileReader object can be used with AJAX to upload file when you have a single File object. 

readAsDataURL converts the content of the file to a base64 encoded string which can then be easily sent to the server and decode back to the original content. 

JavaScript
function sendFile()
{
    var url = "upload.php";
    var file = document.getElementById("file1").files[0];
    xhr = new XMLHttpRequest();
    xhr.open("POST", url, true);
    xhr.setRequestHeader("Content-type", 
      "application/x-www-form-urlencoded"); //very important
    xhr.onreadystatechange = function()
    {
        if(xhr.readyState == 4 && xhr.status == 200)
        {
            document.getElementById("serverresponse").innerHTML = xhr.responseText;
        }
    }
    //Read the file and send to server
    var fileObj = new FileReader();
    fileObj.onload = function()
    {
        /* Don't do this, it will cause serious 
           hugging of CPU resources when the file is very large
        var param = "file1=" + fileObj.result; xhr.send(param); */
        xhr.send("file1=" + fileObj.result); //Send to server
    }
    fileObj.readAsDataURL(file);
}

On the server side for readAsDataURL

Maybe I should talk about the drawback of using this method before discussing the server side code.

Ok, this is not actually a drawback of the method itself but the way PHP handles spaces in it base64 encoded strings (so if you are not using PHP on the server side this might not concern you). To prevent corruption of the data sent using the readAsDataURL in PHP, every space character in the encoded string must be replaced by the '+' character before decoding (it took me hours and reading through the User Contributed Notes on base64_decode on PHP website to figure out why the file am sending keep getting corrupted). 

That been said, to get down to the codes, the sent data can be accessed through the $_POST variable in PHP. You will notice I sent the the file from AJAX like thus xhr.send("file1=" + fileObj.result);, this means I can access the sent data with index 'file1' of $_POST. That is $_POST['file1'] will be holding the sent data in base64 encoded string BUT with some MIME information prepended to it so before decoding the data, we have to remove this MIME information.

The sent data is always in this format

data:<MIME info>;base64,<base64 string>

If the data is a jpg image is will look like

data:image/jpeg;base64,/9j/4AAQSkZJRg...

What we have to do is remove the data before the comma (including the comma), and decode the remaining data (if you are using PHP, remember to replace all spaces with '+')

PHP
<?php
    $data = $_POST['file1'];
    //Split the data into two. Data format is "data:<MIME
    // info>;base64,<base64 encoded string>"
    $data = explode(",", $data);
    //PHP handles spaces in base64 encoded string differently
    //so to prevent corruption of data, convert spaces to +
    $data[1] = str_replace(' ', '+', $data[1]);
    //now we can decode our data and write it to disk
    //You can get the right extention to use from the MIME info
    //in data[0] but I will assume our data is a jpg picture
    $fh = fopen("picture.jpg", "w");
    fwrite($fh, base64_decode($data[1])); //decode and write to file
    fclose($fh);
    echo "File written successfully";
?>

If you don't know the type of file that will be coming over (so as to know the right file extension to use), remember the zeroth element of $data contains the MIME information but NOTE that the actual MIME part of the data might be empty sometimes if the file type is strange to the browser sending the request

data:;base64,AxYtd... 

The Upload Progress notification

The upload notification is the same for all the method so I won't be repeating it.

Percentage completed, total bytes already sent, total to send, remaining bytes to send

The percentage of upload completed can be completed by attaching a progress eventListener to the upload method of the XMLHttpRequest object.

xhr.upload.addEventListener('progress', uploadProgress, false);

In the callback function "uploadProgress" the total bytes to upload and the total already uploaded can be gotten from the event object (e.total and e.loaded respectiviely). With this the percentage completed can be calculated.

var uploaded = 0, prevUpload = 0, speed = 0, total = 0, remainingBytes = 0, timeRemaining = 0;
function uploadProgress(e)
{
	if (e.lengthComputable) 
	{
		uploaded = e.loaded;
		total = e.total;
		//percentage
		var percentage = Math.round((e.loaded / e.total) * 100);
		document.getElementById('progress_percentage').innerHTML = percentage + '%';
		document.getElementById('progress').style.width = percentage + '%';
		
		document.getElementById("remainingbyte").innerHTML =  j.BytesToStructuredString(e.total - e.loaded);//remaining bytes
		document.getElementById('uploadedbyte').innerHTML = j.BytesToStructuredString(e.loaded);//uploaded bytes
		document.getElementById('totalbyte').innerHTML = j.BytesToStructuredString(e.total);//total bytes
	}
}

Upload speed and ETR (Estimated Time Remaining)

To get the upload speed which will be in <bytes>/seconds and the ETR there has to be a function called every seconds. Since e.loaded holds the value of total bytes already sent, speed can be calculated thus:

  1. Have a function called every seconds
  2. Have a variable prevUpload (var prevUpload = 0)
  3. At the first call of the function, get the total bytes already sent
  4. Speed will then be "present value of total upload (e.loaded) – value of total upload 1 seconds ago (prevUpload)"
  5. Equate prevUpload to e.loaded</li>
  6. Repeat 3 – 6
ETR (Estimated Time Remaining) can be calculated by finding the amount of bytes remaining to be sent (e.total – e.loaded) and dividing the result by the speed. ETR = (total-uploaded)/speed
function UploadSpeed()
{
	//speed
	speed = uploaded - prevUpload;
	prevUpload = uploaded;
	document.getElementById("speed").innerHTML = j.SpeedToStructuredString(speed);
	
	//Calculating ETR
	remainingBytes = total - uploaded;
	timeRemaining = remainingBytes / speed;
	document.getElementById("ETR").innerHTML = i.SecondsToStructuredString(timeRemaining);
}

Using readAsArrayBuffer

This method can be used when you have a single File object. It uses the readAsArrayBuffer to read the content of the file into an ArrayBuffer. This ArrayBuffer can then be converted to BinaryString and the BinaryString encoded to base64 (or any other format you please, like compressing the file) which can then be sent to the server (some people have even gone to the extent of tar balling the file before sending it over to the server). The question now is why go through all these stress when you can actually use readAsDataURL? The reason is you now have control over what is sent to the server, you can easily manipulate the data.

The only drawback this method has is that you have to read the content of the file into a variable so as to be able to manipulate it, this will cause serious performance issue if the file is large.

JavaScript
function sendFile()
{
    var url = "upload.php";
    var file = document.getElementById("file1").files[0];
    xhr = new XMLHttpRequest();
            
    xhr.open("POST", url, true);
    xhr.setRequestHeader("Content-type", 
      "application/x-www-form-urlencoded"); //very important
    xhr.onreadystatechange = function()
    {
        if(xhr.readyState == 4 && xhr.status == 200)
        {
            document.getElementById("serverresponse").innerHTML = xhr.responseText;
        }
    }
    
    //Read the file content convert to BinaryString, encode BinaryString to base64 and send
    var fileObj = new FileReader();
    fileObj.onload = function()
    {
        //Convert ArrayBuffer to BinaryString
        var data = "";
        bytes = new Uint8Array(fileObj.result);
        var length = bytes.byteLength;
        for(var i = 0; i < length; i++)
        {
            data += String.fromCharCode(bytes[i]);
        }
        
        //Encode BinaryString to base64
        data = btoa(data);
        xhr.send("file1=" + data); //Send to server
    }
    fileObj.readAsArrayBuffer(file);
}

Server side for readAsArrayBuffer

The server side is now easier since you choice the format of how the data is sent, but still remember if you are using PHP replace all space with '+'

PHP
<?php
    $data = $_POST['file1'];
    
    //PHP handles spaces in base64 encoded string differently
    //so to prevent corruption of data, convert spaces to +
    $data = str_replace(' ', '+', $data);
    
    //now we can decode our data and write it to disk
    $fh = fopen("picture.jpg", "w");
    fwrite($fh, base64_decode($data)); //decode and write to file
    fclose($fh);
    
    echo "File written successfully";
?>

Using FormData

This is the easiest of all since you don't have to manipulate any data and upload is treat like the traditional upload in the server side but you can only use it when you have an HTMLFormElement.

This method also does not have any performance issue making it the best method to always use.

HTML
<form id="form1" name="form1" enctype="multipart/form-data">
<input type="file" id="file1" name="file1">
    <input type="file" id="file2" name="file2">
    <input type="text" id="uname" name="uname" placeholder="Your name">
    <input type="button" value="Send" onclick="sendFile();" />
</form>

The AJAX part will be

JavaScript
function sendFile()
{
    var url = "upload.php";
    var formData = new FormData(document.getElementById("form1"));
    xhr = new XMLHttpRequest();    
    xhr.open("POST", url, true);
    //xhr.setRequestHeader("Content-type", 
      //  "application/x-www-form-urlencoded"); //no longer necessary here
    xhr.onreadystatechange = function()
    {
        if(xhr.readyState == 4 && xhr.status == 200)
        {
            document.getElementById("serverresponse").innerHTML = xhr.responseText;
        }
    }
    
    xhr.send(formData); //Send to server
}

The first thing you should note is the setRequestHeader("Content-type", "application/x-www-form-urlencoded"); of the XMLHttpRequest object in no longer needed BUT replaced with enctype="multipart/form-data" attribute of <form>. The only drawback for this method is your can't get progress notification for individual files, the reported progress notification is for the whole form data.

The server side for FormData

The server side is treated just like a traditional file upload. You can access the uploaded files from the $_FILES variable and other form element from the $_POST variable.

PHP
<?php
    //This is just like a normal form submission
    //You can access the uploaded files through $_FILES 
    if(isset($_FILES["file1"]))
        move_uploaded_file($_FILES["file1"]["tmp_name"], "picture1.jpg");
    
    if(isset($_FILES["file2"]))
        move_uploaded_file($_FILES["file2"]["tmp_name"], "picture2.jpg");
    
    //You can access other form element through $_POST
    echo "Thanks {$_POST['uname']}! Files received successfully.";
?>

A word about readAsBinaryString/sendAsBinary

Note that the readAsBinaryString of the FileReader object and the sendAsBinary of the XMLHttpRequest are not standards. They are only supported in Firefox (and readAsBinaryString only in Chrome). What they do is exactly what we did in the readAsArrayBuffer method. It is just Firefox way of making life easier for developers by helping them write some of the codes Wink | ;)

Conclusion 

Please note that these as just my own conclusions, they are not standards.

  1. Use readAsDataURL method to send large files when you have access to only the File object.
  2. Use readAsArrayBuffer method to send smaller files as using it for large files can cause performance issue because the file content has to first be read into a variable.
  3. Use FormData if you don't need to manipulate the data, you want very little alteration to the tradition way of handling uploads in the server side, and of course you have access to the HTMLFormElement. In my option, it is the best method to use.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)