aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Développement Web

Téléchargement de fichiers PHP : streamer les gros fichiers

Le téléchargement de fichiers PHP paraît trivial au premier abord : on lit le fichier, on l'affiche, c'est terminé. Mais dès que vous essayez de servir ainsi une archive de logs de plusieurs centaines de mégaoctets ou un fichier vidéo, la mémoire du serveur se remplit instantanément, le processus PHP meurt avec l'erreur Allowed memory size exhausted et l'utilisateur se retrouve avec un téléchargement à moitié terminé. Dans cet article, j'explique comment servir des fichiers avec les bons en-têtes et comment streamer les gros fichiers sans saturer la mémoire.

Pourquoi l'approche naïve fait exploser la mémoire

L'erreur la plus courante est de charger tout le fichier en mémoire d'un coup :

// MAUVAIS : un fichier de 500 Mo = 500 Mo de RAM
$data = file_get_contents('/path/to/big.zip');
echo $data;

Utiliser file_get_contents (ou bufferiser avant echo) copie tout le fichier dans l'espace mémoire de PHP. Si votre memory_limit est de 256 Mo, un fichier de 300 Mo tue le processus avant même que le téléchargement ne commence. La solution est de lire le fichier morceau par morceau et d'écrire chaque morceau immédiatement dans la sortie, autrement dit de le streamer. Ainsi, seul un petit tampon réside en mémoire à un instant donné.

Les bons en-têtes de téléchargement

Pour que le navigateur télécharge le fichier au lieu de l'afficher, vous devez envoyer les bons en-têtes HTTP. Les plus essentiels sont :

  • Content-Type — le type MIME du fichier (utilisez application/octet-stream s'il est inconnu).
  • Content-Disposition: attachment; filename="..." — indique au navigateur de le télécharger et sous quel nom l'enregistrer.
  • Content-Length — la taille du fichier ; nécessaire au bon fonctionnement de la barre de progression.
$file = '/path/to/big.zip';
$name = 'archive.zip';

header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $name . '"');
header('Content-Length: ' . filesize($file));
header('X-Content-Type-Options: nosniff');

Si le nom de fichier dans Content-Disposition contient des espaces ou des caractères non-ASCII, il est recommandé d'ajouter aussi la syntaxe filename*=UTF-8'' pour la compatibilité avec les navigateurs plus anciens.

Streaming simple avec readfile

Pour des fichiers de taille moyenne, la fonction readfile de PHP suffit. readfile envoie le fichier directement au tampon de sortie sans le charger dans la mémoire de PHP ; cet avantage est cependant perdu si la bufferisation de sortie est active. Videz donc d'abord le tampon :

// Ferme tout tampon de sortie ouvert
while (ob_get_level() > 0) {
    ob_end_clean();
}

readfile($file);
exit;

readfile règle la question en une ligne mais ne vous donne aucun contrôle sur le flux. Si vous avez besoin de limitation de débit, de téléchargements reprenables ou d'un traitement morceau par morceau, vous devez passer à la lecture manuelle.

Contrôle total avec fopen/fread

Pour les fichiers vraiment volumineux et un contrôle total, lisez le fichier par blocs de taille fixe. La boucle ci-dessous n'utilise que 8 Ko de mémoire à la fois :

$handle = fopen($file, 'rb');
if ($handle === false) {
    http_response_code(404);
    exit;
}

while (!feof($handle)) {
    echo fread($handle, 8192); // blocs de 8 Ko
    flush();                   // envoie le tampon au client immédiatement
}
fclose($handle);
exit;

Ouvrir le fichier en mode 'rb' (binaire) évite la corruption des fichiers binaires comme les zips et les images sur les serveurs Windows. L'appel flush() envoie les données sans les retenir. Pour les très gros téléchargements, vous pouvez ajouter une vérification if (connection_aborted()) break; à chaque itération afin de cesser une lecture inutile si l'utilisateur ferme l'onglet.

Temps d'exécution et sécurité

Sur une connexion lente, un gros fichier peut dépasser le max_execution_time par défaut (généralement 30 s). Réinitialisez la limite avant de démarrer le streaming ; set_time_limit(0) peut rafraîchir le minuteur à chaque morceau, mais l'approche la plus sûre est d'appeler set_time_limit périodiquement dans la boucle de streaming.

Côté sécurité, le point le plus critique est de ne jamais placer directement les entrées utilisateur dans le chemin du fichier :

  • Bloquez le path traversal : nettoyez les entrées comme ../../etc/passwd avec basename() et vérifiez que le fichier se trouve dans le répertoire autorisé avec realpath().
  • Utilisez une liste blanche : associez les fichiers téléchargeables à un identifiant en base de données ; n'exposez jamais les chemins bruts au client.
  • Contrôlez les autorisations : vérifiez que l'utilisateur a accès à ce fichier avant de démarrer le flux.
$base = '/var/www/downloads';
$path = realpath($base . '/' . basename($_GET['file'] ?? ''));

if ($path === false || strpos($path, $base) !== 0) {
    http_response_code(403);
    exit('Accès refusé');
}

La voie pratique dans Laravel et Symfony

Si vous utilisez un framework, inutile de tout faire à la main. Dans Laravel, response()->download() sert le fichier comme un téléchargement, tandis que response()->streamDownload() streame du contenu généré (un CSV à la volée, par exemple) sans saturer la mémoire. Côté Symfony, BinaryFileResponse fait de même et prend en charge l'en-tête X-Sendfile.

// Laravel
return response()->download('/path/to/big.zip', 'archive.zip');

// Contenu généré à la volée
return response()->streamDownload(function () {
    $out = fopen('php://output', 'w');
    foreach ($rows as $row) {
        fputcsv($out, $row);
    }
    fclose($out);
}, 'rapport.csv');

Pour des performances maximales, le mieux est de ne jamais faire passer les fichiers statiques par PHP et de les déléguer au serveur web : avec X-Accel-Redirect sur Nginx ou l'en-tête X-Sendfile sur Apache, PHP effectue le contrôle d'autorisation tandis que le serveur lui-même livre le fichier. Cela évite qu'un worker PHP reste bloqué plusieurs secondes.

Questions fréquentes

Dois-je utiliser readfile ou une boucle fread ?

Pour des fichiers de taille moyenne, et une fois le tampon de sortie vidé, readfile suffit et reste la voie la plus simple. Si vous avez besoin de réglages fins comme la limitation de débit, les téléchargements reprenables ou la détection d'interruption de connexion, la boucle fopen/fread vous donne un contrôle total.

Mon téléchargement se coupe et j'obtiens une erreur de « fichier corrompu », pourquoi ?

La cause la plus fréquente est un espace ou un caractère BOM qui fuit dans la sortie avant le fichier. Assurez-vous de ne rien afficher avant les en-têtes, fermez tout tampon ob_* ouvert et vérifiez que Content-Length correspond exactement à la taille réelle.

Pourquoi le serveur ralentit-il quand de nombreux utilisateurs téléchargent en même temps ?

Chaque téléchargement actif occupe un worker PHP. Pour les gros fichiers statiques, déléguez la livraison au serveur web avec X-Accel-Redirect (Nginx) ou X-Sendfile (Apache) ; ainsi les workers PHP sont libérés rapidement.

Votre infrastructure de téléchargement pose problème ? Je peux transformer des téléchargements qui saturent la mémoire en flux sûrs et rapides. Contactez-moi et évaluons votre projet ensemble.

Bu kategorideki tüm yazılar →

Devamı için