Uploader un fichier volumineux par le web

En le découpant au préalable

Il y a quelques temps, on m’a parlé d’une problématique à laquelle je n’avais pas été encore confronté. Il s’agissait d’uploader des fichiers volumineux vers un serveur par une interface web (en l’occurrence, c’était des photos, et l’upload pouvait facilement peser 500 Mo). Voici les problèmes qui se posaient:

  • la taille de maximale du fichier uploadé était limité par la configuration de PHP qui faisait tourner l’application web (les clés de configuration upload_max_filesize et post_max_size);

  • on ne voit pas de progression de l’upload. Même si la fibre se développe (lentement), uploader 500 Mo n’est pas une mince affaire !

  • et que se passe-t-il si un problème dans l’upload survient lorsque 499 Mo ont déjà bien été envoyés ? Je préfère mieux ne pas le savoir… >_<

On m’a ensuite parlé d’une technique qui consiste à découper (split) les fichiers en petits morceaux (slice ou chunk) en JavaScript (par le client, donc), envoyer ces petits morceaux, et ensuite les ré-assembler sur le serveur (en PHP, pour ma part).

Curieux d’en savoir plus, j’ai demandé à mon moteur de recherche favori. Je vous propose dans ce billet de vous montrer mes découvertes.

From scratch...

J’ai donc voulu commencer par construire un système qui fait exactement ce que j’ai décrit dans l’introduction: découpage, upload et ré-assemblage.

Le code qui suit est très fortement inspiré de cet article. Celui-ci aussi explique cette méthode.

Pour ce qui est du HTML, que du classique:

1
2
3
4
5
6
7
8
9
<form>
    <input type="file" name="file" id="file-input" /><br />
    <input type="submit" value="Upload" id="submit-button" />
</form>

<div id="upload-progress"></div>

<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="upload.js"></script>

Le JavaScript maintenant:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
$(function() {
    var reader = {};
    var file = {};
    var slice_size = 1000 * 1024; // Taille de chaque segment

    function start_upload(event) {
        event.preventDefault();

        reader = new FileReader();
        file = document.querySelector('#file-input').files[0];

        upload_file(0);
    }

    $('#submit-button').on('click', start_upload);

    function upload_file(start) {
        var next_slice = start + slice_size + 1;
        var blob = file.slice(start, next_slice); // on ne voudra lire qu'un segment du fichier

        reader.onloadend = function (event) { // fonction à exécuter lorsque le segment a fini d'être lu
            if (event.target.readyState !== FileReader.DONE) {
                return;
            }

            $.ajax({
                url: "upload.php",
                type: 'POST',
                dataType: 'json',
                cache: false,
                data: {
                    file_data: event.target.result,
                    file: file.name
                },
                error: function(jqXHR, textStatus, errorThrown) {
                    console.log(jqXHR, textStatus, errorThrown);
                },
                success: function(data) {
                    var size_done = start + slice_size;
                    var percent_done = Math.floor((size_done / file.size) * 100);

                    if (next_slice < file.size) {
                        $('#upload-progress').html('Uploading File - ' + percent_done + '%');

                        upload_file(next_slice); // s'il reste à lire, on appelle récursivement la fonction
                    } else {
                        $('#upload-progress').html('Upload Complete!');
                    }
                }
            });
        };

        reader.readAsDataURL(blob); // lecture du segment
    }
});

Et le fichier PHP upload.php qui réceptionne les segments:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

function decode_chunk($data) {
    $data = explode(';base64,', $data);

    if (!is_array($data) || !isset($data[1])) {
        return false;
    }

    $data = base64_decode($data[1]);
    if (!$data) {
        return false;
    }

    return $data;
}

// $file_path: fichier cible: garde le même nom de fichier, dans le dossier uploads
$file_path = 'uploads/' . $_POST['file'];
$file_data = decode_chunk($_POST['file_data']);

if (false === $file_data) {
    echo "error";
}

/* on ajoute le segment de données qu'on vient de recevoir 
 * au fichier qu'on est en train de ré-assembler: */
file_put_contents($file_path, $file_data, FILE_APPEND);

// nécessaire pour que JavaScript considère que la requête s'est bien passée:
echo json_encode([]); 

Et ça fonctionne ! Les fichiers sont correctement uploadés et on voit bien la progression !

Mais en fait, des bibliothèques existent !

En me renseignant sur le sujet, je me suis finalement rendu compte que de nombreuses bibliothèques JavaScript existaient pour répondre à ce problème. En voici une liste (bien-sûr non exhaustive !):

Je ne les ai pas testées, mais visiblement, elles possèdent toutes les fonctionnalités sympas liées à l’upload de fichiers (drag&drop, visualisation de la progression, …).


Lorsque j’ai commencé à creuser un peu ce sujet, j’avais comme objectif de me développer tout un petit système d’upload (visualisation de la progression, reprise en cas d’erreur, mise en pause, drag & drop des fichiers à uploader, …) utilisant cette technique de découpe et ne reposant sur une aucune bibliothèque tierce. Le but était de comprendre comment fonctionnaient tous ces mécanismes.

Autant vous dire que je ne suis pas allé bien loin dans l’élaboration de ce système d’upload. Principalement pour deux raisons:

  • je n’avais pas besoin (personnellement) d’un tel système, c’était par pure curiosité;
  • comme j’en ai parlé plus haut, il existe en réalité de nombreuses bibliothèques JavaScript qui implémentent déjà tout ce que je souhaitait faire. Et comme un développeur ne réinvente jamais la roue… :D

J’espère, par ce billet, avoir contribué à l’explication de cette technique, que je ne trouve pas excessivement documentée sur Internet. N’hésitez pas à partager les articles/bibliothèques/explications que vous auriez sur le sujet !

7 commentaires

Merci pour ce partage d’expérience !

À première vue, ça peut être un détail peu important d’implémentation mais c’est toujours bien de comprendre ce qu’il se passe vraiment quand on télécharge un fichier volumineux.

Au final, ça me serra certainement utile un jour.

+4 -0

Merci beaucoup pour ce superbe article. Je m’en sers pour essayer de me coder un petit programme type WeTransfer, c’est assez efficace. Je pars donc de cette base et incrémente progressivement pour avoir toutes les fonctionnalités souhaitées.

Très bon tuto. J’aimerai cependant envoyer 2 fichiers d’un coup. Quelle serai la procédure à suivre avec votre code. Les liens fournis restent à mon goût trop complexe à mettre en oeuvre Merci (…)

<label for="f1">Veuillez sélectionner f1</label><br>
                <input type="file" name="f1" required /> <br><br>
                <label for="f2">Veuillez sélectionner f2</label><br>
                <input type="file" name="f2" required /><br>
                <br>
          <input type="submit" value="Lancez l'Upload"/>

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte