Monday, March 9, 2009

Safari 4 Multiple Upload With Progress Bar

Update - I slightly modified the demo page adding another bar, the one for the current file. I'll update the zip asap but all you need is a copy and paste of the JS in the index. Apparently multiple should be set as multiple, rather than true ... in any ... it works as long as it is an attribute.


This one from Ajaxian has been one of the best news I could read about HTML5 specs and browsers evolution.
I could not wait to download Safari 4 and test instantly this feature.
I have always been interested in this possibility, both progress bar, plus multiple uploads and that's why I would like to add this post to this list:


First Step: The Input Type File

We can create a multiple input files in different ways: directly in the layout

<input type="file" multiple="multiple" />

or via JavaScript:

var input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("multiple", "multiple"); // note: input.multiple = "multiple" does not work in Safari 4 beta

I know we all like graceful degradation, but in this post I will only talk about a client side progress bar, something that is cool and possible, so far, only via JavaScript ;-)


Second Step: XMLHttpRequest Version 2

The last implementation of this constructor brings some cool stuff with him, the XMLHttpRequestUpload interface, used by an automatic created property called upload.
The reason this property is as cool as welcome, is that it can trace sent binary data via dispatched events like onload, onprogress, and others.

var xhr = new XMLHttpRequest,
upload = xhr.upload;
upload.onload = function(){
console.log("Data fully sent");
};
xhr.open("post", page, true);
xhr.send(binaryData);

Above snippet is the basis of the new feature introduced by Safari 4, feature better explained in the dedicated example I prepared for this post.


Third Step: The PHP Manager

If we use multiple attribute without XMLHttpRequest, we simply need to set a name, and manage data in the server via the superglobal $_FILES. But if we want to send directly the binary stream, the only thing we need to do in the server, security checks a part, is to save the content sent via input:

<?php
// e.g. url:"page.php?upload=true" as handler property
if(isset($_GET['upload']) && $_GET['upload'] === 'true'){
$headers = getallheaders();
if(
// basic checks
isset(
$headers['Content-Type'],
$headers['Content-Length'],
$headers['X-File-Size'],
$headers['X-File-Name']
) &&
$headers['Content-Type'] === 'multipart/form-data' &&
$headers['Content-Length'] === $headers['X-File-Size']
){
// create the object and assign property
$file = new stdClass;
$file->name = basename($headers['X-File-Name']);
$file->size = $headers['X-File-Size'];
$file->content = file_get_contents("php://input");

// if everything is ok, save the file somewhere
if(file_put_contents('files/'.$file->name, $file->content))
exit('OK');
}

// if there is an error this will be the output instead of "OK"
exit('Error');
}
?>



Last Step: The Client Manager With The Progress Bar

This is the last thing we should care about, and only after we are sure we implemented best security checks to avoid problems for users and the server itself. In this example I did not implement too many checks, so please take it as a hint, rather than a final solution for public production environments.
The client side is really simple and entirely managed via JavaScript.

/** basic Safari 4 multiple upload example
* @author Andrea Giammarchi
* @blog WebReflection [webreflection.blogspot.com]
*/
onload = function(){

function size(bytes){ // simple function to show a friendly size
var i = 0;
while(1023 < bytes){
bytes /= 1024;
++i;
};
return i ? bytes.toFixed(2) + ["", " Kb", " Mb", " Gb", " Tb"][i] : bytes + " bytes";
};

// create elements
var input = document.body.appendChild(document.createElement("input")),
bar = document.body.appendChild(document.createElement("div")).appendChild(document.createElement("span")),
div = document.body.appendChild(document.createElement("div"));

// set input type as file
input.setAttribute("type", "file");

// enable multiple selection (note: it does not work with direct input.multiple = true assignment)
input.setAttribute("multiple", "multiple");

// auto upload on files change
input.addEventListener("change", function(){

// disable the input
input.setAttribute("disabled", "true");

sendMultipleFiles({

// list of files to upload
files:input.files,

// clear the container
onloadstart:function(){
div.innerHTML = "Init upload ... ";
bar.style.width = "0px";
},

// do something during upload ...
onprogress:function(rpe){
div.innerHTML = [
"Uploading: " + this.file.fileName,
"Sent: " + size(rpe.loaded) + " of " + size(rpe.total),
"Total Sent: " + size(this.sent + rpe.loaded) + " of " + size(this.total)
].join("<br />");
bar.style.width = (((this.sent + rpe.loaded) * 200 / this.total) >> 0) + "px";
},

// fired when last file has been uploaded
onload:function(rpe, xhr){
div.innerHTML += ["",
"Server Response: " + xhr.responseText
].join("<br />");
bar.style.width = "200px";

// enable the input again
input.removeAttribute("disabled");
},

// if something is wrong ... (from native instance or because of size)
onerror:function(){
div.innerHTML = "The file " + this.file.fileName + " is too big [" + size(this.file.fileSize) + "]";

// enable the input again
input.removeAttribute("disabled");
}
});
}, false);

bar.parentNode.id = "progress";

};

The external file with sendFile and sendMultipleFile function is in my repository and in the attached zip, while a workable example page with a limit of 1Mb for each file is here in my host.

No comments:

Post a Comment