Monitoring File Uploads using Ajax and PHP

  • articles
  • Licence:
  • Author:
  • 05/03/2015
Download , 7 Downloads

About Monitoring File Uploads using Ajax and PHP

Monitoring File Uploads using Ajax and PHP

Quentin Zervaas

Introduction

Because of the limitations of HTTP, it is difficult to monitor the status of files as they are uploaded via HTML forms. While other programming languages have built-in methods to monitor file uploads, PHP does not. This article shows how to implement such a solution in PHP.

In order to achieve this, the APC (Alternative PHP Cache) PHP extension is required, as well as PHP 5. Specifically, APC 3.0.13 or newer and PHP 5.2 or newer are required for the code in this article to work. We will cover installation of APC in the next section. It is assumed you already have a working PHP 5.2 installation.

In this article we will develop a solution that will allow users to upload a file from their computer using HTML forms. We will then determine the progress of the upload while it is in progress using Ajax, and display the status to the user.

In addition to using PHP, we will also be using the Prototype JavaScript library (version 1.6.0), which you can download for free from http://www.prototypejs.org.

The steps we will follow in this article are as follows:

  • Installation of the APC extension for PHP.
  • How to monitor uploads using APC.
  • Creating a PHP class to manage file uploads.
  • Implementing a traditional file uploader.
  • Extending the uploader to display upload status.

Installing APC

Before we can begin implementing our file upload monitor we must install the Alternative PHP Cache extension (APC). If you are using a UNIX based operating system, the simplest way to install APC is to use the PECL command line installer.

Specifically, you should use the following command:

# pecl install apc

Executing this command should result in the necessary files being downloaded, compiled and installed. You will then need to modify your php.ini file to load the extension. If you built PHP from source the typical location for php.ini is /usr/local/lib/php.ini. If you use FreeBSD and used ports to install PHP then php.ini can be found in /usr/local/etc/php.ini.

extension=apc.so

Note: You may need to specify the full path to this file (depending on your configuration).

If you are instead running PHP on Windows, the extension DLL file should already exist – it simply needs to be activated in the php.ini (typically in c:\windows\php.ini) file using a similar command to above:

extension=php_apc.dll

Enabling File Upload Monitoring

Next the feature of APC that monitors file uploads must be activated. To do so, the apc.rfc1867 setting must be switched on.

Note: RFC 1867 is the document that describes how HTTP file uploads work. Refer to http://www.ietf.org/rfc/rfc1867.txt for more details.

Simply add the following line to your php.ini file.

apc.rfc1867 = On

Restarting Your Web Server

Once the changes described in this section have been made you must restart your web server. To do so you can type apachectl restart in UNIX or simply restart the Apache service in Windows.

After the web server has been restarted, confirm the changes by viewing the phpinfo() output for your web server. If all is working, there will be an APC section on this page (in which the apc.rfc1867 setting will be switched to On).

Using APC to Monitor a File Upload

Assuming you have installed APC and enabled the apc.rfc1867 setting (as described in the previous section), the APC upload monitoring will be automatically activated when a file upload occurs which also contains a form value called APC_UPLOAD_PROGRESS. The value of this field is a value of your own choosing that you can use to identify the upload.

For example, the following form could be used to activate upload monitoring:

<form method=”post” action=”upload.php” enctype=”multipart/form-data”>

<div>

<input type=”hidden” name=”APC_UPLOAD_PROGRESS” value=”1234″ />

<input type=”file” name=”myFile” />

<input type=”submit” value=”Upload file…” />

</div>

</form>

Note: The APC_UPLOAD_PROGRESS form element must appear in the form before the file input.

When this form is submitted, the file upload begins as usual. APC can now be polled to monitor the upload status (this must be done in a separate request, since the primary request is being used to fulfil the upload – we’ll deal with this later using Ajax).

To retrieve cache data from APC, the apc_fetch() function is used. We use the value of the APC_UPLOAD_PROGRESS form element when calling this function. In the above example, the value of the field is 1234. We must prefix this value with upload_, meaning in order to fetch the upload information about upload 1234 we use apc_fetch(‘upload_1234′).

If no cache data is found for the key passed to apc_fetch() then false is returned. On the other hand, if data is found (as it should be if everything is configured correctly), an array will be returned. This return array contains the following useful values:

  • done: A Boolean value indicating whether or not the upload has completed.
  • total: The total file size in bytes of the file being uploaded.
  • current: The number of bytes that have been completed.

