Archives du mot-clé up.neissa.org

Envoi de fichiers : up.neissa.org

Au quotidien, j’ai souvent besoin de transférer des fichiers. Une ressource à déposer sur un serveur, une capture d’une partie de l’écran à joindre au gestionnaire de tickets, un fichier à utiliser sur un autre poste du réseau, un document à envoyer à des amis…

Il existe une foultitude d’outils permettant de faire toutes ces actions mais peu les font toutes et peu les font parfaitement. Compression des images ? Retouches ? Envoi multiple ? Création d’archives (ZIP) ? Génération d’un lien court ? Pas de limite de taille ? Placement du lien dans le presse-papier en un clic ?

C’est en ligne qu’on trouve les outils répondant le mieux à ce « cahier des charges », et étant développeur HTML5/PHP je n’ai pas résisté trouvé meilleure idée que d’en faire un moi-même : up.neissa.org.

Ergonomie

L’interface doit être agréable, pour ne pas dégoûter, pour cela elle doit être la plus simple possible… mais doit permettre de tout faire : drag & drop, ctrl+v ou sélection depuis le disque. L’outil doit être intuitif pour ne pas nécessiter d’expliquer comment on s’en sert !

Ainsi s’il est indiqué qu’il faut déposer un fichier dans le cadre vert, il est en fait possible de déposer ce fichier n’importe où dans la page. De même, il est indiqué « Envoi de fichiers » alors qu’en collant (ctrl+v) le texte du presse papier sera envoyé.

Si une image est envoyée, sa miniature apparaît sous l’interface. En cliquant dessus, il est possible de sélectionner un fragment de l’image originale à envoyer. À moins de décocher la case correspondante, les images envoyées seront au préalable compressées (taille réduite jusqu’à tenir dans un carré de 1280×1280, et mise en JPEG ou en PNG).

Une fois envoyé, le lien vers le fichier (ou vers les fichiers, zippés) peut être copié en un clic. Ce n’est pas proposé automatiquement car c’est impossible parce que ce n’est peut être pas voulu (cas du redimensionnement).

Les URLs générées doivent rester courtes, de manière à pouvoir ctrl+c/ctrl+v avec les yeux d’un PC à l’autre du réseau 🙂

Cible : Le but est que ça marche sur la dernière version stable de Google Chrome, après si ça fonctionne tout seul sur les autres navigateurs (de bureau comme mobiles), c’est un plus certain !

Technique

Coté client j’ai utilisé du HTML5 et du Javascript, coté serveur du PHP. C’est mon choix 🙂 Pour copier-coller j’ai utilisé du Flash mais déjà tout fait, fourni par ZeroClipboard (merci).

Drag & Drop : les events dragenter/dragover/dragleave sont désactivés, et drop est écouté. En accédant à event.dataTransfer.files, on obtient un tableau FileList contenant les fichiers dans des objets File.

Ctrl+V : l’event paste est écouté. En accédant à event.clipboardData.files et event.clipboardData.items on récupère les données associées au presse-papier respectivement sous formes d’objets File et DataTransferItem. Attention la politique de sécurité du navigateur peut interférer et il n’est ainsi pas possible de copier coller un fichier depuis l’explorateur vers le navigateur… Les DataTransferItem peuvent être convertis en File ou en DomString (selon le type) via les méthodes getAsFile et getAsString.

Sélection depuis le disque : la balise HTML <input> suffit. Pour permettre la sélection de plusieurs fichiers n’oublions pas l’attribut multiple= »multiple » et remercions le HTML5. Plutôt que d’écouter l’event change, j’ai préféré utiliser un formulaire avec un bon vieux bouton submit. Ainsi il est possible de corriger sa sélection avant d’envoyer les fichiers, ce qui est un peu plus fair play vis à vis de l’utilisateur. L’event submit est donc écouté, et c’est l’attribut files ($moninput.files) qui est un tableau FileList contenant les fichiers dans des objets File.

Compression des images : les File dont le type correspond à une image sont lus sous forme de « Data URI« , et pour cela c’est l’objet FileReader qui est utilisé. Le résultat lors du loadend est défini comme source d’une image <img> créée à la volée. Au load de l’image, cette dernière est dessinée redimensionnée dans un <canvas>. La méthode canvas.toDataURL(mime) permet d’obtenir une « Data URI » recompressé de l’image redimensionnée. Il faut convertir ce « Data URI » en Blob pour continuer. Si la taille du Blob est plus petite que celle du File originel, c’est le Blob qui sera envoyé.

Convertir une « Data URI » en Blob : c’est probablement l’étape la plus complexe, puisqu’il faut parser la chaine de manière à en extraire les données, puis charger ces dernières dans un ArrayBuffer, qu’on pourra utiliser dans un DataView pour créer un Blob. Sauf si comme dans Firefox 14 DataView n’existe pas, auquel cas on utilisera MozBlobBuilder qui est obsolète mais fonctionne très bien.

