Nginx Upload Progress Module
============================
Introduction
============
nginx_uploadprogress_module is an implementation of an upload progress system, that monitors
RFC1867 POST upload as they are transmitted to upstream servers.
It works by tracking the uploads proxied by Nginx to upstream servers without
analysing the uploaded content and offers a web API to report upload progress in JSON format.
It works because Nginx acts as an accelerator of an upstream server, storing uploaded POST content
on disk, before transmitting it to the upstream server. Each individual POST upload request
should contain a progress unique identifier.
This module is Copyright (c) 2007 Brice Figureau, and is licensed under the BSD license (see LICENSE).
* rbtree and shm_zone code is based on Igor Sysoev limit_zone Nginx module.
* expire header code is based on Igor Sysoev header_filter Nginx module.
The JSON idea and the mechanism idea are based on Lighttpd mod_uploadprogress:
http://blog.lighttpd.net/articles/2006/08/01/mod_uploadprogress-is-back
WARNINGS:
* this software has never been tested under load. It only passed a few lab tests.
* it is not intended to be deployed on *production systems*.
* when compiled with --with-debug, this module will produce high number of log messages.
Installation
============
nginx_uploadprogress_module has been tested with Nginx 0.6.1, 0.6.12 and 0.6.13.
Download the Nginx sources from http://nginx.net/ and unpack it.
To build Nginx, change to the directory which contains the Nginx
sources, and run the configuration script making sure to add the path
to the nginx_uploadprogress_module sources using the --add-module option: ::
$ ./configure --add-module=/path/to/nginx_uploadprogress_module/
Now you can build and install the software:
$ make
and as root:
$ make install
Configuration
=============
Each upload request should be assigned a unique identifier. This unique identifier will be used
to store the request and reference it to report.
This identifier can be transmitted either as a GET argument or as an HTTP header whose name is X-Progress-ID.
upload_progress
+++++++++++++++
:Syntax: upload_progress <zone_name> <zone_size>
:Default: none
:Context: http
:Description:
This directive enables the upload progress module and reserve <zone_size> bytes to the <zone_name> which
will be used to store the per-connection tracking information.
track_uploads
+++++++++++++
:Syntax: track_uploads <zone_name>
:Default: none
:Context: location
:Description:
This directive enables tracking uploads for the current location. Each POST landing in this location will register
the request in the <zone_name> upload progress tracker.
Since Nginx doesn't support yet RFC 1867 upload, the location must be a proxy_pass or fastcgi location.
The POST _must_ have a query parameter called X-Progress-ID (or an HTTP header of the same name) whose value is the
unique identifier used to get progress information. If the POST has no such information, the upload will not be tracked.
report_uploads
++++++++++++++
:Syntax: report_uploads <zone_name>
:Default: none
:Context: location
:Description:
This directive allows a location to report the upload progress that is tracked by track_uploads for <zone_name>.
The returned document is a JSON text with the possible 4 results:
* the upload request hasn't been registered yet or is unknown:
new Object({ 'state' : 'starting' })
* the upload request has ended:
new Object({ 'state' : 'done' })
* the upload request generated an HTTP 413 error (Request entity too large):
new Object({ 'state' : 'error', 'status' : 413 })
* the upload request is in progress:
new Object({ 'state' : 'uploading', 'received' : <size_received>, 'size' : <total_size>})
The HTTP request to this location must have a X-Progress-ID parameter or HTTP header containing a valid
unique identifier of an inprogress upload.
Configuration Example:
+++++++++++++++++++++
http {
# reserve 1MB under the name 'proxied' to track uploads
upload_progress proxied 1m;
server {
listen 127.0.0.1 default;
server_name _ *;
root /path/to/root;
location / {
# proxy to upstream server
proxy_pass http://127.0.0.1;
proxy_redirect default;
# track uploads in the 'proxied' zone
track_uploads proxied;
}
location ^~ /progress {
# report uploads tracked in the 'proxied' zone
report_uploads proxied;
}
}
Usage Example
=============
(based on Lighttd mod_uploadprogress module example):
First we need a upload form:
<form id="upload" enctype="multipart/form-data"
action="/upload.php" method="post"
onsubmit="openProgressBar(); return true;">
<input type="hidden" name="MAX_FILE_SIZE" value="30000000" />
<input name="userfile" type="file" label="fileupload" />
<input type="submit" value="Send File" />
</form>
And a progress bar to visualize the progress:
<div>
<div id="progress" style="width: 400px; border: 1px solid black">
<div id="progressbar"
style="width: 1px; background-color: black; border: 1px solid white">
</div>
</div>
<div id="tp">(progress)</div>
</div>
Then we need to generate the Unique Identifier and launch the upload on submit
action. This also will start the ajax progress report mechanism.
interval = null;
function openProgressBar() {
/* generate random progress-id */
uuid = "";
for (i = 0; i < 32; i++) {
uuid += Math.floor(Math.random() * 16).toString(16);
}
/* patch the form-action tag to include the progress-id */
document.getElementById("upload").action="/upload.php?X-Progress-ID=" + uuid;
/* call the progress-updater every 1000ms */
interval = window.setInterval(
function () {
fetch(uuid);
},
1000
);
}
function fetch(uuid) {
req = new XMLHttpRequest();
req.open("GET", "/progress", 1);
req.setRequestHeader("X-Progress-ID", uuid);
req.onreadystatechange = function () {
if (req.readyState == 4) {
if (req.status == 200) {
/* poor-man JSON parser */
var upload = eval(req.responseText);
document.getElementById('tp').innerHTML = upload.state;
/* change the width if the inner progress-bar */
if (upload.state == 'done' || upload.state == 'uploading') {
bar = document.getElementById('progressbar');
w = 400 * upload.received / upload.size;
bar.style.width = w + 'px';
}
/* we are done, stop the interval */
if (upload.state == 'done') {
window.clearTimeout(interval);
}
}
}
}
req.send(null);
}