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

PHP-Dateidownload: Große Dateien sicher streamen

Ein PHP-Dateidownload wirkt auf den ersten Blick trivial: Datei lesen, ausgeben, fertig. Doch sobald du auf diese Weise ein Log-Archiv von mehreren hundert Megabyte oder eine Videodatei ausliefern willst, füllt sich der Serverspeicher sofort, der PHP-Prozess stirbt mit Allowed memory size exhausted und der Nutzer bleibt mit einem halb fertigen Download zurück. In diesem Artikel zeige ich, wie du Dateien mit den richtigen Headern ausliefern und große Dateien streamen kannst, ohne den Speicher zu überlasten.

Warum der naive Ansatz den Speicher sprengt

Der häufigste Fehler ist, die gesamte Datei auf einmal in den Speicher zu laden:

// SCHLECHT: eine 500-MB-Datei = 500 MB RAM
$data = file_get_contents('/path/to/big.zip');
echo $data;

file_get_contents zu verwenden (oder vor dem echo zu puffern) kopiert die gesamte Datei in den Speicherbereich von PHP. Liegt dein memory_limit bei 256 MB, tötet eine 300-MB-Datei den Prozess, noch bevor der Download überhaupt beginnt. Die Lösung besteht darin, die Datei Stück für Stück zu lesen und jedes Stück sofort in die Ausgabe zu schreiben, also zu streamen. So befindet sich zu jedem Zeitpunkt nur ein kleiner Puffer im Speicher.

Die richtigen Download-Header

Damit der Browser die Datei herunterlädt, statt sie anzuzeigen, musst du die richtigen HTTP-Header senden. Die wichtigsten sind:

  • Content-Type — der MIME-Typ der Datei (verwende application/octet-stream, falls unbekannt).
  • Content-Disposition: attachment; filename="..." — weist den Browser an, sie herunterzuladen, und unter welchem Namen zu speichern.
  • Content-Length — die Dateigröße; nötig, damit die Fortschrittsanzeige korrekt funktioniert.
$file = '/path/to/big.zip';
$name = 'archiv.zip';

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

Enthält der Dateiname in Content-Disposition Leerzeichen oder Nicht-ASCII-Zeichen, ist es ratsam, zusätzlich die Syntax filename*=UTF-8'' für die Kompatibilität mit älteren Browsern anzugeben.

Einfaches Streaming mit readfile

Für mittelgroße Dateien genügt die PHP-Funktion readfile. readfile sendet die Datei direkt an den Ausgabepuffer, ohne sie in den PHP-Speicher zu ziehen; dieser Vorteil geht jedoch verloren, wenn Output-Buffering aktiv ist. Leere daher zuerst den Puffer:

// Schließe alle offenen Ausgabepuffer
while (ob_get_level() > 0) {
    ob_end_clean();
}

readfile($file);
exit;

readfile erledigt die Aufgabe in einer Zeile, gibt dir aber keine Kontrolle über den Stream. Wenn du Rate Limiting, fortsetzbare Downloads oder eine Verarbeitung pro Block brauchst, musst du auf manuelles Lesen umsteigen.

Volle Kontrolle mit fopen/fread

Für wirklich große Dateien und volle Kontrolle liest du die Datei in Blöcken fester Größe. Die folgende Schleife verwendet jeweils nur 8 KB Speicher:

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

while (!feof($handle)) {
    echo fread($handle, 8192); // 8-KB-Blöcke
    flush();                   // den Puffer sofort an den Client senden
}
fclose($handle);
exit;

Die Datei im Modus 'rb' (binär) zu öffnen, verhindert die Beschädigung binärer Dateien wie Zips und Bilder auf Windows-Servern. Der Aufruf flush() sendet die Daten, ohne sie zurückzuhalten. Bei sehr großen Downloads kannst du pro Durchlauf eine Prüfung if (connection_aborted()) break; einfügen, damit du das nutzlose Lesen beendest, wenn der Nutzer den Tab schließt.

