In this step we describe how to add an upload progress bar to the file uploads. The Nginx documentation for the upload progress module can be found here. This post builds on the code described in the previous two posts.

Parameterising and generifying the Javascript

In this step we chose to parameterise the Javascript; replacing hard coded element ids with parameters. Evolving the code in this way allows instantiation of multiple file upload components on the same page. Pollution of the Javascript global namespace is minimised to a single shared method - startUpload. Finally the generic Javascript has been moved to a file library.

Nginx configuration changes

Firstly we modify our Nginx configuration. At the bottom of the location fileupload block add:

# file upload progress tracking - 30s is the timeout (progress tracking is
# available 30s after the upload has finished)
# this must be the last directive in the location block.
track_uploads proxied 30s;

and create a new location block to track progress:

# used to report upload progress - defined by the Nginx Upload Progress Module
# see http://wiki.nginx.org/HttpUploadProgressModule
location  /progress {
    report_uploads proxied;
}

This location block is key to reporting upload progress; once the upload has started, we poll /progress to receive updates on the upload status.

Within the main http block of the Nginx configuration add:

upload_max_file_size 10m;
upload_progress proxied 10m;

and ensure the sizes set match the client_max_body_size directive:

client_max_body_size 10m;

Seaside modifications

According to the upload progress documentation:

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

Back in our Smalltalk class we add a helper method to generate a unique upload id:

xProgressId
	^xProgressId ifNil: [ xProgressId := WAKeyGenerator current keyOfLength: 15 ]

and a reset:

resetXProgressId
	xProgressId := nil

In the form we modify the hiddenInput to work twice as hard for us. To the callback processing we add a call to #resetXProgressId, and we store #xProgressId in the hiddenInput’s value:

renderUploadFormOn: html
	| formId |

	html form
		multipart;
		attributeAt: 'target' put: (iframeId := html nextId);
		id: (formId := html nextId);
		with: [
			| fileUploadField fileUploadId progressBarId notificationId |

			"The Nginx handler stores the file uploaded in a specified location and adds POST parameters
			for the filename, the size, the file type etc.
			These parameters are then interpreted by the server form handling code.
			Note: the file uploaded parameter (the file contents) is removed from the form variables."

			fileUploadField := html fileUpload
				id: (fileUploadId := html nextId);
				callback: [ :file |
					"should never get here as Nginx's upload file model removes the
					uploaded parameter from the form variables in the post request, so
					Seaside never fires the callback"
					self error: 'Check your Nginx configuration' ].

			html hiddenInput
				id: (hiddenXProgressId := html nextId);
				value: self xProgressId;			
				callback: [:val | | uploadFieldName |
					"what name did Seaside assign to the file upload form field?"
					uploadFieldName := fileUploadField attributeAt: 'name'.
					self storeUploadedFile: uploadFieldName.
					self resetXProgressId.
					self renderIFrameResponse ].		

			progressBarId := html nextId.
			notificationId := html nextId.
			uploadedFilesContainerId := html nextId.

        		html button
				bePush;
				onClick: ((JSStream on: 'window')
					call: 'startUpload'
					withArguments: (Array
						with: (html jQuery id: fileUploadId)
						with: (html jQuery id: formId)
						with: (html jQuery id: hiddenXProgressId)
						with: (html jQuery id: progressBarId)
						with: (html jQuery id: notificationId)));
				with: 'Upload File'.

			self renderUploadProgressBarOn: html progressBarId: progressBarId notificationId: notificationId ].

	self renderHiddenIFrameOn: html

Note that the hard coded ids in renderUploadFormOn: in the previous steps have been replaced with dynamically assigned local and instance variables which are used in other Javascript handlers to reference the form elements.

The form’s submit button has changed to a push button with an #onClick: handler which calls a modified startUpload with parameters of JQuery references to various fields. Alternatively you could add an #onChange: handler to the fileUpload control and call startUpload from there, dispensing with the need for the button.

The Javascript function startUpload is defined as:

function startUpload(fileUploadField, form, xProgressField, progressBar, notificationField) {
var filename = fileUploadField.val();
if (!filename && filename.length) {
	var xProgressId = xProgressField.val();
	var formActionUrl = form.attr("action");
	/* add X-Progress-ID parameter if its not already there otherwise replace it */
	if (formActionUrl.indexOf ("X-Progress-ID") == -1) {
		formActionUrl += "&X-Progress-ID=" + xProgressId;
	} else {
		formActionUrl = formActionUrl.replace(/^(.*X-Progress-ID=)(.*)(&.*)?$/, "$1" +xProgressId + "$3");
	}
	form.attr("action", formActionUrl);
	form.submit();

	fileUploadField.val("");
	}
}

