[PHP, JS] Téléchargement dynamique comme MEGA

développement - juin 2016

Pour un projet personnel, je me suis lancé dans la création d'un système de téléchargement dynamique comme peut le proposer le célèbre site MEGA. L'objectif étant de pouvoir lancer plusieurs téléchargements tout en les traitant un à un et ceux de façon plus conviviale.

Fonctionnement

Il s'agit à partir d'un lien pointant vers un fichier, de récupérer ce dernier en mémoire avec javascript, d'afficher sa progression et enfin de lancer le téléchargement classique une fois son chargement complet. Le fichier étant en mémoire, le téléchargement sera local et presque instantané (dépendra de la vitesse d'écriture du disque de l'utilisateur).

Nous avons un fichier html pour les liens, un fichier javascript pour la gestion, un petit peu de css pour la mise en forme et un ou plusieurs fichiers à télécharger à la racine.

index.html

<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<link rel="stylesheet" type="text/css" href="site.css" />
</head>
<body>
<div id="down"><div id="downencours" class="listedown" ></div></div>
<div id="global">
<a class="download" href="exemple.zip" download="fichier.zip">Lancer le téléchargement du fichier d'exemple #1</a><br />
<a class="download" href="exemple2.bin" download="fichier.bin">Lancer le téléchargement du fichier d'exemple #2</a><br />
<a class="download" href="exemple3.txt" download="fichier.txt">Lancer le téléchargement du fichier d'exemple #3</a>
</div>
<script type="text/javascript" src="telechargements.js"></script>
</body>
</html>

Les liens de téléchargement ont la classe download. Ici, si javascript est désactivé, les liens se comporteront normalement.

site.css

body {
margin: 0;
font-family: Arial;
cursor: default;
}
a {
text-decoration: none;
}
/*-------------------------------------------------------*/
#down { /* div des téléchargements */
font-size: 85%;
background: rgb(255,255,255);
color: rgb(50,50,50);
}
.listedown, .listewait { /* élément(s) en téléchargement */
padding: 5px;
background: rgba(0,0,0,.1);
}
.listedown>div, .listewait>div { /* nom du fichier */
overflow: hidden;
text-align: left;
}
#down a {
color: rgb(50,50,50);
}
#down .del { /* croix de suppression */
color: darkred;
}
.listedown>div>span, .listewait>div>span { /* taille du fichier en Mo */
float: right;
text-align: right;
}
.listedown>div.prog { /* barre de progression */
height: 3px;
background: black;
margin-bottom: 5px;
}

La mise-en-forme est minimale et concerne essentiellement l'aspect des div de téléchargement (nom, taille et barre de progression).

telechargements.js

