How to create a progress bar on file upload using PHP/AJAX

Post Reply
User avatar
Shane
Captain
Captain
Posts: 226
Joined: Sun Jul 19, 2009 9:59 pm
Location: Jönköping, Sweden

How to create a progress bar on file upload using PHP/AJAX

Post by Shane » Wed Feb 24, 2010 10:21 am

Introduction
In this tutorial I will explain how to create a progress bar for PHP file uploads. There is no method built into PHP for returning the status of an upload in progress, so this requires a module called the Alternative PHP Cache (APC). This allows you to store data across sessions and includes built-in functionality for storing/returning file upload stats.

This tutorial assumes you have some basic understanding of XHTML, PHP, and JavaScript. If you don't, it may be hard to understand.

Installing APC is beyond the scope of this tutorial, so visit the APC Installation page for help or talk to your host. To find out if APC is installed already, create a PHP file on your site containing "<?php phpinfo(); ?>", visit it in your browser, and look for a section titled 'APC.' If it is present, then it is installed.

Creating the HTML Form
The first step is to create the form that will accept the file upload. This page will also contain the progress bar, hidden when the page loads. Start with creating the <form> element:

Code: Select all

<html>
<head>
<title>Upload Progress Bar</title>
</head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data" name="upload" id="upload" target="upload_frame">
  <input type="hidden" name="APC_UPLOAD_PROGRESS" id="progress_key" value="<?php echo $uid; ?>" />
  <input type="file" name="file" id="file" />
  <input type="submit" name="submit" id="submit" value="Upload!" />
</form>
</body>
</html>
Save this file as upload_form.php.

Now to break down the code:

Code: Select all

<form action="upload.php" method="post" enctype="multipart/form-data" name="upload" id="upload" target="upload_frame"> 
The important thing that allows a form to accept files is the enctype="multipart/form-data" attribute, which allows binary data (files). The action is set to the script that will accept the file, target is set to a hidden iframe that we will add, and id is set so we can access the form with JavaScript.

Code: Select all

<input type="hidden" name="APC_UPLOAD_PROGRESS" id="progress_key" value="<?php echo $uid; ?>" />
This part tells APC to record the upload stats from this form. The value will be a unique string generated by PHP used to identify the upload session.

The next two lines are the file selector and submit button and are fairly self-explanatory.

Next, we need to add the progress bar. The bar will consist of two CSS-styled divs, one nested within the other. The inner one will expand based on the upload progress. Here is what it looks like:

Code: Select all

<div id="pb_outer">
  <div id="pb_inner"></div>
</div> 
Here is some good CSS to style the progress bar. Place this within the head tags:

Code: Select all

<style type="text/css">
#pb_outer {
    height: 20px;
    border: 1px inset #000000;
    width: 80%;
    margin: 20px auto;
    display: none;
}
#pb_inner {
    font-weight: bold;
    color: #FFFFFF;
    background-color: #003399;
    height: 20px;
    width: 1px;
    text-align: center;
}
</style> 
This sets the size (80% width, 20px height), position (centred), colours (blue bar with white text), and makes it all hidden initially. You can easily change the style by just editing this CSS block.

Now is also a good time to add that hidden iframe that will upload the file in the background:

Code: Select all

<iframe style="display: none" id="upload_frame" name="upload_frame"></iframe> 
Here is what upload_form.php should look like now:

Code: Select all

<html>
<head>
<title>Upload Progess Bar</title>
<style type="text/css">
#pb_outer {
    height: 20px;
    border: 1px inset #000000;
    width: 80%;
    margin: 20px auto;
    display: none;
}
#pb_inner {
    font-weight: bold;
    color: #FFFFFF;
    background-color: #003399;
    height: 16px;
    width: 1px;
    text-align: center;
    padding: 2px 0px;
}
</style>
</head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data" name="upload" id="upload" target="upload_frame">
  <input type="hidden" name="APC_UPLOAD_PROGRESS" id="progress_key" value="<?php echo $uid; ?>" />
  <input type="file" name="file" id="file" />
  <input type="submit" name="submit" id="submit" value="Upload!" />
