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

PHP-bestandsdownload: grote bestanden veilig streamen

Een PHP-bestandsdownload lijkt op het eerste gezicht triviaal: lees het bestand, echo het, klaar. Maar zodra je op deze manier een logarchief van enkele honderden megabytes of een videobestand probeert te serveren, loopt het servergeheugen direct vol, sterft het PHP-proces met Allowed memory size exhausted en blijft de gebruiker met een half voltooide download achter. In dit artikel laat ik zien hoe je bestanden met de juiste headers serveert en hoe je grote bestanden streamt zonder het geheugen uit te putten.

Waarom de naïeve aanpak het geheugen opblaast

De meest gemaakte fout is het hele bestand in één keer in het geheugen laden:

// SLECHT: een bestand van 500 MB = 500 MB RAM
$data = file_get_contents('/path/to/big.zip');
echo $data;

file_get_contents gebruiken (of bufferen vóór echo) kopieert het hele bestand naar de geheugenruimte van PHP. Als je memory_limit 256 MB is, doodt een bestand van 300 MB het proces nog voordat de download is begonnen. De oplossing is het bestand stuk voor stuk te lezen en elk stuk meteen naar de uitvoer te schrijven, oftewel het te streamen. Zo bevindt zich op elk moment slechts een kleine buffer in het geheugen.

De juiste downloadheaders

Om de browser het bestand te laten downloaden in plaats van weergeven, moet je de juiste HTTP-headers sturen. De belangrijkste zijn:

  • Content-Type — het MIME-type van het bestand (gebruik application/octet-stream als het onbekend is).
  • Content-Disposition: attachment; filename="..." — vertelt de browser het te downloaden en onder welke naam het op te slaan.
  • Content-Length — de bestandsgrootte; nodig om de voortgangsbalk correct te laten werken.
$file = '/path/to/big.zip';
$name = 'archief.zip';

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

Als de bestandsnaam in Content-Disposition spaties of niet-ASCII-tekens bevat, is het verstandig ook de filename*=UTF-8''-syntaxis toe te voegen voor compatibiliteit met oudere browsers.

Eenvoudig streamen met readfile

Voor middelgrote bestanden volstaat de readfile-functie van PHP. readfile stuurt het bestand rechtstreeks naar de uitvoerbuffer zonder het in het PHP-geheugen te trekken; dat voordeel gaat echter verloren als output buffering aanstaat. Leeg daarom eerst de buffer:

// Sluit alle open uitvoerbuffers
while (ob_get_level() > 0) {
    ob_end_clean();
}

readfile($file);
exit;

readfile regelt het in één regel, maar geeft je geen controle over de stream. Als je rate limiting, hervatbare downloads of verwerking per stuk nodig hebt, moet je overstappen op handmatig lezen.

Volledige controle met fopen/fread

Voor echt grote bestanden en volledige controle lees je het bestand in blokken van een vaste grootte. De onderstaande lus gebruikt slechts 8 KB geheugen per keer:

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

while (!feof($handle)) {
    echo fread($handle, 8192); // blokken van 8 KB
    flush();                   // stuur de buffer nu naar de client
}
fclose($handle);
exit;

Het bestand openen in 'rb'-modus (binair) voorkomt beschadiging van binaire bestanden zoals zips en afbeeldingen op Windows-servers. De flush()-aanroep stuurt data zonder die vast te houden. Bij zeer grote downloads kun je per iteratie een controle if (connection_aborted()) break; toevoegen, zodat je stopt met nutteloos lezen wanneer de gebruiker het tabblad sluit.

Uitvoeringstijd en beveiliging

Op een trage verbinding kan een groot bestand de standaard max_execution_time (meestal 30 s) overschrijden. Reset de limiet voordat het streamen begint; set_time_limit(0) kan de timer per stuk verversen, maar de veiligste aanpak is set_time_limit periodiek aan te roepen binnen de streamlus.

Aan de beveiligingskant is het meest kritieke punt om gebruikersinvoer nooit rechtstreeks in het bestandspad te zetten:

  • Blokkeer path traversal: saneer invoer zoals ../../etc/passwd met basename() en controleer met realpath() dat het bestand zich in de toegestane map bevindt.
  • Gebruik een whitelist: koppel downloadbare bestanden aan een ID in de database; stel nooit ruwe paden bloot aan de client.
  • Controleer autorisatie: verifieer dat de gebruiker toegang heeft tot dat bestand voordat je de stream start.
$base = '/var/www/downloads';
$path = realpath($base . '/' . basename($_GET['file'] ?? ''));

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

De praktische route in Laravel en Symfony

Als je een framework gebruikt, hoef je dit niet met de hand te doen. In Laravel serveert response()->download() het bestand als download, terwijl response()->streamDownload() gegenereerde inhoud (bijvoorbeeld een CSV ter plekke) streamt zonder het geheugen te vullen. Aan de Symfony-kant doet BinaryFileResponse hetzelfde en ondersteunt het de X-Sendfile-header.

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

// Inhoud die ter plekke wordt gegenereerd
return response()->streamDownload(function () {
    $out = fopen('php://output', 'w');
    foreach ($rows as $row) {
        fputcsv($out, $row);
    }
    fclose($out);
}, 'rapport.csv');

Voor maximale prestaties kun je statische bestanden het beste helemaal niet via PHP laten gaan en ze overdragen aan de webserver: met X-Accel-Redirect op Nginx of de X-Sendfile-header op Apache doet PHP de autorisatiecontrole, terwijl de server zelf het bestand levert. Zo blijft een PHP-worker niet seconden achtereen bezet.

Veelgestelde vragen

Moet ik readfile of een fread-lus gebruiken?

Voor middelgrote bestanden, en zodra je de uitvoerbuffer hebt geleegd, volstaat readfile en is het de eenvoudigste weg. Heb je fijnafstelling nodig zoals rate limiting, hervatbare downloads of detectie van verbroken verbindingen, dan geeft de fopen/fread-lus je volledige controle.

Mijn download breekt af en ik krijg een fout "beschadigd bestand", waarom?

De meest voorkomende oorzaak is witruimte of een BOM-teken dat in de uitvoer lekt vóór het bestand. Zorg dat je niets afdrukt vóór de headers, sluit alle open ob_*-buffers en controleer dat Content-Length exact overeenkomt met de werkelijke grootte.

Waarom wordt de server traag als veel gebruikers tegelijk downloaden?

Elke actieve download houdt een PHP-worker bezig. Delegeer voor grote statische bestanden de levering aan de webserver met X-Accel-Redirect (Nginx) of X-Sendfile (Apache); zo worden PHP-workers snel vrijgegeven.

Veroorzaakt je downloadinfrastructuur problemen? Ik kan downloads die het geheugen opblazen omzetten in veilige, snelle streams. Neem contact op en laten we je project samen bekijken.

Bu kategorideki tüm yazılar →

Devamı için