/*==========================================================================================*/
/* TELECHARGEMENT INTERACTIF AVEC FILE D'ATTENTE */
/*==========================================================================================*/
var down_id = 0; // identifiant du téléchargement
var down_nb = 0; // downs en cours (pour ne permettre qu'un téléchargement à la fois)
var div_telech = document.getElementById("down"); // div contenant la liste les téléchargements
var div_down = document.getElementsByClassName("download"); // liste les liens "download"
window.URL = window.URL || window.webkitURL;
/*------------------------------------------------------------------------------------------*/
setInterval(checkfile, 3000); // vérifie la file d'attente toutes les x secondes
check_clic_down();
var del = document.getElementsByClassName('del'); // liens permettant de supprimer les élements de la liste
deletefile();
/*==========================================================================================*/
/* FONCTIONS */
/*==========================================================================================*/
function check_clic_down () {
for (var i = 0; i < div_down.length; i++) { // pour tous les liens "download"
div_down[i].addEventListener('click', addfile, false); // detecte le clique sur l'un et l'ajoute à la file
}
}
/*==========================================================================================*/
function addfile (u) { // au clique, ajoute une div de file d'attente
down_id++; // nouvel ID
var nom_fichier = this.getAttribute("download"); // nom du fichier (de l'attribut download)
var div_fichier = document.createElement('div'); // nouvelle div (div#waitxx)
div_fichier.id = "wait"+down_id; // son ID
div_fichier.className = "listewait"; // et sa classe
var div_nom = document.createElement('div'); // nouvelle div pour le nom ou lien et span
var span_lien = document.createElement('a'); // lien pour le nom
span_lien.href = this.getAttribute("href");
span_lien.id = 'await'+down_id;
span_lien.download = nom_fichier;
span_lien.innerHTML = nom_fichier;
var span_wait = document.createElement('span'); // ---------- span wait...
span_wait.innerHTML = '<span><a href="#" title="téléchargement en attente, cliquer pour annuler" rel="'+div_fichier.id+'" class="del">X</a></span>';
div_telech.appendChild(div_fichier); // ajoute la div du down dans la liste
div_fichier.appendChild(div_nom); // ajoute la div nom/span
div_nom.appendChild(span_lien); // ajoute le nom en lien
div_nom.appendChild(span_wait); // ajoute les 3 p'tits points
u.preventDefault(); // désactive le lien avec JS
}
/*==========================================================================================*/
function checkfile () { // toutes les x sec check la file var div_wait = document.getElementsByClassName("listewait"); // liste les liens "en attente"
if (down_nb == 0) { // s'il n'y a pas de téléchargement en cours
if (div_wait[0]) // et s'il y a un élément en attente
startfile(div_wait[0]); // lance le téléchargement (via xhr) du prochain fichier
}
}
/*==========================================================================================*/
function startfile (u) { // lance le téléchargement
down_nb = 1; // indique que le téléchargement est en cours
var yy = document.getElementById("a"+u.getAttribute('id')); // a#awaitxx à traiter
var nom_fichier = yy.getAttribute("download"); // nom du fichier
var div_fichier = document.getElementById("downencours");
var div_nom = document.createElement('div'); // div du nom ou lien et span
div_nom.id = "down"+u.getAttribute('id');
var div_prog = document.createElement('div'); // div de la barre de progression
div_prog.id = "progdown"+u.getAttribute('id'); // son ID
div_prog.className = "prog"; // et sa classe
div_fichier.appendChild(div_nom); // ajoute le nom
div_fichier.appendChild(div_prog); // et la barre de progression
u.className = ''; // vire la class (à defaut de la supprimer) pour enchainer les autres
u.innerHTML = ''; // vide la div du fichier en attente
del = document.getElementsByClassName('del');
deletefile();
/*------------------------------------------------------------------------------------------*/
var xhr = new XMLHttpRequest(); // REQUETE
xhr.open('GET', yy.getAttribute("href"), true); // lien du fichier cliqué
xhr.responseType = 'arraybuffer';
/*------------------------------------------------------------------------------------------*/
xhr.onprogress = function (e) { // EN COURS
div_prog.style.width = Math.ceil(100 / e.total * e.loaded)+"%"; // pourcentage
div_nom.innerHTML = nom_fichier+'<span><b>'+Math.ceil(e.loaded / 1000000)+' Mo</b><a href="#" title="téléchargement en cours, cliquer pour annuler" rel="'+div_nom.id+'" class="del">X</a></span>'; // nom + progression
del = document.getElementsByClassName('del');
deletefile();
};
/*------------------------------------------------------------------------------------------*/
xhr.onloadend = function(e) { // TERMINE
if (this.status == 200) {
var blob = new Blob([xhr.response]); // nouveau blob avec requête
var span_lien = document.createElement('a'); // lien pour le nom
span_lien.innerHTML = nom_fichier;
span_lien.href = window.URL.createObjectURL(blob); // met le blob en lien
span_lien.download = nom_fichier;
div_nom.innerHTML = '<span><b>'+Math.ceil(blob.size / 1000000)+' Mo</b><a href="#" title="téléchargement terminé, cliquer pour supprimer" rel="'+div_nom.id+'" hreflang="'+span_lien.href+'" class="del">X</a></span>';
del = document.getElementsByClassName('del');
deletefile();
div_nom.appendChild(span_lien);
span_lien.click(); // final, lance le téléchargement du blob
down_nb = 0; // plus de down, permet le suivant
blob = '';
}
};
/*------------------------------------------------------------------------------------------*/
xhr.send();
}
/*==========================================================================================*/
function deletefile() { // supprime un telechargement (en attente, en cours ou terminé)
for (var i = 0; i < del.length; i++) {
var wa = del[i];
wa.onclick = function (u) { // détecte le clic
var rel = this.getAttribute("rel");
if (rel.search(/downwait/)) // rel contient "downwait", vire la div en attente
document.getElementById("down").removeChild(document.getElementById(rel));
else { // rel contient "downwait", vire la div en cours ou terminée
if (this.getAttribute("hreflang"))
window.URL.revokeObjectURL(this.getAttribute("hreflang")); // supprime l'adresse du blob
document.getElementById("downencours").removeChild(document.getElementById(rel));
document.getElementById("downencours").removeChild(document.getElementById('prog'+rel));
}
u.preventDefault(); // empêche l'exécution du lien
}
}
}

Le gros du travail : le script va vérifier toutes les 3 secondes s'il y a un fichier à traiter dans la liste d'attente. S'il n'y a pas de téléchargement en cours, il traite le suivant.

L'utilisateur peut à tout moment supprimer un élément. Si le fichier est déjà téléchargé et existe sous forme de blob, il est supprimé pour libérer de la mémoire.

Conclusion

Il ne s'agit pas d'une solution miracle, les petites configurations par exemple auront bien du mal à charger des fichiers de plusieurs giga puisque ces derniers sont stockés dans la mémoire du navigateur.

Dans cet exemple, il revient à l'utilisateur de supprimer les éléments déjà téléchargés pour libérer de la place. Ce qui est inutile si le script est dédié à un seul fichier puisque le blob sera détruit à la fermeture de la page.