dataURItoBlob = function(data)
{
  var pos = data.indexOf('base64,');
  var mime = data.substr(0,pos).split(':')[1].split(';')[0];
  var bin = atob(data.substr(pos+7));
  var buff = new ArrayBuffer(bin.length);
  var conv = new Uint8Array(buff);
  for(var i=0; i<bin.length; i++)
    conv[i] = bin.charCodeAt(i);

  if(typeof(Blob) != 'undefined')
    if(typeof(DataView) != 'undefined')
      return new Blob([new DataView(buff)],{type:mime});

  if(typeof(BlobBuilder) == 'undefined')
    if(typeof(WebKitBlobBuilder) != 'undefined')
      BlobBuilder = WebKitBlobBuilder;
  if(typeof(BlobBuilder) == 'undefined')
    if(typeof(MozBlobBuilder) != 'undefined')
      BlobBuilder = MozBlobBuilder;
  if(typeof(BlobBuilder) == 'undefined')
    return alert('impossible de creer un blob a partir du base64');

  var builder = new BlobBuilder();
  builder.append(buff);
  return builder.getBlob(mime);
}

Fragmentation des données : si la taille des données à envoyer est supérieure à 4Mo, elles vont être découpées en morceaux de 4Mo maximum (pour un fichier de 25Mo : six fragments de 4Mo et un fragment de 1Mo). Pour cela le Blob/File en entrée est lu dans un FileReader, et le résultat au loadend est découpé via la méthode Blob.slice(start,end). On se retrouve donc avec 7 Blob. Pour pouvoir les rassembler coté serveur on fait tout de suite une requête Ajax de manière à obtenir un identifiant unique pour le lot.

Envoi des données : il s’agit d’une requête Ajax, donc via l’objet XMLHttpRequest. On utilise la méthode open(mode,uri,async) en mode POST et évident en asynchrone. Il vaut mieux définir le Content-Type à multipart/form-data via la méthode setRequestHeader(key,value). J’ajoute aux en-têtes le nom, le type et la taille du fichier, mais aussi dans le cas de la fragmentation l’identifiant du lot, le numéro de fragment et le nombre total de fragments. Les données sont envoyées simplement via la méthode send(file), comme on l’aurait fait pour des données POST classique.

Mise en file d’attente : étrangement en faisant glisser les 30 photos d’un coup, leur compression et leur envoi fige complètement le navigateur (vous avez dit « sandbox » ?). C’est parce que toutes les méthodes de lecture et d’écriture sont asynchrones. Pour pallier à ce phénomène j’ai construit deux files d’attentes, une pour la compression et une pour l’envoi. En clair, pas le droit de compression plus d’une image à la fois ou d’envoyer plus de 4 fragments à la fois. C’est surement un tout petit peu plus lent, mais au moins le navigateur reste disponible.

Découpe d’image : en envoyant une image j’ai créé un objet <img>, que j’affiche en fait en dessous de l’interface. En cliquant dessus je l’affiche en taille réelle, et je propose de sélectionner deux coordonnées via un curseur fait avec deux <div>. L’image étant toujours dans le <canvas> il suffit d’en extraire la découpe, et de la traiter comme un nouveau Blob à envoyer.

[PHP] key(), génération d’un identifiant : les identifiants ne doivent pas être consécutifs de manière à rendre complexe la découverte d’un autre fichier en possédant un premier fichier. De plus ils doivent être courts pour être mémorisables. Il faut aussi pouvoir envoyer un nombre conséquent de fichiers. J’ai donc choisi de générer 5 caractères, chacun parmi 0-9, a-z et A-Z. Ainsi il est possible d’envoyer 1 milliard de fichiers, et si cette limite est atteinte je passerais à 6 caractères : 60 milliards de fichiers. Aujourd’hui, après 6 mois d’utilisation, il y a 1600 fichiers envoyés… soit 10 fichiers par jour environ.

[PHP] set(data), envoi : en utilisant $_SERVER pour récupérer les valeurs définies dans les en-têtes et file_get_contents(‘php://input’), on récupère toutes les données nécessaires au traitement du fichier. L’identifiant généré est renvoyé de manière à proposer le lien à l’utilisateur. Le dossier de stockage ne doit pas être accessible, ainsi on peut librement envoyer des scripts .php par exemple.

[PHP] get(id), récupération : en utilisant $_REQUEST pour obtenir l’identifiant, il est facile de renvoyer le fichier correspondant. Il ne faut pas oublier de contrôler que l’identifiant correspond bien à un fichier envoyé…

Conclusion

Je me suis bien amusé à faire cet outil ! Il y avait beaucoup de notions à comprendre et surtout j’ai découvert que c’est vraiment utile. Depuis j’en ai un usage quotidien. J’ai même découvert que ALT+ImprEcran fait une capture de la fenêtre en cours et non de tout l’écran (mais ça n’a rien à voir hein).

Une fonctionnalité à implémenter serait de pouvoir choisir l’identifiant généré au moment de l’envoi, bien qu’aujourd’hui ce soit déjà possible pour un administrateur de déplacer un fichier envoyé de manière à lui donner un nom plus facile à retenir.

Pour finir je rajouterais que j’ai fait des choix techniques importants, en ne proposant qu’un fichier (au lieu de référencer JS et CSS) je suis certain que les utilisateurs ont la dernière version du code, et qu’elle n’a pas été altérée par exemple par SFR s’ils utilisent une connexion 3G. Ou encore en n’utilisant pas de base de données de manière à avoir un outil portable dont la sauvegarde est simple à mettre en oeuvre.