On the first upload the javascript appends the X-Progress-ID to the URL. On subsequent uploads, the regular expression replaces the X-Progress-ID parameter’s value with the current value set in the xProgressField hidden field.

We also add a line to fileUploadedCallbackJSOn: to set a new value of #xProgressId=after a file has been upload:

fileUploadedCallbackJSOn: html
	^
'$("#', uploadedFilesContainerId, '").replaceWith($("#',iframeId,'").contents().find("#', uploadedFilesContainerId, '"));
$("#', hiddenXProgressId, '").val($("#',iframeId,'").contents().find("#newXProgressId").val())'

this method is called from:

renderHiddenIFrameOn: html
	html iframe
		name: iframeId;
		id: iframeId;
		style: 'position:absolute;top:-1000px;left:-1000px'.

	html document addLoadScript:  ((html jQuery id: iframeId) onLoad: (self fileUploadedCallbackJSOn: html))

with the iframes onload handler being set dynamically at page load.

Adding the progress bar

We’ve created the infrastructure for the upload progress bar, now we add the progress bar:

renderUploadProgressBarOn: html progressBarId: progressBarId notificationId: notificationId
	html div
		id: progressBarId;
		style: 'display: none'; "It starts hidden"
		script: (html jQuery this progressbar value: 0).

	html div id: notificationId

Javascript support for polling /progress and reporting on status is added to startUpload. The code is now generic and I’ve chosen to move the method into a file library which is then referenced in updateRoot:

updateRoot: anHtmlRoot
	super updateRoot: anHtmlRoot.
	anHtmlRoot javascript url: NAFileUploadStep3FileLibrary / #startUploadJs

The associated Javascript now becomes a little more complex from version presented above:

var startUpload = function (fileUploadField, form, xProgressField, progressBar, notificationField) {
	var filename = fileUploadField.val();

	if (filename && filename.length) {
		var xProgressId = xProgressField.val();
		var formActionUrl = form.attr("action");
		/* add X-Progress-ID parameter if its not already there otherwise replace it */
		if (formActionUrl.indexOf ("X-Progress-ID") == -1) {
			formActionUrl += "&X-Progress-ID=" + xProgressId;
		} else {
			formActionUrl = formActionUrl.replace(/^(.*X-Progress-ID=)(.*)(&.*)?$/, "$1" +xProgressId + "$3");
		}
		form.attr("action", formActionUrl);
		form.submit();

		fileUploadField.val("");
		progressBar.progressbar({value:0});
		progressBar.css("display", "block");
		var intervalId = window.setInterval(function(){
			updateBar(progressBar, xProgressId, notificationField, intervalId)
			}, 1000);
	}

	var updateBar = function(progressBar, xProgressId, notificationField, intervalId) {
		$.ajax({
			url: "/progress",
			beforeSend: function (xhr) {
				xhr.setRequestHeader("X-Progress-ID", xProgressId);
			},
			success: function (data, textStatus, xhr) {
				var upload = eval(data);

				if (upload.state == "done" || upload.state == "uploading") {
					var percentageComplete = Math.floor(upload.received * 100 / upload.size);
					progressBar.progressbar({ value: percentageComplete });
					if (upload.received != upload.size) {
						setUploadStatus("Uploading: " + percentageComplete + "%");
					} else {
						setUploadStatus("Uploaded: processing");
					}
				}
				if (upload.state == "starting") {
					setUploadStatus("Starting upload");
				}
		        	if (upload.state == "done") {
					clearUploadProgress();
		        	}
			},
			error: function(xhr, textError) {
				setUploadStatus("Error: " + textError);
				window.clearTimeout (intervalId);
			}
		});

		var clearUploadProgress = function() {
			window.clearTimeout (intervalId);
			progressBar.css ("display", "none");
			setUploadStatus ("");
		}

		var setUploadStatus = function(statusText) {
			notificationField.html ("<div class=''status''>" + statusText + "</div>");
		}
	}
}

The script polls /progress once a second, using a low level ajax call which allows the X-Progress-ID parameter to be set:

beforeSend: function (xhr) {
	xhr.setRequestHeader("X-Progress-ID", xProgressId);
}

The success callback interprets the response from /progress and updates the progress bar and the text status.

Understanding how the progress bar receives updates:

Form submission creates an http-post which includes the X-Progress-ID parameter:

we then poll /progress once a second passing the X-Progress-ID parameter:

which results in a response of the form:

The response is evaluated:

var upload = eval(data);

and can be used to calculate the percentage complete:

var percentageComplete = Math.floor(upload.received * 100 / upload.size);

Download the code

The code described above is contained in NAFileUploadStep3 and can be downloaded from the repository http://www.squeaksource.com/fileupload

Next Step

Step 4: File upload as a pluggable component.