</form>
<div id="pb_outer">
  <div id="pb_inner"></div>
</div>
<iframe style="display: none" id="upload_frame" name="upload_frame"></iframe>
</body>
</html>
Creating the PHP Scripts
Now that we have a form that submits a file to a non-existent PHP script via a frame, it's time to make that script (and some helper scripts).

First, we need to generate that unique key with just one simple line in upload_form.php:

Code: Select all

<?php $uid = md5(uniqid(rand())); ?>
Place this line at the very top of upload_form.php. This creates a random unique id (uniqid()) and hashes it with md5 to make sure it is URL safe. When the page loads, "APC_UPLOAD_PROGRESS" will have a value set to this string.

Now it's time for an actual file upload script, which generally looks like this:

Code: Select all

<?php
if($_FILES['file']['error'] == UPLOAD_ERR_OK){
   $path = '/var/www/uploads/';
   $path .= basename($_FILES['file']['name']);
   if(move_uploaded_file($_FILES['file']['tmp_name'], $path)){
      // upload successful
   }
}
?>
Note: On PHP versions prior to 4.1.0 replace $_FILES with $HTTP_POST_FILES
Place this code in a file called upload.php. This will make sure there was no upload error, and then move the file to the specified path. Note that if you do not use move_uploaded_file(), the uploaded file will be deleted from the temp directory.

The last bit of PHP we need is for fetching the current upload progress:

Code: Select all

<?php
if(isset($_GET['uid'])){
   $status = apc_fetch('upload_' . $_GET['uid']);
   echo round($status['current']/$status['total']*100);
}
?>
Place this in a file named getprogress.php. This fetches an array of upload stats from APC using the unique id with the "upload_" prefix (this will be passed via a GET variable). Then it calculates the uploaded percentage by dividing the current position ($status['current']) by the total size ($status['total']) and multiplying by 100. The script will return this number rounded to the nearest whole number.

There is one slight problem with this script, especially since we will be calling this with AJAX, and that is caching. Some browsers will cache the output of the script and cause the bar to freeze after the initial request. To solve this, we can use headers to instruct the browser not to cache:

Code: Select all

header('Expires: Tue, 08 Oct 1991 00:00:00 GMT');
header('Cache-Control: no-cache, must-revalidate'); 
The first header tricks the browser into thinking this is a very old document that expired in 1991, and the second is the standard cache control settings. Both are needed in case the browser ignores the Cache-Control header.

Here is what getprogress.php should look like:

Code: Select all

<?php
header('Expires: Tue, 08 Oct 1991 00:00:00 GMT');
header('Cache-Control: no-cache, must-revalidate');
 
if(isset($_GET['uid'])){
   $status = apc_fetch('upload_' . $_GET['uid']);
   echo round($status['current']/$status['total']*100);
}
?>
Creating the JavaScript (AJAX)
Now this is where it gets interesting. We need to uses JavaScript to animate the progress bar based on the output of getprogress.php, and this requires the use of AJAX. AJAX is a method that allows you to load content on demand without reloading the entire web page.

To set up the AJAX functionality, we need to create a cross-browser request object. There are two main types of request objects, one that works on most browsers, and one that works in Internet Explorer. Below is the code that I use to handle this. Place this within the head tags in upload_form.php:

Code: Select all

<script type="text/javascript">
var HttpRequestObject = false;
if(window.XMLHttpRequest) {
   HttpRequestObject = new XMLHttpRequest();
}
else if(window.ActiveXObject) {
   HttpRequestObject = new ActiveXObject("Microsoft.XMLHTTP");
}
</script>
This creates an object named HttpRequestObject, which holds whichever AJAX object is available in the user's browser. The first condition checks for the existence of the XMLHttpRequest object (Firefox, etc.), and if that fails it will check for the equivalent ActiveXObject (Internet Explorer). You may also want to add error handling in case neither one is available.

