Upload de gros fichier avec HTML5 et Javascript

N’avez vous jamais eu envie ou besoin d’uploader des fichiers d’une taille supérieure à la limite maximum imposée par votre serveur web? Je me suis aussi retrouvé face à ce problème et n’ayant pas toujours la main sur la config du serveur, j’ai dû trouver des solutions. La plus simple est de passé par un service FTP, ce qui est très bien pour une personne initiée mais pour la partie publique d’un site, nettement moins. Il y a aussi des plugins flash comme uploadify qui propose de l’upload “facile” mais la limite de taille maximum est toujours là. Il faut dire aussi que flash tend à disparaître. C’est le logiciel XtremSplit qui m’a donné l’idée de découper les fichers avant de les envoyer au serveur pour qu’ils les reconstituent. Oui mais comment? C’est HTML5 qui m’a apporté la réponse (je passe la solution flash…) avec son API javascript FILE qui permet de découper des fichier blobs très facilement.

Le fonctionnement est le suivant :

  • L’utilisateur sélectionne un fichier
  • Le fichier est découpé
  • Les parties sont envoyées séparément au serveur
  • Le serveur récupère et réassemble toutes les parties

La librairie est développée en PHP 5 pour les systèmes APACHE. Il est libre à vous de l’adapter pour d’autre système serveur.

Si vous souhaitez le mettre en place il vous faudra:

  • Des clients HTML5
  • Proposer une solution alternative pour les vieux clients
  • Quelques connaissances en JavaScript pour intégrer le script dans vos pages
  • Quelques connaissances en php pour comprendre le fonctionnement côté serveur

Le code est disponible ci-dessous et la librairie est disponible en téléchargement en pied de page. Ce contenu est sous licence creative common.

/**
 * Aderesse des web services appelés
*/
var uploadVars = {
    openUpload : ‘openupload.php’, // Ouverture d’une session d’upload
    uploadManager : ‘uploadmanager.php’, // Envoi d’un bout de fichier
    assembleParts : ‘assembleparts.php’, // Assembler les parties envoyés
    assembleState : ‘assemblestate.php’ // Etat de l’assemblage
}
/**
 * Récupère un objet XMLHttp
*/
function getXmlHttp()
{
    if (window.XMLHttpRequest)
        return new XMLHttpRequest();
    else
        return new ActiveXObject(“Microsoft.XMLHTTP);
}
/**
 * Permet de réaliser un post en AJAX
*/
function post(url, params, callbackdone, callbackfail)
{
    var xmlhttp = getXmlHttp();
    xmlhttp.onreadystatechange=function(){
        if (xmlhttp.readyState==4 && xmlhttp.status==200)
            callbackdone(xmlhttp);
        else if(xmlhttp.readyState==4 && xmlhttp.status!=200)
            callbackfail(xmlhttp);
    };
    xmlhttp.open(POST,url,true);
    xmlhttp.setRequestHeader(“Content-type”,”application/x-www-form-urlencoded”);
    xmlhttp.send(params);
}
/**
 * Réaliser un post avec des datas
*/
function postdatas(url, params, callbackdone, callbackfail, callbackprogress)
{
    var xmlhttp = getXmlHttp();
    if(callbackprogress!=null)xmlhttp.addEventListener(“progress”, callbackprogress, false);
    xmlhttp.onreadystatechange=function(){
        if (xmlhttp.readyState==4 && xmlhttp.status==200)
            callbackdone(xmlhttp);
        else if(xmlhttp.readyState==4 && xmlhttp.status!=200)
            callbackfail(xmlhttp);
    };
    xmlhttp.open(POST,url,true);
    xmlhttp.send(params);
}

/**
 * Classe d’upload
*/
function Upload(file){
    var uploadedFile, aborted, started, currentpart, reader, that, jsonStart, assembleInterval, miniprogress;
    this.start = function()
    {
        this.started = true;
        post(uploadVars.openUpload,&filesize=+this.uploadedFile.size+&filename=+this.uploadedFile.name,
            function(x){
                var json = JSON.parse(x.responseText);
                that.startDone(json);
            },
            function(x){
                that.startFailed(x);
            }
        );
    }
    this.startFailed = function(xmlhttp)
    {
    }
    this.startDone = function(json)
    {
        this.jsonStart = json;
        this.idSet(this.jsonStart.id);
        this.uploadPart(1);
    }
    this.uploadPart = function(numpart)
    {
        this.currentpart = numpart;
        var begin = (numpart-1)this.jsonStart.partsize;
        var end = begin + this.jsonStart.partsize;
        if(end > this.uploadedFile.size) end = this.uploadedFile.size;
        var blob = this.uploadedFile.slice(begin, end);

        var formPost = new FormData();
        formPost.append(“uploadid”, this.jsonStart.id);
        formPost.append(“part”, numpart);
        formPost.append(“blob”, blob);

        that.progressUpdateChanged(this.currentpart + that.miniprogress,this.jsonStart.parts,that.jsonStart.id);
        postdatas(uploadVars.uploadManager,
            formPost,
            function(x){
                that.miniprogress = 0;
                if(numpart < that.jsonStart.parts)
                    that.uploadPart(numpart+1);
                else
                {
                    //the process is over assemble the file
                    post(uploadVars.assembleParts,&uploadid=+that.jsonStart.id,
                        function(x){
                            that.uploadOver(that.jsonStart.id);
                        },
                        function(x){

                        }
                    );
                    that.getAssemblageState();
                }
            },
            function(x){
                that.miniprogress = 0;
                that.uploadPart(numpart);
            },
            function(evt){
                that.miniprogress = evt.loaded / evt.total;
                that.progressUpdateChanged(that.currentpart + that.miniprogress,that.jsonStart.parts,that.jsonStart.id);
            }
        );

        this.reader.readAsBinaryString(blob);
    }
    this.getAssemblageState = function()
    {
        post(uploadVars.assembleState,&uploadid=+that.jsonStart.id,
            function(x){
                var percent = parseInt(x.responseText);
                that.assemblageStateChanged(that.jsonStart.id, percent);
                if(that.assembleInterval!=null)window.clearInterval(that.assembleInterval);
                if(percent != 100)
                    that.assembleInterval = setInterval(function(){that.getAssemblageState();},250);
            },
            function(x){

            }
        );
    }
    this.progressUpdateChanged=function(currentpart, all, id)
    {
        console.log(this.currentpart/this.jsonStart.parts);
    }
    this.uploadOver=function(id)
    {
    }
    this.idSet = function(id)
    {
    }
    this.assemblageStateChanged = function(id, percent)
    {
    }

    that = this;
    this.uploadedFile = file;
    this.started = false;
    this.aborted = false;
    this.state = 0;
    this.reader = new FileReader();
    this.assembleInterval=null;
    this.miniprogress = 0;
}