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:
- How to upload files with AJAX using various methods
- How to get the progress notification of upload
- Some of the drawback of these methods and when to and not to use a particle method
Assumptions
- You are familiar with HTML5 already
- You are familiar with the basic concept of AJAX already
- (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.
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");
xhr.onreadystatechange = function()
{
if(xhr.readyState == 4 && xhr.status == 200)
{
document.getElementById("serverresponse").innerHTML = xhr.responseText;
}
}
var fileObj = new FileReader();
fileObj.onload = function()
{
xhr.send("file1=" + fileObj.result);
}
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
$data = $_POST['file1'];
$data = explode(",", $data);
$data[1] = str_replace(' ', '+', $data[1]);
$fh = fopen("picture.jpg", "w");
fwrite($fh, base64_decode($data[1]));
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;
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);
document.getElementById('uploadedbyte').innerHTML = j.BytesToStructuredString(e.loaded);
document.getElementById('totalbyte').innerHTML = j.BytesToStructuredString(e.total);
}
}
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:
- Have a function called every seconds
- Have a variable prevUpload (var prevUpload = 0)
- At the first call of the function, get the total bytes already sent
- Speed will then be "present value of total upload (e.loaded) – value of total upload 1 seconds ago (prevUpload)"
- Equate prevUpload to e.loaded</li>
- 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 = uploaded - prevUpload;
prevUpload = uploaded;
document.getElementById("speed").innerHTML = j.SpeedToStructuredString(speed);
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.
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");
xhr.onreadystatechange = function()
{
if(xhr.readyState == 4 && xhr.status == 200)
{
document.getElementById("serverresponse").innerHTML = xhr.responseText;
}
}
var fileObj = new FileReader();
fileObj.onload = function()
{
var data = "";
bytes = new Uint8Array(fileObj.result);
var length = bytes.byteLength;
for(var i = 0; i < length; i++)
{
data += String.fromCharCode(bytes[i]);
}
data = btoa(data);
xhr.send("file1=" + data);
}
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
$data = $_POST['file1'];
$data = str_replace(' ', '+', $data);
$fh = fopen("picture.jpg", "w");
fwrite($fh, base64_decode($data));
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.
<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
function sendFile()
{
var url = "upload.php";
var formData = new FormData(document.getElementById("form1"));
xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
xhr.onreadystatechange = function()
{
if(xhr.readyState == 4 && xhr.status == 200)
{
document.getElementById("serverresponse").innerHTML = xhr.responseText;
}
}
xhr.send(formData);
}
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
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");
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
Conclusion
Please note that these as just my own conclusions, they are not standards.
- Use
readAsDataURL
method to send large files when you have access to only the File object. - 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. - 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.