Ausführungszeit und Sicherheit

Bei einer langsamen Verbindung kann eine große Datei die standardmäßige max_execution_time (meist 30 s) überschreiten. Setze das Limit zurück, bevor das Streaming beginnt; set_time_limit(0) kann den Timer pro Block auffrischen, der sicherere Ansatz ist jedoch, set_time_limit innerhalb der Stream-Schleife periodisch aufzurufen.

Auf der Sicherheitsseite ist der kritischste Punkt, Benutzereingaben niemals direkt in den Dateipfad zu setzen:

  • Blockiere Path Traversal: bereinige Eingaben wie ../../etc/passwd mit basename() und prüfe mit realpath(), dass die Datei im erlaubten Verzeichnis liegt.
  • Verwende eine Whitelist: ordne herunterladbare Dateien einer ID in der Datenbank zu; gib niemals rohe Pfade an den Client weiter.
  • Prüfe die Berechtigung: stelle sicher, dass der Nutzer Zugriff auf diese Datei hat, bevor du den Stream startest.
$base = '/var/www/downloads';
$path = realpath($base . '/' . basename($_GET['file'] ?? ''));

if ($path === false || strpos($path, $base) !== 0) {
    http_response_code(403);
    exit('Zugriff verweigert');
}

Der praktische Weg in Laravel und Symfony

Wenn du ein Framework verwendest, musst du das nicht von Hand erledigen. In Laravel liefert response()->download() die Datei als Download aus, während response()->streamDownload() generierte Inhalte (zum Beispiel eine spontan erzeugte CSV) streamt, ohne den Speicher zu füllen. Auf der Symfony-Seite leistet BinaryFileResponse dasselbe und unterstützt den X-Sendfile-Header.

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

// Spontan generierter Inhalt
return response()->streamDownload(function () {
    $out = fopen('php://output', 'w');
    foreach ($rows as $row) {
        fputcsv($out, $row);
    }
    fclose($out);
}, 'bericht.csv');

Für maximale Performance ist es am besten, statische Dateien gar nicht durch PHP laufen zu lassen und sie an den Webserver zu übergeben: Mit X-Accel-Redirect bei Nginx oder dem X-Sendfile-Header bei Apache führt PHP die Berechtigungsprüfung durch, während der Server selbst die Datei ausliefert. So bleibt ein PHP-Worker nicht sekundenlang blockiert.

Häufige Fragen

Sollte ich readfile oder eine fread-Schleife verwenden?

Für mittelgroße Dateien und sobald du den Ausgabepuffer geleert hast, genügt readfile und ist der einfachste Weg. Brauchst du Feinabstimmung wie Rate Limiting, fortsetzbare Downloads oder die Erkennung von Verbindungsabbrüchen, gibt dir die fopen/fread-Schleife volle Kontrolle.

Mein Download bricht ab und ich erhalte einen Fehler "beschädigte Datei", warum?

Die häufigste Ursache ist Leerraum oder ein BOM-Zeichen, das vor der Datei in die Ausgabe gelangt. Stelle sicher, dass du vor den Headern nichts ausgibst, schließe alle offenen ob_*-Puffer und prüfe, dass Content-Length exakt mit der realen Größe übereinstimmt.

Warum wird der Server langsam, wenn viele Nutzer gleichzeitig herunterladen?

Jeder aktive Download hält einen PHP-Worker beschäftigt. Delegiere bei großen statischen Dateien die Auslieferung an den Webserver mit X-Accel-Redirect (Nginx) oder X-Sendfile (Apache); so werden PHP-Worker schnell wieder frei.

Macht deine Download-Infrastruktur Probleme? Ich kann speicherüberlastende Downloads in sichere, schnelle Streams verwandeln. Kontaktiere mich und wir schauen uns dein Projekt gemeinsam an.

Bu kategorideki tüm yazılar →

Devamı için