Skip to content

Stream d'un fichier volumineux avec Symfony

Auteur : Philippe Le Van - @plv

Date : 1er mars 2022

Introduction

Parfois il faut envoyer au navigateur de l'utilisateur un fichier très volumineux.

Pour éviter des dépassements de mémoire et des timeout, il ne faut pas charger le contenu du fichier en mémoire. Nous vous proposons une solution basée sur les stream Symfony.

Solution proposée

La solution que nous proposons consiste à couper le fichier en morceaux de 1ko et de les envoyer 1 par 1 à travers un stream en utilisant la classe StreamedResponse de Symfony.

 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
<?php
namespace App\Controller;

use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\HeaderUtils;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\Routing\Annotation\Route;

class DownloadController
{
    #[Route(path: '/download_video', methods: ['GET'])]
    public function downloadResultAction() 
    {
        $completeFilePath = "video_de_8h.mkv";
        $fs = new Filesystem();
        if (! $fs->exists($completeFilePath)) {
            throw new NotFoundHttpException('file not found');
        }

        // on fixe le timeout php à 1h
        set_time_limit(3600);
        // je referme le ob_start par défaut de symfony
        ob_flush();
        // streamed response
        $response = new StreamedResponse();
        // pour virer le bufferring du serveur web (nginx par ex)
        $response->headers->set('X-Accel-Buffering', 'no');
        // pour forcer le téléchargement
        $disposition = HeaderUtils::makeDisposition(
            HeaderUtils::DISPOSITION_ATTACHMENT,
            basename($filename)
        );
        $response->headers->set('Content-Disposition', $disposition);

        // le callback est la fonction qui renvoie effectivement la donnée
        $response->setCallback(function () use ($completeFilePath) {
            $handle = fopen($completeFilePath, 'rb');
            // on coupe le fichier en tronçons de 1ko (des chunks)
            $chunkSize = 1024*1024;
            while (!feof($handle)) {
                $buffer = fread($handle, $chunkSize);
                echo $buffer;
                ob_flush();
                flush();
            }
            fclose($handle);
        });
        // lance le processus (et notamment le callback)
        $response->send();
    }
}