Webiny file upload with HTML5 and AJAX using PHP streams

About the author

During the development of Webiny we have tried out and changed several ways of how files are uploaded to the server. Each time we faced a different problem, from usability and security to memory usage problems.
The first attempt was to give a user several file input fields where he can select files one by one. Here we faced a problem where he needed to click several times for each file he wanted to upload, which was a very bad approach. 
Our second attempt was using a flash based file upload plugin called SWFUpload which proved a quite good solution, if you had flash player installed. Here we have had several issues, and one was problematic in particular. Namely flash doesn't inherit current session, instead it creates a new session, which caused the loss of current users login cookie. This problem could be avoided by passing the current session id and login cookie through POST parameters with each file upload, but that could also be a potential security issue. Therefore we decided that SWFUpload isn't also a good solution. So there was nothing else left but to develop our own file upload mechanism.

The Technology

HTML5 has been around for quite a while now. Several systems already use it, but there are many more that don't, mostly because it's not compatible with older browsers (read Internet Explorer). When it comes to Webiny, we have a rule for system administration that all code must be compatible with the latest version of browsers, so no backwards compatibility, in the means of HTML, CSS and JS. This rule had to be applied so we could use new cool stuff that came with HTML5 & CSS3. 

The Code

We have developed a JavaScript object that attaches certain observers to a defined file-input field. Those observers then trigger some actions and call callback functions that control the file upload process. But in this blog post, we'll only concentrate on how to upload files using XMLHttpRequest (v2) and HTML5.  

HTML

The HTML code is essential, just a normal input field, with an optional "multiple" attribute. The "multiple" attribute was introduced in HTML5 and allows the user to select multiple files by holding SHIFT or CTRL key while selecting files on his computer.


JavaScript

The upload function uses FileReader class, which is included in HTML5 API and supported on all modern browsers. The FileReader is responsible for reading the file contents. Once the file is read, we start the upload process using XHR request. Since we stream the file contents to the server, we can track the upload progress and read the file contents from the input stream on the server side. In order for all that to be possible, we have to send the POST parameters in an binary format. Firefox browser supports sendAsBinary function on XHR objects, while other browsers don't, so we had to create a function for streaming binary data that would work on all browsers, thus the 'mySendAsBinary' function in the code below. While file is uploading, browser triggers a callback function called 'progress' on xhr.upload object. Inside that function you have the total size of file that is currently uploading, and the size that is already sent to the server. With those two numbers you can get the upload percentage. 

function upload(fileInputId, fileIndex)
		{
			// take the file from the input
			var file = document.getElementById(fileInputId).files[fileIndex]; 
			var reader = new FileReader();
			reader.readAsBinaryString(file); // alternatively you can use readAsDataURL
			reader.onloadend  = function(evt)
			{
					// create XHR instance
					xhr = new XMLHttpRequest();
					
					// send the file through POST
					xhr.open("POST", 'upload.php', true);

					// make sure we have the sendAsBinary method on all browsers
					XMLHttpRequest.prototype.mySendAsBinary = function(text){
						var data = new ArrayBuffer(text.length);
						var ui8a = new Uint8Array(data, 0);
						for (var i = 0; i < text.length; i++) ui8a[i] = (text.charCodeAt(i) & 0xff);
			
                        if(typeof window.Blob == "function")
			            {
			                 var blob = new Blob([data]);
			            }else{
			                 var bb = new (window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder)();
			                 bb.append(data);
			                 var blob = bb.getBlob();
			            }

						this.send(blob);
					}
					
					// let's track upload progress
					var eventSource = xhr.upload || xhr;
					eventSource.addEventListener("progress", function(e) {
						// get percentage of how much of the current file has been sent
						var position = e.position || e.loaded;
						var total = e.totalSize || e.total;
						var percentage = Math.round((position/total)*100);
						
						// here you should write your own code how you wish to proces this
					});
					
					// state change observer - we need to know when and if the file was successfully uploaded
					xhr.onreadystatechange = function()
					{
						if(xhr.readyState == 4)
						{
							if(xhr.status == 200)
							{
								// process success
							}else{
								// process error
							}
						}
					};
					
					// start sending
					xhr.mySendAsBinary(evt.target.result);
			};
		}

This is only the essential code needed for you to understand how this works. You can easily expand it with the functionality to add the validation of file types, file size, additional callbacks like when the upload has started, when a new file added to the queue and similar.

In Webiny file upload we have integrated the following callbacks:

  • onFileAdded - called when a file is added to the upload queue
  • onQueueInsertDone - called when all files are added to the queue
  • onClearQueue - called after all files have been cleared from the queue
  • onFileDiscarded - called when a file has been discarded from the queue because of invalid extension or its size
  • onStartUpload - called when the startUpload() has been issued
  • onFileUploadStart - called when a specific file has started uploading
  • onUploadProgress - called several time while a specific file is uploading
  • onFileUploaded - called when a specific file was uploaded
  • onQueueDone - called when all files from the queue have been uploaded 

 

Hope this gives you additional ideas of what can be achieved using this method for uploading files. Here is a screenshot of how the file uploader looks on Webiny.

Server Side (PHP)

The code on the server side is quite simple, just read the data from the input stream and save it somewhere on the server.

// read contents from the input stream
$inputHandler = fopen('php://input', "r");
// create a temp file where to save data from the input stream
$fileHandler = fopen('/tmp/myfile.tmp', "w+");

// save data from the input stream
while(true) {
	$buffer = fgets($inputHandler, 4096);
	if (strlen($buffer) == 0) {
		fclose($inputHandler);
		fclose($fileHandler);
		return true;
	}

	fwrite($fileHandler, $buffer);
}

// done

You can additionally improve this function by passing desired file-name, under which it should be saved, from JavaScript. Also when the upload is done, a JSON response is suitable so you can easily show the results on client side.

Why streams and not just file_put_contents?

Using PHP input:// stream the memory footprint on the server is <1MB, on an upload file size of 12MB, compared to normal file upload (without binary streams) where memory footprint reaches over 60MB on the same file size. If you don't want to use streams, but you are concerned about the memory usage, one good alternative is to use an event based server like nginx or lighttpd which would outperform apache when it comes to file upload. 

Subscribe


Don't worry we will not use your email for spam