Uploader en ajax un fichier avec barre de progression

Vous savez sans doute uploader via un formulaire une image vers un serveur, et aussi via ajax (c’est un peu plus compliqué). Mais il est possible d’uploader en Ajax progressivement par morceau (chunk en anglais).

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>BLOB upload</title>
</head>

<body>
    <form action="#">
        <input type="file" value="" id="file" name="file"><br>
        <input type="submit" id="uploadBtn" value="UPLOAD">
    </form>
</body>
<script>
...
</script>
</html>

Grâce à l’attribut name on peut faire référence au fichier. Dans la balise script on a le code suivant :

<script>
    const uploadBtn = document.querySelector('#uploadBtn')
    const url = 'http://localhost.test/blobupload.php'

    uploadBtn.addEventListener('click', function (e) {
        e.preventDefault()
        const file = document.querySelector('#file').files[0]
        uploadBlob(file)

    })


</script>

La fonction uploadBlob() va prendre le fichier qui est passé en argument, puis le décomposer dans une boucle en plusieurs chunk de taille fixe. Notre boucle se termine quand on a atteint la fin du fichier.

<script>
    const uploadBtn = document.querySelector('#uploadBtn')
    const url = 'http://localhost.test/blobupload.php'

    uploadBtn.addEventListener('click', function (e) {
        e.preventDefault()
        const file = document.querySelector('#file').files[0]
        uploadBlob(file)

    })
  
  function uploadBlob(file) {
        
        const chunkSize = 100_000;
      
        let start = 0

        for (let start = 0; start < file.size; start += chunkSize) {

            const chunk = file.slice(start, start + chunkSize);
            const fd = new FormData();
            fd.set('file', chunk,);


            fetch(url, {
                method: "POST",
                body: chunk
            })
                .then(response => alert('Blob Uploaded'))
                .catch(err => alert(err));
        }
    }
</script>

Note boucle for va saucissonner le fichier en portions égales (à l’exception du dernier tronçon) à chaque itération. Ensuite ce morceau sera uploadé.

Côté backend pour la réception des fichiers en PHP

<?php
//  blobupload.php
header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
$result = file_get_contents('php://input');
$written = file_put_contents('cpu.png', $result, FILE_APPEND);
echo $written;

C’est plutôt simple côté backend, le code est synchrone. Il retourne la quantité de bytes écrits ( 1 byte = 1 octet)

Essai d’upload par paquet en javascript

Nous allons prendre un fichier qui fait plus de 100000 octets pour déclencher la découpe. Pour l’exemple j’ai une image de 811 Ko nommée cpu4.png. Cela fait combien de paquets (chunks) à envoyer? Simple division 811000 / 100000 = 9. Le dernier paquet fait moins de 100000 octets ou bytes.

Image que nous allons envoyer.

On ouvre les devtools, et effectivement j’ai 9 envois.

Si vous essyez d’ouvrir l’image cependant, vous ne pourrez pas la visualiser. En fait l’image uploadée est différente de l’image choisie, vous n’avez qu’à comparer leurs checksum sur un outil comme celui-ci.

Mais comment est ce possible? Regardez l’image ci-dessus, le fichier php renvoit la taille écrite en guise de réponse, et l’avant dernier paquet montre 29669 octets, alors que ce devait être le dernier paquet qui doit montrer ce nombre ! Bref les paquets sont envoyés dans le désordre !! Nous allons corriger cela simplement avec async/await, qui va rendre un peu de synchronisme dans la boucle for.

Async/await à la rescousse.

Nous allons modifier le code de la façon suivante : uploadBlob sera async et on « awaitera » fetch.

    async function uploadBlob(file) {
        //reader = new FileReader() // le file reader va lire à partir d'une position donnée
        const chunkSize = 100_000;
        // const filename = file.name;
        let start = 0
        let i = 0

        for (let start = 0; start < file.size; start += chunkSize) {
            console.log("morceau ", i, "uploadé")
            i++
            let end = start + chunkSize
            const chunk = file.slice(start, end);
            const fd = new FormData();
            fd.set('file', chunk,);


            await fetch(url, {
                method: "POST",
                body: chunk
            })
                .then(response => response.text())

        }
    }

Maintenant le dernier paquet est bien le plus petit. Tous les paquets sont envoyés dans l’ordre. Nous pouvons vérifier que l’image s’ouvre bien. Vérifiez les checksum de ces deux fichiers, ils sont pareil.

Il est à noter que dans le cas des envois en désordre, la taille du fichier reçu est variable !

Retour en haut