Transfert asynchrone de fichiers
avec les interfaces Javascript
FileAPI et XHR

Il faut bien le reconnaitre, transférer un fichier en utilisant un formulaire HTML n'est pas très glamour : après avoir cliqué sur un vilain bouton, on choisit le fichier de nos rêves, on clique sur « envoyer » et on attend que le transfert se fasse, sans trop savoir ce qui se passe. Le manque de retour et le délai imposé par la méthode sont sans doute deux des raisons qui font du transfert asynchrone de fichiers – avec un retour sur sa progression – un des Saint Graal du web.

Une des solutions les plus simples pour le transfert asynchrone de fichiers consiste à utiliser un objet Flash pour procéder au transfert en arrière plan, et afficher la progression du transfert grâce à une fonction de rappel. Cela marche plutôt bien, quand on ne lui en demande pas trop… Aujourd'hui, la proposition FileAPI pointe le bout de son nez rusé avec la promesse d'une interface Javascript standard pour un transfert asynchrone de données par XHR. Une nouvelle solution pour nous libérer de Flash ?

Associé à cet article, vous attend une démonstration de transfert asynchrone de fichiers avec les interfaces Javascript FileAPI et XHR.

Flash et ses limitations

J'utilise encore Flash dans mon CMS pour l'envoi asynchrone de fichiers mais, comme souvent avec Flash, il a fallu contourner quelques limitations (voire bugs) pour obtenir une solution à peu près satisfaisante. Dans les choses qui manquent et/ou qui fâchent il y a : les cookies que l'on doit envoyer à la main, les types MIME des fichiers qui sont remplacés par « application/octet-stream », la valeur de retour tronquée de façon aléatoire, et récemment (avec la version 10.1.85.3) un arrêt total du fonctionnement sous Firefox.

Autant de raisons qui m'ont poussé à chercher une nouvelle solution.

L'avènement de FileAPI

Cela fait maintenant un an que j'ai découvert FileAPI, en lisant l'article W3C FileAPI in Firefox 3.6 sur le blog Mozilla Hack. Cette spécification, éditée par le comité W3C, propose une API permettant la représentation de fichiers au sein d'applications web, en exposant des interfaces pour la sélection de fichiers et l'accès à leurs données. Ce n'est pourtant qu'il y a quelques jours, en lisant l'article Using files from web applications, que je me suis dis que j'avais pratiquement trouvé le bonheur, même si l'exemple de Mozilla se contente d'envoyer un morceau de texte.

Il semblait pourtant qu'il ne manquait pas grand chose pour transférer des fichiers binaires. L'objet XMLHttpRequest propose une méthode sendAsBinary()la version de Mozilla du moins – qui laisse rêveur. Toutefois l'article se termine sur une phrase penaude : « This needs to be modified for working with binary data, too. ».

Qu'à cela ne tienne, je serais celui qui modifie !

La création de la requête de transfert

J'ai eu beau parcourir l'internet, pas grand chose à se mettre sous la dent… en tout cas rien qui ressemble à un transfert de type POST. Alors, armé de mon Firebug adoré, j'ai décortiqué la requête POST pour essayer de la reproduire afin de l'envoyer en binaire en XHR.

Je ne vais pas passer en revue toutes les étapes, qui, vous le verrez en consultant le code, sont assez simples. Voici juste un sommaire :

  1. On obtient l'objet File depuis un input de type file, ou depuis un évènement drop.
  2. On crée un objet FileReader et on lui demande de charger notre fichier en tant que chaine binaire. À la fin de la lecture, l'évènement loadend se déclenche.
  3. On crée un objet XMLHttpRequest qui va nous servir a envoyer les données, et on récupère l'objet XMLHttpRequestUpload de celui-ci pour y coller des observateurs qui nous servirons à suivre la progression du transfert.
  4. On crée le corps de notre requête.
  5. On envoie tout ça en utilisant la méthode sendAsBinary() de notre objet XMLHttpRequest.

Voici quelques explications sur la préparation du corps de la requête et de ses entêtes.

Collecter les informations à envoyer

En ce qui concerne le fichier, l'interface File, étendue par Mozilla, permet d'obtenir son nom, mais aussi sa taille et son type MIME. La lecture des données se fait par l'interface FileReader. On utilisera la méthode readAsBinaryString pour obtenir les données brutes de notre fichier.

Une fois que l'on a toutes ces informations, il ne nous reste plus qu'à créer le corps et les entêtes de la requête.

Préparer le corps et les entêtes

Comme pour un transfert de fichier en POST, la requête est de type multipart/form-data, c'est à dire que la requête est coupée en petits morceaux de type form-data, délimités par des séparateurs, chaque morceau représentant la valeur d'une clé, habituellement le nom de l'élément de saisie. On définit le nom de la partie – comme elle aurait était définie par un input – avec l'attribut name de l'entête Content-Disposition, quant au nom du fichier transféré, on le défini avec l'attribut filename. FileAPI nous donne le type MIME du fichier, pas comme cet avare de Flash, on se fera une joie de le définir en utilisant l'entête Content-Type.

Vous noterez dans le code de la démonstration que j'échappe le nom du fichier transféré avec la fonction encodeURIComponent(), c'est parce que malgré tous mes efforts, je n'ai pas trouvé de meilleure solution que cela pour gérer les fichiers accentués, alors il faudra restaurer le nom du fichier du côté serveur avec la fonction urldecode().

Et pour le retour, un morceau de PHP

Une fois prête, la requête est envoyée au fichier « handler.php » qui se contente de voir ce qui se trouve dans $_FILES['Filedata'] et de le renvoyer en JSON. Voici son code en PHP :

<?php

if (empty($_FILES['Filedata']))
{
    header('HTTP/1.0 404 Missing Filedata');

    exit;
}

$rc = $_FILES['Filedata'];

header('Content-Type: application/json; charset=utf-8');

$rc['name'] = urldecode($rc['name']);

if (get_magic_quotes_gpc())
{
    $rc['name'] = stripslashes($rc['name']);
}

echo json_encode($rc);

Et voilà !

Rassembler, pour mieux s'aimer

Effectivement, c'est carrément merveilleux mais pour le moment seul Firefox propose l'interface et surtout des extensions clé aux interfaces définies par le w3c. On peut espérer que les autres navigateurs suivent, mais vu la guerre qui règne en ce moment, il ne serait pas si drôle que d'allumer un cierge dans un lieu froid à la belle acoustique.

Mais ne boudons pas notre plaisir, et transformons plutôt une démonstration de transfert asynchrone de fichiers avec les interfaces Javascript FileAPI et XHR en réalité !

Laisser un commentaire

2 commentaires

Jikoo
Jikoo

Bonjour. Hé bien, je découvre en ce jour un très clair et symphatique blog. Merci pour votre travail. C'est très agréable à lire. Concernant cet article, après de longues recherches sur le web, j'ai dû trouvé des dizaines et des dizaines de scripts que je n'arrivais pas à faire fonctionner avec $_FILES en PHP. Désormais, grâce à vous, c'est chose faite. Je l'ai arrangé à ma façon pour uploader plusieurs fichiers et voir le pourcentage de progression de l'upload. Merci beaucoup.

clem
clem

Manque plus qu'une petite mise à jour pour l'utilisation avec FormData