PHP Upload Progress Bar

Originally posted August 23, 2006

———————————————–
Update: 4/8/07, added a 5.2.1 patch to the php5 tar ball
———————————————–

Ever wanted to display the progress of an upload to your user? This is handy for large uploads, gives users of your web site an idea of how long is left on the upload.

Unfortunately, PHP doesn’t provide this ability natively. Your first line of code in your PHP program receiving the upload doesn’t run until after the upload is complete. Therefore, there is no way, natively, for you to know the progress of the upload during the upload. Fortunately, I have a real solution. I downloaded a PHP source code patch (original patch available here http://pdoru.from.ro/) that adds this ability to PHP, then modified it to log data to a MySQL table or a flat file. The original patch did flat file only, which doesn’t work well on load balanced systems that need to share a data repository.

In a nutshell, here is how it works. During the upload, the PHP interpretter, behind the scenes, before your first line of code is executed, PHP updates a row in a MySQL table (or write to a flat file). It logs time started, time now, avg speed, speed last, bytes_uploaded, bytes total, files uploaded and eta. So, during the upload, you can load another window or iframe and repeatedly check the row in the MySQL table or flat file. If you check every second, you can continuely update your iframe, and give a smooth indicator of percent complete, eta, Kbps, etc.

This solution DOES require a recompile of PHP. You need root level access on your server to implement this solution.

DEMO: http://www.modphp.org/upload5/

I’ll start off by talking about how to install the patch. Later, I’ll give you an AJAX demo that allows you to upload files and display the upload progress inline. There is a pop-up window code example contained in the distribution tarball, so you can get started. But, be sure to check back for a really cool xAJAX demo.

—————————————-
Source of the modified, MySQL based, patches are here:

http://www.modphp.org/src/upload-progress-php-4.3.6-patch.tar.gz
and
http://modphp.com/src/upload-progress-php5-patch.tar.gz
—————————————-

The 4.3.6 patch is know to work on 4.3.6+ and the 5.0.1 patch is known to work on 5.0.1+.

As of 4/8/07, the upload-progress-php-5.0.1-patch.tar.gz file also contains a patch for 5.2.1+
As of 1/27/07, the upload-progress-php5-patch.tar.gz file contains a patch for 5.2.5+

Download the appropriate tarball, untar it, and follow the installation instructions.

For your convenience, here is a copy of the installation instructions:
————————————–

Here is how it works (these directions may be found in the INSTALL file of the above mentioned patch:

1.
first you apply the patch an install (or re-install) PHP:

 

cd /path/to/php-5.0.1/source/
patch -p1 < /path/to/upload-progress-php-5.0.1-patch
make clean
./configure –with-apxs=/etc/httpd/bin/apxs –with-mysql [any other flags you want] make

#stop, think, do you really want to make install, or just copy the compiled libphp5.so file into place?

make install

NOTE: If your “make” fails due to cannot find mysql.h, you may need to install the “mysql-devel” rpm.

If you get “undefined reference to ‘mysql_init'”, then you forgot to “–with-mysql”

2. Then you add something like this to the httpd.conf and restart apache:

#for php-progress-bar
# If you use upload_metter_dir, you should comment out the upload_metter_mysql_* lines
# If want to use Mysql to track the sessions then use the upload_metter_mysql_*
# and comment out the upload_metter_dir definition.
# 2097152 is 2 MB
php_value post_max_size 2097152
<Directory /www/htdocs>
php_value upload_metter 1
php_value upload_max_filesize 2097152
# php_value upload_metter_dir “/tmp/uploadbar”
php_value upload_metter_mysql_host localhost
php_value upload_metter_mysql_user uploaduser
php_value upload_metter_mysql_pass uploadpass
php_value upload_metter_mysql_db uploadbar
</Directory>

– This will activate the progress metter for php uploads inside the /www/htdocs folder
– And it will write progress information to the MySQL Database defined.
– You may optionally comment out the upload_metter_mysql_* lines and use
– file based progress meter by defining the upload_metter_dir. This
– will tell it to write progress informations to /tmp/uploadbar

Alternatively, and better for versatility, you can enable it in a .htaccess file in a dir by dir basis, assuming you have enabled overrides in your httpd.conf:

php_value upload_metter 1
php_value upload_max_filesize 10485760
php_value upload_metter_mysql_host localhost
php_value upload_metter_mysql_user uploaduser
php_value upload_metter_mysql_pass uploadpass
php_value upload_metter_mysql_db uploadbar

3.

A. If you used file based:

mkdir /tmp/uploadbar; chmod 777 /tmp/uploadbar

I have to say that 0777 and /tmp are not the best choises from a
security point of view. It really only needs to be writable by your Apache user (probably nobody).

B. If you used MySQL based:
Log into your MySQL database and from the MySQL prompt:

create database uploadbar;
use uploadbar;
CREATE TABLE `uploadbar` (
`id` varchar(32) NOT NULL default ”,
`time_start` int(11) NOT NULL default ‘0’,
`time_last` int(11) NOT NULL default ‘0’,
`speed_average` int(11) NOT NULL default ‘0’,
`speed_last` int(11) NOT NULL default ‘0’,
`bytes_uploaded` int(11) NOT NULL default ‘0’,
`bytes_total` int(11) NOT NULL default ‘0’,
`files_uploaded` int(11) NOT NULL default ‘0’,
`eta` int(11) NOT NULL default ‘0’,
`last_touched` timestamp(14) NOT NULL,
PRIMARY KEY (`id`)
) TYPE=MyISAM;

4. copy (progress_mysql.php && updemo_mysql.php) or (progress_file.php && updemo_file.php) to your web directory, /www/htdocs in this example.

5. point your browser to the updemo_mysql.php or updemo_file.php script. upload some files. enjoy!

6. Users may quick the download or close the progress bar before it’s finished. We need to clean up the mess.

A. If you are using File based, put this in your crontab:

1 1 * * * find /tmp/uploadbar -type f -mmin +1440 -print | xargs rm -f 1>/dev/null 2>&1

B. If you are using MySql based, put this in your crontab (requires mysql client program to be installed at /usr/bin/mysql):

1 1 * * * let OLD=`date +%s`-86400; QUERY=”DELETE FROM uploadbar WHERE last_touched<FROM_UNIXTIME($OLD)”; /usr/bin/mysql -u uploaduser -puploadpass -h localhost -e “$QUERY” uploadbar 1>/dev/null 2>&1

Ok, so, have it installed? Good. Now you want to make use of it… check the distro tarball, there is an example in there, but you’re going to have to wait for the really cool AJAX example :)



And now, what you’ve all been waiting for, the code base for the super cool AJAX demonstration, as seen at http://www.modphp.org/upload5/.

In previous articles PHP UPLOAD PROGRESS BAR and RECOMPILING PHP ON FC4 WITH PLESK 8.0.1, I hinted at giving away the source code for the AJAX demo, and now it’s time. Going to give it up now.

First, get xajax and drop in your document root. You don’t have to do anything but drop it in place:
wget http://easynews.dl.sourceforge.net/sourceforge/xajax/xajax_0.2.4.zip
unzip xajax_0.2.4.zip
ln -s xajax_0.2.4 xajax

Now, for the code base, the demo consists of these files:

Upload.js
/* Filename: Upload.js
* Purpose: Hold UploadForm, UploadBar static function set
*/

function UploadForm(){}
function UploadBar(){}

// Set these to values of your liking:
UploadForm.containerDivId = ‘form_container_div’; // This div should exist
UploadForm.formDivPrefix = ‘div_form’;
UploadForm.formIdPrefix = ‘the_form’;
UploadForm.formAction = ‘upload.php’;
UploadForm.formTarget = ‘iframe_upload’;
UploadForm.checkStartUploadSleep = 1000; // 1 second

UploadBar.containerDivId = ‘progress_bar_container_div’; // This div should exist
UploadBar.barDivPrefix = ‘div_bar’;
UploadBar.recheckSleep = 1000; // 1 second
UploadBar.progressColor = ‘#00007F’;

// Leave the rest alone
UploadForm.formCount = 0;
UploadForm.queuePointer = 1;

// Hides existing file picker’s DIV/FORM and creates a new DIV/FORM
// for the next upload.
// @returns void
UploadForm.createNewForm = function()
{
if ( UploadForm.formCount > 0 )
document.getElementById(UploadForm.formIdPrefix+UploadForm.formCount).style.display = ‘none’;
UploadForm.formCount++;
var tempDiv = document.createElement(‘div’);
tempDiv.setAttribute(‘id’, UploadForm.formDivPrefix+UploadForm.formCount);
document.getElementById(UploadForm.containerDivId).appendChild( tempDiv );

document.getElementById(UploadForm.formDivPrefix+UploadForm.formCount).innerHTML += ‘<form id=”‘+UploadForm.formIdPrefix+UploadForm.formCount+'” action=”‘+UploadForm.formAction+'” target=”‘+UploadForm.formTarget+'” method=”POST” enctype=”multipart/form-data”><input type=”hidden” name=”UPLOAD_METTER_ID”><input type=”file” name=”filename” onchange=”UploadForm.queueUpload();”></form>’;
}

// Puts the currently active form in queue for upload.
// @requires UploadBar
// @returns void
UploadForm.queueUpload = function()
{
var upload_metter_id = Math.floor(Math.random()*1999999999);
var form = document.getElementById(UploadForm.formIdPrefix+UploadForm.formCount);
var filename = form.filename.value;
if ( filename.lastIndexOf(“\\”) > -1 )
filename = filename.substring(filename.lastIndexOf(“\\”)+1, filename.length);
else if ( filename.lastIndexOf(“/”) > -1 )
filename = filename.substring(filename.lastIndexOf(“/”)+1, filename.length);
filename = filename.replace(/’/g, “”); // strip single quotes
form.UPLOAD_METTER_ID.value = upload_metter_id;
UploadBar.createNewDiv(UploadForm.formCount, filename);
UploadForm.checkStartUpload(UploadForm.formCount, filename);
UploadForm.createNewForm();
}

// Checks if it’s time to start this upload, if so, submit the form.
// otherwise, sleep, then check again.
UploadForm.checkStartUpload = function(formNum, filename)
{
if ( UploadForm.queuePointer == formNum )
{
document.getElementById(UploadForm.formIdPrefix+formNum).submit();
UploadBar.startProgressBar(formNum, filename);
}
else
{
setTimeout(“UploadForm.checkStartUpload(“+formNum+”, ‘”+filename+”‘)”, UploadForm.checkStartUploadSleep);
}
}

/******************************************************************************************/
/************************************UploadBar functions************************************/
/******************************************************************************************/

// Create a new DIV to display the upload progress in:
UploadBar.createNewDiv = function(divNum, filename)
{
var tempDiv=document.createElement(‘div’);
tempDiv.setAttribute(‘id’,UploadBar.barDivPrefix+divNum);
tempDiv.style.width=’100%’;
document.getElementById(UploadBar.containerDivId).appendChild(tempDiv);
var div =document.getElementById(UploadBar.barDivPrefix+divNum);
div.innerHTML = ‘Queued for upload: ‘+filename;
}

UploadBar.startProgressBar = function(divNum, filename)
{
var form = document.getElementById(UploadForm.formIdPrefix+divNum);
var upload_metter_id = form.UPLOAD_METTER_ID.value;
xajax_fetchProgress(upload_metter_id, divNum, filename);
}

UploadBar.drawProgress = function(divNum, filename, total, upl, eta, speed)
{
var str = ”;
var color = ”;
var width = 0;
if ( upl > total ) upl = total;
var percent = total>0 ? Math.round(upl/total*100*10)/10 : 0;
str += ‘<table border=1 WIDTH=”100%” cellpadding=0 cellspacing=0 style=”BORDER-BOTTOM: 0px inset; BORDER-LEFT: 0px inset; BORDER-RIGHT: 0px inset; BORDER-TOP: 0px inset”><tr><td><table border=0 WIDTH=”100%” COLS=”34″><tr>’;
for (var i=0; i<100; i+=3)
{
color = (i<percent) ? ” bgcolor='”+UploadBar.progressColor+”‘ ” : ”;
width = (i+3<100) ? 3 : 100-i;
str += ‘<td ‘+color+’ style=”font-size: 1px;” WIDTH=”‘+width+’%”> </td>’;
}
str += ‘</tr></table></td></tr></table>’;
if ( eta != ‘Finished’ )
eta += ‘ remaining’;
str += ‘<table width=100%><tr><td>’+filename+’, ‘+eta+’ (‘+speed+’KB/sec)</td><td align=right>’+UploadBar.nice_value(upl)+’/’+UploadBar.nice_value(total)+’ (‘+percent+’%)</td></tr></table>’;
document.getElementById(UploadBar.barDivPrefix+divNum).innerHTML = str;
if ( upl < total )
setTimeout(“UploadBar.startProgressBar(“+divNum+”, ‘”+filename+”‘)”, UploadBar.recheckSleep);
else
UploadForm.queuePointer++;
}

UploadBar.nice_value = function( x )
{
if (x < 100) x;
if (x < 1024*10) return Math.round((x/1024)*100)/100 + “KB”; // sprintf(“%.2fKB”, x/1000);
if (x < 1024*1024) return Math.round(x/1000) + “KB”; // sprintf(“%dKB”, x/1000);
return Math.round((x/1024/1024)*100)/100 + “MB”; // sprintf(“%.2fMB”, x/1000/1000);
}

index.php:
<?
require (‘xajax/xajax.inc.php’);

function read_progress_metter($id)
{
$mysql_host = ‘localhost’;
$mysql_user=’uploaduser’;
$mysql_pass=’xxxxxxxxxxxxxxxxxxx’;
$mysql_db=’uploadbar’;

mysql_connect($mysql_host, $mysql_user, $mysql_pass);
mysql_select_db($mysql_db);

$X = array (
“time_start” => 0,
“time_last” => 0,
“speed_average” => 0,
“speed_last” => 0,
“bytes_uploaded” => 0,
“bytes_total” => 1,
“files_uploaded” => 0,
“eta” => 0
);

if (!$id) return FALSE;

$aSQL = “SELECT * FROM uploadbar WHERE id=md5(‘”.addslashes($id).”‘)”;
error_log($aSQL.” from $id”);
$aResult = mysql_query($aSQL);
if ( !$aResult) return $X;
$aNum = mysql_num_rows($aResult);
if ( !$aNum ) return $X;
$aRow = mysql_fetch_assoc($aResult);

while ( list($key, $val) = each($aRow) ) {
$buffer = “$key=$val”;
if (preg_match(‘/(\w+)=(\d+)/’, $buffer, $matches)) {
$X[$matches[1]]=$matches[2];
}
}

return $X;
}

function fetchProgress($id, $divNum, $filename)
{
$objResponse = new xajaxResponse();

$X = read_progress_metter($id);
if ( !$X )
{
$objResponse = new xajaxResponse();
$objResponse->addScript(‘setTimeout(“UploadBar.startProgressBar(‘.divNum.’, \”.$filename.’\’)”, UploadBar.retrySleep);’);
return $objResponse;
}

$sp = $X[‘speed_last’];
if ($sp < 10000) $speed = sprintf(“%.2f”, $sp / 1000);
else $speed = sprintf(“%d”, $sp / 1000);
$sJS = “drawProgressTable({$X[‘bytes_total’]}, {$X[‘bytes_uploaded’]}, ‘”.sprintf(“%02d:%02d”, $X[‘eta’] / 60, $X[‘eta’] % 60 ).”‘, $speed);”;

// get $upl,$total,$speed,$eta;
$upl = $X[‘bytes_uploaded’];
$total = $X[‘bytes_total’];
if ( $X[‘eta’] > 0 )
$eta = sprintf(“%02d:%02d left”, $X[‘eta’] / 60, $X[‘eta’] % 60 );
else
$eta = ‘Finished’;
if ( $upl > $total ) $upl = $total;
$percent = $total>0 ? round($upl/$total*100*10)/10 : 0;

$objResponse = new xajaxResponse();
$objResponse->addScript(“UploadBar.drawProgress($divNum, ‘$filename’, $total, $upl, ‘$eta’, ‘$speed’);”);
return $objResponse;
}

// Instantiate the xajax object. No parameters defaults requestURI to this page, method to POST, and debug to off
$xajax = new xajax();

//$xajax->debugOn(); // Uncomment this line to turn debugging on

// Specify the PHP functions to wrap. The JavaScript wrappers will be named xajax_functionname
$xajax->registerFunction(“fetchProgress”);

// Process any requests. Because our requestURI is the same as our html page,
// this must be called before any headers or HTML output have been sent
$xajax->processRequests();

?>
<HTML>
<HEAD>
<TITLE>Upload test </TITLE>
<?php $xajax->printJavascript(‘xajax/’); // output the xajax javascript. This must be called between the head tags ?>
<script language=”javascript” src=”Upload.js”></script>
</HEAD>
<BODY onLoad=”UploadForm.createNewForm();”>
<iframe id=”iframe_upload” name=”iframe_upload” src=”blank.html” style=”border: 0px; width: 0px; height: 0px;”></iframe>
<P><B>UPLOAD PROGRESS INLINE EXAMPLE</b>
<div id=”form_container_div”></div>
<br>
<div id=”progress_bar_container_div”></div>
<br><br>
Note: You don’t have to wait for the first upload to finish. Try uploading a couple more.
<br><br>
<b>IMPORTANT: 5 MB max… I set my limit at 5 MB to keep bandwidth under control. If you try to upload more than 5 MB, it WILL break the upload progress bar. There is a way to fix this, when I get the time, I’ll come back here and show you how. So, for this demo, just make sure you are uploading a total of less than 5 MB.</b>
</BODY></HTML>

upload.php (this isn’t really required, but you want to do something with the files that are uploaded, right!?)
<?php
if ( count($_FILES) > 0 )
{
foreach ( $_FILES as $file )
{
if ($file[‘size’] > 0 )
{
move_uploaded_file ( $file[‘tmp_name’], “/tmp/upload/”.time().”.”.escapeshellcmd($file[‘name’]) );
print “Thank you for uploading “.htmlspecialchars($file[‘name’]).”<br>\n”;
}
}
}
print “Finished with upload”;
?>

DEMO: http://www.modphp.org/upload5/

sdfsdfsdf

37 thoughts on “PHP Upload Progress Bar”

Comments are closed.