These values allow us to determine how much of the upload has completed, as demonstrated by the following code.

<?php

$upload = apc_fetch(‘upload_1234′);

 

if ($upload) {

if ($upload[‘done’])

$percent = 100;

else if ($upload[‘total’] == 0)

$percent = 0;

else

$percent = $upload[‘current’] / $upload[‘total’] * 100;

}

?>

Hard-coding the ID value for the upload obviously is neither ideal nor a practical solution, so part of the solution we implement in the next section will use code similar to above in conjunction with PHP sessions. Additionally, we will also handle the case where APC isn’t installed so your code will work across all servers.

Creating the PHP FileUploader Class

Now that we have an understanding of how to use upload monitoring with APC, we can create a new PHP class to handle file uploads and to return this upload information. In the coming sections we will write code to interface this class, but for now we will focus on the development of the class.

We are going to use PHP sessions to store upload information. The reason we do this is so the script will complete correctly even if APC upload monitoring is not enabled. When we develop the Ajax part of this solution, the status monitoring script will be continually polling the status script. Once the upload has completed we need this loop to finish. For now though, let’s focus on developing the PHP class.

The code created in this section will be broken down by each method. The full FileUploader.php file can be downloaded from the article file list.

Initializing FileUploader

The first thing we do is to create the class constructor. Because this solution uses sessions, we call session_start() in the constructor. This isn’t necessarily an ideal solution: your application may already initiate sessions but we just do it here so the code for this article is all self-contained.

Here is the code to define the class, as well as its constructor.

<?php

class FileUploader

{

const SESSION_KEY = ‘__upload_status';

const ID_KEY     = ‘APC_UPLOAD_PROGRESS';

 

public function __construct()

{

session_start();

 

if (!array_key_exists(self::SESSION_KEY, $_SESSION)) {

$_SESSION[self::SESSION_KEY] = array();

}

}

 

// … other code

}

?>

We are going to store upload status session data as an array, with each element corresponding to a single file upload. We will store this data in a single element in the $_SESSION array. We define the name of this using the SESSION_KEY constant. Doing this using a constant allows us to easily change the name of this as required in the future without having to update all points in the code.

Additionally, we define the ID_KEY constant, which refers to the name of the form element that APC looks for in order to define an upload’s ID. Once again this is unlikely to change but makes for cleaner code.

Checking for APC Upload Monitoring Support

The next step is to determine whether or not we have the ability to monitor the upload status. Doing this ensures that our code will work on all platforms regardless of whether APC is installed. For APC upload monitoring to work the following conditions must be met:

  • The APC module must be loaded
  • The apc_fetch() function must exist (which it should if APC is loaded, but we’ll double check anyway).
  • APC must be enabled (upload monitoring is disabled if APC is disabled, although this wasn’t the case in APC 3.0.13).
  • The rfc1867 setting must be enabled.

In order to check each of these conditions we can use the following code:

<?php

class FileUploader

{

// … other code

 

public static function CanGetUploadStatus()

{

if (!extension_loaded(‘apc’))

return false;

 

if (!function_exists(‘apc_fetch’))

return false;

 

return ini_get(‘apc.enabled’) && ini_get(‘apc.rfc1867′);

}

 

// … other code

}

?>

Retrieving the Upload Status for a File

Next we implement a method to retrieve the upload status for a given file ID. This method works first by checking for existing status data in the session. If there isn’t, the data is initialized. Next the code calls apc_fetch() to retrieve the upload information as described in the previous section. The session is then updated and the data is returned.

This code as described is shown below. The comments describe exactly what is occurring in the getUploadStatus() method.

<?php

class FileUploader

{

// … other code

 

public function getUploadStatus($id)

{

// sanitize the ID value

$id = preg_replace(‘/[^a-z0-9]/i’, ”, $id);

if (strlen($id) == 0)

return;

 

// ensure the uploaded status data exists in the session

if (!array_key_exists($id, $_SESSION[self::SESSION_KEY])) {

$_SESSION[self::SESSION_KEY][$id] = array(

‘id’       => $id,

‘finished’ => false,

‘percent’ => 0,

‘total’   => 0,

‘complete’ => 0

);

}

 

// retrieve the data from the session so it can be updated and returned

$ret = $_SESSION[self::SESSION_KEY][$id];

 

// if we can’t retrieve the status or the upload has finished just return

if (!self::CanGetUploadStatus() || $ret[‘finished’])

return $ret;

 

// retrieve the upload data from APC

$status = apc_fetch(‘upload_’ . $id);

 

// false is returned if the data isn’t found

if ($status) {

$ret[‘finished’] = (bool) $status[‘done’];

$ret[‘total’]   = $status[‘total’];

$ret[‘complete’] = $status[‘current’];

 

// calculate the completed percentage

if ($ret[‘total’] > 0)

$ret[‘percent’] = $ret[‘complete’] / $ret[‘total’] * 100;

 

// write the changed data back to the session

$_SESSION[self::SESSION_KEY][$id] = $ret;

}

 

return $ret;

}

 

// … other code

}