Now that we have that set up, we need the actual functions that do the work. We will create two functions, one that initializes the progress bar and one that moves the progress bar. We will start with the one that moves the bar and makes the AJAX calls. Let's start with the backbones of using AJAX:

Code: Select all

function getProgress(uid) {
   if(HttpRequestObject) {
      HttpRequestObject.open('GET', 'getprogress.php?uid=' + uid, true);
      HttpRequestObject.onreadystatechange = function() {
         if(HttpRequestObject.readyState == 4 && HttpRequestObject.status == 200) {
         }
      }
      HttpRequestObject.send(null);
   }
} 
This creates the function getProgress(), which has the unique ID passed to it as an argument. First it checks if the HttpRequestObject exists before trying to use it. Then using that, it uses the open method to set up the request. The open method accepts 3 arguments: the request method, the URL, and whether the request is asynchronous. The request method is GET in this case, but the other common method is POST which is slightly more complex. Asynchronous means that other actions (other AJAX calls) can take place while the request is being made; setting this to false will cause everything to pause until it is done, which might be useful in some situations.

Since we are using GET, we pass variables by appending it to the URL in the format ?key=value, in this case we pass the unique ID this way. The next part of the function uses the onreadystatechange event handler to set up the actions that will happen as soon as the data is returned. If the readystate is 4 (finished), and HTTP response code is 200, then it is ready to do stuff. The last line of the function is what actually sends the request to the server.

Now that we have out function set up to query the server for upload status, it is now time to use the info to move the progress bar. Here's the code we'll use:

Code: Select all

            var progress = HttpRequestObject.responseText;
            document.getElementById('pb_inner').style.width = progress + '%';
            document.getElementById('pb_inner').innerHTML = progress + '%'; 
This fetches the response as text using responseText and stores it in a variable called progress. The next line sets the width to the percentage using the style.width property, and the one after that sets the text inside of the bar to display the response using the innerHTML property.

At this point, the function will execute just once. We need to cause it to loop until the upload is complete. This can be easily accomplished using the setTimeout function, which calls a function after a delay set in milliseconds. In this case we'll call the getProgress() function again.

Code: Select all

            if(progress < 100) {
               setTimeout('getProgress("' + uid + '")', 100);
            }
            else {
               document.getElementById('pb_inner').innerHTML = 'Upload Complete!';
            } 
This will cause the function to call itself every 10th of a second as long as the percentage is below 100. If it is at 100, it will stop and set the text to say "Upload Complete."

Here is the function in it's entirety:

Code: Select all

function getProgress(uid) {
   if(HttpRequestObject) {
      HttpRequestObject.open('GET', 'getprogress.php?uid=' + uid, true);
      HttpRequestObject.onreadystatechange = function() {
         if(HttpRequestObject.readyState == 4 && HttpRequestObject.status == 200) {
            var progress = HttpRequestObject.responseText;
            document.getElementById('pb_inner').style.width = progress + '%';
            document.getElementById('pb_inner').innerHTML = progress + '%';
            if(progress < 100) {
               setTimeout('getProgress("' + uid + '")', 100);
            }
            else {
               document.getElementById('pb_inner').innerHTML = 'Upload Complete!';
            }
         }
      }
      HttpRequestObject.send(null);
   }
} 
Now it is time to make that other function that starts the whole process:

Code: Select all

function startProgress(uid) {
   document.getElementById('upload').style.display = 'none';
   document.getElementById('pb_outer').style.display = 'block';
   setTimeout('getProgress("' + uid + '")', 1000);
} 
All this does is hides the form by setting style.display to 'none' and shows the progress bar by setting style.display to 'block.' Next, it calls the getProgress() function after 1 second, passing the unique ID along.

