Comment envoyer un ou plusieurs fichiers vers un web service REST depuis un composant Svelte? Éléments de réponse dans cet article

Code d’exemple

Une fois n’est pas coutume, je vais commencer par exposer le code, les explications étant à la suite.

    
 1<style>
 2    @import "../../frameworks/bulma/bulma.css";
 3</style>
 4
 5<script>
 6	let files = null;
 7	let filesDsp = null;
 8
 9    function doProcess() {
10        processNextFile(0);
11    };
12
13	function handleFileUploadChange() {
14		filesDsp = [];
15		if (0 < files.length) {
16			for (let pos = 0; pos < files.length; pos++) {
17				filesDsp = [...filesDsp, { name: files[pos].name, uploaded: false } ];
18			}
19		}
20	};
21
22    function processNextFile(index) {
23        let fileRead;
24
25        if (index < files.length) {
26            fileRead = new FileReader();
27            fileRead.addEventListener("load", function(evt) {
28		let dataToSend = {
29		    filename: files[index].name,
30		    data: extractBase64(fileRead.result)
31		};
32                postJson('/upload', dataToSend, function(data) {
33	                if ((null !== data) && data.written) {
34                            filesDsp[index] = { name: filesDsp[index].name, uploaded: true };
35	                    processNextFile(index++);
36	                }
37	            }, function(error) {
38	                console.log('error during upload!');
39	            } );
40            }, false);
41	    fileRead.readAsDataURL(files[index]);
42        }
43    };
44</script>
45
46<main>
47    <div class="columns">
48        <div class="column is-full mx-2 my-2">
49	        <input id="fileUpload" type="file" multiple bind:files on:change={handleFileUploadChange} />
50	        {#if filesDsp}
51		        {#each filesDsp as send}
52		        <div>
53			        {send.name}&nbsp;
54			        {#if send.uploaded}
55			            <span class="icon"><i class="mdi mdi-check-circle-outline is-success"></i></span>
56			        {/if}
57		        </div>
58		        {/each}
59	        {/if}
60        </div>
61    </div>
62	<div class="columns">
63	    <div class="column is-full mx-2 my-2">
64		    <a class="button is-primary is-pulled-right" on:click="{doProcess}">
65			    <span class="icon-text"><span class="icon"><i class="mdi mdi-file-send"></i></span><span>Send</span></span>
66		    </a>
67	    </div>
68    </div>
69</main>

Commentaires techniques

Il y a plusieurs problèmes à résoudre pour envoyer un lot de fichiers à un webservice REST: on ne peut pas envoyer tous les fichiers à la fois et le service REST attend du JSON.

Lecture séquentielle des fichiers côté client

Le champ HTML est définit comme ceci: <input id="fileUpload" type="file" multiple bind:files on:change={handleFileUploadChange} />

Lorsqu’on choisit des fichiers à envoyer, le champ “files” contient la liste des fichiers. En parallèle, on construit un nouvelle liste “filesDsp” (via la méthode handleFileUploadChange)qui permettra d’afficher les noms des fichiers mais également leur statut (envoyé ou non).

Ensuite, du fait du caractère asynchrone de l’envoi au serveur, il n’est pas possible d’utiliser une simple boucle “for” pour traiter les fichiers. Ici on utilise une fonction récursive “processNextFile” qui va envoyer un fichier puis se relancer pour le fichier suivant, jusqu’à épuisement de la liste.

Transformation en contenu compatible JSON

Les fichiers référencés par un champ de type “file” sont lisibles avec un objet de type “FileReader”. Quatres méthodes sont proposées pour lire le contenu des fichiers:

  • readAsArrayBuffer: le contenu est mis à disposition sous forme de ArrayBuffer (un tableau d’octets).
  • readAsBinaryString: le contenu est stocké dans un String, sous forme binaire.
  • readAsText: le contenu est ici stocké sous forme de String (gaffe à l’encodage donc).
  • readAsDataUrl: cette méthode est intéressante car le contenu est rendu sous forme de String Base64.

Dans la fonction d’envoi, on utilisera donc la méthode “readAsDataUrl”. Il y a cependant un tout petit bémol: si le String contient bien du Base64, il contient également un entête qui représente le type de contenu et rend le String inutilisable en l’état. Il faut donc supprimer “data:*/*;base64,” (*/* peut être remplacé par n’importe quoi, par exemple “application/pdf” si on a choisit un fichier Pdf). Note: c’est le rôle de la méthode “extractBase64” qui n’est pas détaillée dans cet article.

Mise à jour de l’interface

La liste “filesDsp” est utilisée pour afficher le nom des fichiers choisis. La boucle {#each filesDsp as send} permet de parcourir les éléments (dont la structure est {name: 'XXX', uploaded: false}). Ensuite, à chaque envoi réussi, on met jour la liste que Svelte se chargera de ré-afficher (via la ligne filesDsp[index] = { name: filesDsp[index].name, uploaded: true };).

Conclusion

Même s’il reste des améliorations à apporter (éviter que le bouton d’envoi soit utilisable s’il n’y a aucun fichiers, affichage des erreurs, effacement du champ d’upload après utilisation par exemple), il est plutôt simple de créer une page d’envoi de fichiers dans un web service REST avec Svelte.