?>

Handling File Uploads

To complete the FileUploader PHP class we will implement a method for actually handling the upload web form. Since this tutorial isn’t focus specifically on how to handle file uploads the code has been somewhat simplified, but in essence this method will simply save an uploaded file to the specified location.

In addition to saving the file to the file system, this method will also update the upload status data in the session to indicate that the file upload has complete. If you refer to the getUploadStatus() method, there is a line of code that checks the finished array key. We use this to help the Ajax portion of our code know when to stop polling for the upload status.

<?php

class FileUploader

{

// … other code

 

public function upload($key, $path)

{

// ensure the given file has been uploaded

if (!isset($_FILES[$key]) || !is_array($_FILES[$key]))

return false;

 

$file = $_FILES[$key];

$id   = $_POST[self::ID_KEY];

 

// only proceed if no errors have occurred

if ($file[‘error’] != UPLOAD_ERR_OK)

return false;

 

// write the uploaded file to the filesystem

$fullpath = sprintf(‘%s/%s’, $path, basename($file[‘name’]));

if (!move_uploaded_file($file[‘tmp_name’], $fullpath))

return false;

 

// update the session data to indicate the upload has completed

$size = filesize($fullpath);

 

$_SESSION[self::SESSION_KEY][$id] = array(

‘id’       => $id,

‘finished’ => true,

‘percent’ => 100,

‘total’   => $size,

‘complete’ => $size

);

 

return true;

}

}

?>

In order to focus specifically on monitoring upload status, the error handling in this code has been somewhat simplified. Additionally, the code described here doesn’t deal with duplicate files or do any checking on the type of file.

This completes the FileUploader.php file. You can download the complete file from the article file listing.

Creating the File Upload Form

The next step is to create the file upload form, which simply consists of a standard HTML form that includes a file input. The JavaScript code we will develop later will create a hidden iframe that this form will submit to, however in order to keep this form accessible for users that don’t have JavaScript enabled we don’t need to include this iframe in this HTML document.

This code loads the prototype.js file, which is available for download from http://prototypejs.org. The version of Prototype used in this article 1.6.0. Additionally, the code loads FileUploader.js and upload.js which we will create later in this article.

We load the FileUploader.php in this file, allowing us to refer to the ID_KEY constant (which corresponds to APC_UPLOAD_PROGRESS). Obviously you could just hardcode this value – I’ve just done it this way to keep the code tidy with no “magic values”.

<?php

require_once(‘FileUploader.php’);

?>

<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Strict//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd”>

<html xmlns=”http://www.w3.org/1999/xhtml” lang=”en” xml:lang=”en”>

<head>

<title>File Uploader</title>

<meta http-equiv=”Content-Type” content=”text/html; charset=iso-8859-1″ />

<script type=”text/javascript” src=”prototype.js”></script>

<script type=”text/javascript” src=”FileUploader.js”></script>

<script type=”text/javascript” src=”upload.js”></script>

</head>

<body>

<form method=”post” action=”upload.php” enctype=”multipart/form-data” id=”theForm”>

<div>

<input type=”hidden” name=”<?php echo FileUploader::ID_KEY ?>” value=”” />

<input type=”file” name=”myFile” />

<input type=”submit” name=”submit” value=”Upload file…” />

</div>

</form>

 

<div id=”status” style=”display: none”>

Upload status: #{complete}/#{total} (#{percent}%)

</div>

</body>

</html>

This HTML document is completed by the inclusion of a div called #status. This element is used to show the upload status data. At this stage it includes only placeholders for where the values will appear. We will use the Template class that comes with Prototype to insert the values into the placeholders. By default, we hide this element.

Note: In order to use the show() and hide() methods of Prototype we must hide the element using an inline style tag, rather than using a CSS rule such as #status { display : none; }. Alternatively, you could leave this element to be displayed then hide it once the page has loaded using JavaScript.