Now all that is left is to tie the form the the JavaScript by adding this to the form HTML element:

Code: Select all

onSubmit="startProgress('<?php echo $uid; ?>');"
The next page contains the completed code. Enjoy!

The Finished Product
I hope this was helpful! Any questions, post a comment. For your convenience, here is all the code used in this tutorial.

To see it in action, see our example.

upload_form.php

Code: Select all

<?php $uid = md5(uniqid(rand())); ?>
<html>
<head>
<title>Upload Progess Bar</title>
<style type="text/css">
#pb_outer {
    height: 20px;
    border: 1px inset #000000;
    width: 80%;
    margin: 20px auto;
    display: none;
}
#pb_inner {
    font-weight: bold;
    color: #FFFFFF;
    background-color: #003399;
    height: 20px;
    width: 1px;
    text-align: center;
}
</style>
<script type="text/javascript">
var HttpRequestObject = false;
if(window.XMLHttpRequest) {
   HttpRequestObject = new XMLHttpRequest();
}
else if(window.ActiveXObject) {
   HttpRequestObject = new ActiveXObject("Microsoft.XMLHTTP");
}
function startProgress(uid) {
   document.getElementById('upload').style.display = 'none';
   document.getElementById('pb_outer').style.display = 'block';
   setTimeout('getProgress("' + uid + '")', 1000);
}
function getProgress(uid) {
   if(HttpRequestObject) {
      HttpRequestObject.open('GET', 'getprogress.php?uid=' + uid, true);
      HttpRequestObject.onreadystatechange = function() {
         if(HttpRequestObject.readyState == 4 && HttpRequestObject.status == 200) {
            var progress = HttpRequestObject.responseText;
            document.getElementById('pb_inner').style.width = progress + '%';
            document.getElementById('pb_inner').innerHTML = progress + '%';
            if(progress < 100) {
               setTimeout('getProgress("' + uid + '")', 100);
            }
            else {
               document.getElementById('pb_inner').innerHTML = 'Upload Complete!';
            }
         }
      }
      HttpRequestObject.send(null);
   }
}
</script>
</head>
<body>
<form onSubmit="startProgress('<?php echo $uid; ?>');" action="upload.php" method="post" enctype="multipart/form-data" name="upload" id="upload" target="upload_frame">
  <input type="hidden" name="APC_UPLOAD_PROGRESS" id="progress_key" value="<?php echo $uid; ?>" />
  <input type="file" name="file" id="file" />
  <input type="submit" name="submit" id="submit" value="Upload!" />
</form>
<div id="pb_outer">
  <div id="pb_inner"></div>
</div>
<iframe style="display: none" id="upload_frame" name="upload_frame"></iframe>
</body>
</html>
upload.php

Code: Select all

<?php
if($_FILES['file']['error'] == UPLOAD_ERR_OK){
   $path = '/var/www/uploads/';
   $path .= basename($_FILES['file']['name']);
   if(move_uploaded_file($_FILES['file']['tmp_name'], $path)){
      // upload successful
   }
}
?>
getprogress.php

Code: Select all

<?php
header('Expires: Tue, 08 Oct 1991 00:00:00 GMT');
header('Cache-Control: no-cache, must-revalidate');
 
if(isset($_GET['uid'])){
   $status = apc_fetch('upload_' . $_GET['uid']);
   echo round($status['current']/$status['total']*100);
}
?>
Courtesy of http://www.ultramegatech.com/blog/2008/ ... s-bar-php/
Mysoogal
Captain
Captain
Posts: 223
Joined: Thu Dec 17, 2009 7:15 am
Location: Planet VPS

Re: How to create a progress bar on file upload using PHP/AJAX

Post by Mysoogal » Mon Apr 19, 2010 3:45 am

i hope i can use this on my video encoding application :geek:
Post Reply

Return to “PHP & MySQL”