Handling the File Upload

In the index.php file created in the previous section, the form is submitted to a file called upload.php. The upload.php file is simply used to make a call to the upload() method of the FileUploader PHP class.

The other thing we do in this file is specify the directory where the file should be saved. Typically your own upload functionality will behave slightly different, but for our purposes this will suffice.

Note: This directory must be writable by the web server.

<?php

require_once(‘FileUploader.php’);

 

$path = realpath(dirname(__FILE__) . ‘/uploads’);

 

$fu = new FileUploader();

$fu->upload(‘myFile’, $path);

?>

The first argument passed to upload() is the name of the file form element in index.php, while the second argument is the path where the file should be saved.

Retrieving a File’s Upload Status

Next we write another PHP script, which is used to send back the status of a file as it is being uploaded. Essentially this is an interface to the getUploadStatus() method of the FileUploader PHP class.

This script will be utilized by the Ajax code we will develop later in this chapter.

As we saw earlier in this article, the getUploadStatus() method returns an array containing the upload data for the file with the given ID. We are going to send this data back to the browser using JSON (JavaScript Object Notation).

Since PHP 5.2.0, data can easily be encoded into JSON format using the json_encode() method. The HTTP Content-type header associated with JSON data is application/json.

<?php

require_once(‘FileUploader.php’);

 

$id = isset($_POST[‘id’]) ? $_POST[‘id’] : 0;

$fu = new FileUploader();

 

$status = $fu->getUploadStatus($id);

 

header(‘Content-type: application/json’);

echo json_encode($status);

?>

Developing a JavaScript Monitoring Class

Now that all required PHP code for this article has been covered we can implement the required JavaScript code. Note that the code we develop here makes extensive use of the advancements provided by Prototype. You can download the prototype.js file from http://prototypejs.org. Version 1.6.0 is used in this article. Note that this article doesn’t cover extensively how Prototype works, however I will try to keep the explanations as simple as possible.

The code in this section will be broken up once again by method, while the complete file can be downloaded from the article file list.

Initializing the FileUploader JavaScript Class

We begin the class by creating several variables that are used in the class. Firstly, we create a variable called ID_KEY to hold the name of the element required for APC upload monitoring. Once again this is just to keep the code clean and to avoid using magic values. We also define the URL where the status of an uploaded file can be retrieved from in statusUrl variable.

Please read the extensive comments to understand how the code works. The important points regarding Prototype that the comments do not describe include:

  • create() is how classes are created using Prototype. More information can be found at http://prototypejs.org/api/class/create.
  • When creating a class with Prototype, the constructor function is called initialize().
  • Events can be observed with Prototype using the observe() The first argument is the name of the event without the ‘on’ (so onmouseover would be mouseover). The second argument is the handler function. This code binds the handler function to the instance of FileUploader. More information on function binding with Prototype can be found at http://prototypejs.org/api/function.
  • Since Prototype 1.6.0, new DOM elements can easily be created using the Element More information can be found at http://prototypejs.org/api/element.

var FileUploader = Class.create({

 

ID_KEY         : ‘APC_UPLOAD_PROGRESS’,

statusUrl     : ‘status.php’,

pollDelay     : 0.5,

 

form           : null, // HTML form element

status         : null, // element where the upload status is displayed

statusTemplate : null, // Prototype Template object

idElement     : null, // element that holds the APC upload ID

iframe         : null, // iframe we create that form will submit to

 

initialize : function(form, status)

{

// initialize the form and observe the submit element

this.form = $(form);

this.form.observe(‘submit’, this._onFormSubmit.bindAsEventListener(this));

 

// create a hidden iframe

this.iframe = new Element(‘iframe’, { name : ‘_upload_frame’ }).hide();

 

// make the form submit to the hidden iframe

this.form.appendChild(this.iframe);

this.form.target = this.iframe.name;

 

// initialize the APC ID element so we can write a value to it later

this.idElement = this.form.getInputs(null, this.ID_KEY)[0];

 

// initialize the status container

this.status = $(status);

 

// create a template based on the HTML inside the status container

this.statusTemplate = new Template(this.status.innerHTML);

 

// clear the status template

this.status.update();

},

 

// … other code

});

Note: Because each method is an element of a JavaScript object, they must be separated by commas. Therefore, each method must end in a comma after the closing brace, except for the final method. You will be able to see this clearly in the full class file.

Utility Methods

Next we will define two utility methods that help with our implementation. The first method is used to generate an ID for the upload file. This value is used when calling the upload.php file. We will generate this value based on the current date and time, thereby allowing a user to upload a second file after their first one has completed.

The other method we define is used to “pause” our code as required. Since we are going to periodically poll the status script we need a way to time this polling.

var FileUploader = Class.create({

 

// … other code

 

generateId : function()

{

var now = new Date();

return now.getTime();

},

 

delay : function(seconds)

{

var ms   = seconds * 1000;

var then = new Date().getTime();

var now = then;

 

while ((now – then) < ms)

now = new Date().getTime();

},

 

// … other code

});

Handling the Upload Form Submission

In the initialize() method, the code is instructed to call the _onFormSubmit() method when the user submits the upload form. We will now implement this method. The goal with this method is to firstly generate a unique ID for this upload and write it to the form before it is submitted, then to continue to monitor the upload until it has completed.

We will use the generateId() method just created and write the returned value to the idElement variable we created in initialize(). The _onFormSubmit() method concludes by calling _monitorUpload(). We will define that method shortly.

var FileUploader = Class.create({

 

// … other code

 

_onFormSubmit : function(e)

{

var id = this.generateId();

 

this.idElement.value = id;

this._monitorUpload(id);

},

 

// … other code

});

Monitoring the File Upload

We can now implement the actual upload monitoring Ajax code. As we just saw, this is performed in a method called _monitorUpload(). When this method is called, an Ajax request is initiated. The status.php script created earlier in this article is requested using the ID passed to _monitorUpload(). This results in the JSON data for the given ID being returned.

An Ajax request is initiated in Prototype by instantiating the Ajax.Request class. The first argument is the URL being requested and the second argument is the required arguments. By default the request method is post, and we define the post data by specifying the parameters element in the options object.

The other option specified is the onSuccess argument. The value for this setting is the function that is called once the Ajax request successfully completes. Once again we use function binding, which you can read more about at the Prototype web site (http://prototypejs.org/api/function).

var FileUploader = Class.create({

 

// … other code

 

_monitorUpload : function(id)

{

var options = {

parameters : ‘id=’ + id,

onSuccess : this._onMonitorSuccess.bind(this)

};

 

new Ajax.Request(this.statusUrl, options);

},

 

_onMonitorSuccess : function(transport)

{

var json = transport.responseJSON;

 

this.status.show();

this.status.update(this.statusTemplate.evaluate(json));

 

if (!json.finished) {

this.delay(this.pollDelay);

this._monitorUpload(json.id);

}

}

});

Once the Ajax request initiated in _monitorUpload() successfully completes, the _onMonitorSuccess() method is called. The Ajax.Request class automatically passes the request data as the first argument (which we have called transport).

From Prototype 1.6.0 we can access returned JSON data using the responseJSON element.

Note: In previous versions of Prototype you would need to use a line such as var json = transport.responseText.evalJSON(true) instead.

Once we have access to this JSON data we use it to update the status container. We firstly use the Template object to generate HTML with the values substituted in, then call update() to insert this HTML into the status container.

The code concludes by checking whether or not the upload has finished. If it has not yet finished, it pauses briefly then calls _monitorUpload() again. This will continue to happen until the upload completes.

The full code listing for this class can be downloading from the article file list.

Instantiating the FileUploader JavaScript Class

The final step in making this code work is to actually instantiate the FileUploader JavaScript class. To do this we implement the upload.js file that was loaded in index.php.

All this code does is wait until the page has finished loading then instantiates the class with the appropriate parameters (the ID values for the form and the status container).

Event.observe(window, ‘load’, function() {

new FileUploader(‘theForm’, ‘status’);

});

Conclusion

At this stage all of the required code has been implemented, meaning you should now be able to upload a file using the index.php file. Assuming your code all works correctly, the status of the upload should be displayed while it is uploading. Note that small files may upload so quickly you don’t see any real-time updates.

While the code we implemented in this article does gloss over a few things (such as informative error handling), hopefully it does demonstrate our primary goal: how to monitor the status of a file as it is being uploaded.

The key points covered in this article included:

  • How to install APC and configure it correctly for upload monitoring
  • How to retrieve upload data from APC
  • How to develop a class using Prototype JavaScript library
  • How to send JSON data with PHP and receive it using Ajax

 

Similar to Monitoring File Uploads using Ajax and PHP