Laden Sie vom Laravel-Speicher herunter, ohne die gesamte Datei in den Speicher zu laden

Lesezeit: 6 Minuten

Benutzer-Avatar
Džuris

Ich verwende Laravel Storage und möchte Benutzern einige Dateien (größer als das Speicherlimit) bereitstellen. Mein Code wurde von einem Beitrag in SO inspiriert und geht so:

$fs = Storage::getDriver();
$stream = $fs->readStream($file->path);

return response()->stream(
    function() use($stream) {
        fpassthru($stream);
    }, 
    200,
    [
        'Content-Type' => $file->mime,
        'Content-disposition' => 'attachment; filename="'.$file->original_name.'"',
    ]);

Leider stoße ich bei großen Dateien auf einen Fehler:

[2016-04-21 13:37:13] production.ERROR: exception 'Symfony\Component\Debug\Exception\FatalErrorException' with message 'Allowed memory size of 134217728 bytes exhausted (tried to allocate 201740288 bytes)' in /path/app/Http/Controllers/FileController.php:131
Stack trace:
#0 /path/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php(133): Symfony\Component\Debug\Exception\FatalErrorException->__construct()
#1 /path/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php(118): Illuminate\Foundation\Bootstrap\HandleExceptions->fatalExceptionFromError()
#2 /path/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php(0): Illuminate\Foundation\Bootstrap\HandleExceptions->handleShutdown()
#3 /path/app/Http/Controllers/FileController.php(131): fpassthru()
#4 /path/vendor/symfony/http-foundation/StreamedResponse.php(95): App\Http\Controllers\FileController->App\Http\Controllers\{closure}()
#5 /path/vendor/symfony/http-foundation/StreamedResponse.php(95): call_user_func:{/path/vendor/symfony/http-foundation/StreamedResponse.php:95}()
#6 /path/vendor/symfony/http-foundation/Response.php(370): Symfony\Component\HttpFoundation\StreamedResponse->sendContent()
#7 /path/public/index.php(56): Symfony\Component\HttpFoundation\Response->send()
#8 /path/public/index.php(0): {main}()
#9 {main}  

Es scheint, dass es versucht, die gesamte Datei in den Speicher zu laden. Ich hatte erwartet, dass die Verwendung von Stream und Passthru dies nicht tun würde … Fehlt etwas in meinem Code? Muss ich irgendwie die Chunk-Größe angeben oder was?

Die Versionen, die ich verwende, sind Laravel 5.1 und PHP 5.6.

  • Das einzige Szenario, das mir einfällt, wo fpassthru in den Speicher allokiert wird, wenn die Ausgabepufferung verwendet wird. Sie können daher eine Schleife anprobieren fread mit einem echo.

    – Bischof

    2. Mai 2016 um 15:23 Uhr


Benutzer-Avatar
Christian

Es scheint, dass die Ausgabepufferung immer noch viel im Speicher aufbaut.

Versuchen Sie, ob zu deaktivieren, bevor Sie fpassthru ausführen:

function() use($stream) {
    while(ob_get_level() > 0) ob_end_flush();
    fpassthru($stream);
},

Es könnte sein, dass mehrere Ausgabepuffer aktiv sind, weshalb das While benötigt wird.

  • Diese Antwort spricht das eigentliche Problem an, das bei meiner versuchten Implementierung Probleme verursacht hat, daher akzeptiere ich das Kopfgeld und spreche es Ihnen zu. Vielen Dank an alle für die anderen Antworten, die auch wertvolle Informationen sind!

    – Džuris

    9. Mai 2016 um 12:03 Uhr

Benutzer-Avatar
Kevin

Anstatt die gesamte Datei auf einmal in den Speicher zu laden, versuchen Sie, fread zu verwenden, um sie Chunk für Chunk zu lesen und zu senden.

Hier ist ein sehr guter Artikel: http://zinoui.com/blog/download-large-files-with-php

<?php

//disable execution time limit when downloading a big file.
set_time_limit(0);

/** @var \League\Flysystem\Filesystem $fs */
$fs = Storage::disk('local')->getDriver();

$fileName="bigfile";

$metaData = $fs->getMetadata($fileName);
$handle = $fs->readStream($fileName);

header('Pragma: public');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Cache-Control: private', false);
header('Content-Transfer-Encoding: binary');
header('Content-Disposition: attachment; filename="' . $metaData['path'] . '";');
header('Content-Type: ' . $metaData['type']);

/*
    I've commented the following line out.
    Because \League\Flysystem\Filesystem uses int for file size
    For file size larger than PHP_INT_MAX (2147483647) bytes
    It may return 0, which results in:

        Content-Length: 0

    and it stops the browser from downloading the file.

    Try to figure out a way to get the file size represented by a string.
    (e.g. using shell command/3rd party plugin?)
*/

//header('Content-Length: ' . $metaData['size']);


$chunkSize = 1024 * 1024;

while (!feof($handle)) {
    $buffer = fread($handle, $chunkSize);
    echo $buffer;
    ob_flush();
    flush();
}

fclose($handle);
exit;
?>

Aktualisieren

Einfacher geht es: Einfach anrufen

if (ob_get_level()) ob_end_clean();

bevor Sie eine Antwort zurücksenden.

Gutschrift an @Christiaan

//disable execution time limit when downloading a big file.
set_time_limit(0);

/** @var \League\Flysystem\Filesystem $fs */
$fs = Storage::disk('local')->getDriver();

$fileName="bigfile";

$metaData = $fs->getMetadata($fileName);
$stream = $fs->readStream($fileName);

if (ob_get_level()) ob_end_clean();

return response()->stream(
    function () use ($stream) {
        fpassthru($stream);
    },
    200,
    [
        'Content-Type' => $metaData['type'],
        'Content-disposition' => 'attachment; filename="' . $metaData['path'] . '"',
    ]);

  • Genau dafür ist fpasstru da, keine Notwendigkeit, die Dinge zu komplizieren.

    – Christian

    8. Mai 2016 um 5:38 Uhr


  • Ich glaube nicht. Ich habe ein Experiment gemacht, fpassthru führte zu genau dem gleichen Fehler. Mit dieser Methode kann ich die Datei herunterladen.

    – Kevin

    8. Mai 2016 um 6:42 Uhr

  • @Christiaan Ich habe den Code in meiner Antwort aktualisiert und Sie können dieses Experiment auf Ihrem Computer durchführen. (Generieren Sie einfach eine 20 GB große Datei)

    – Kevin

    8. Mai 2016 um 6:44 Uhr

  • Haben Sie mit fpasstru sichergestellt, dass Sie die Ausgabepufferung deaktiviert haben? Denn das ist es, was Ihr Beispiel stirbt, indem es jedes Mal Flush ruft.

    – Christian

    8. Mai 2016 um 7:40 Uhr

  • @Christiaan Du hast recht. Danke für den Hinweis. Ja, es ist eigentlich ein sehr einfaches Problem, wie konnte ich den Punkt verpasst haben. Ruf einfach an if (ob_get_level()) ob_end_clean(); bevor Sie eine Antwort zurücksenden. Ich werde die Antwort aktualisieren und Ihnen Anerkennung zollen

    – Kevin

    8. Mai 2016 um 8:00 Uhr

X-Send-File.

X-Send-File ist eine interne Direktive, die Varianten für Apache, nginx und lighthttpd hat. Es erlaubt Ihnen komplett überspringen Verteilen einer Datei über PHP und ist eine Anweisung, die dem Webserver mitteilt, was er als Antwort anstelle der eigentlichen Antwort von FastCGI senden soll.

Ich habe mich damit schon einmal in einem persönlichen Projekt beschäftigt und wenn Sie die Summe meiner Arbeit sehen möchten, können Sie hier darauf zugreifen:
https://github.com/infinity-next/infinity-next/blob/master/app/Http/Controllers/Content/ImageController.php#L250-L450

Dies befasst sich nicht nur mit der Verteilung von Dateien, sondern auch mit der Suche nach Streaming-Medien. Sie können diesen Code frei verwenden.

Hier ist die offizielle nginx-Dokumentation auf X-Send-File.
https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/

Du tun Sie müssen Ihren Webserver bearbeiten und bestimmte Verzeichnisse als intern markieren, damit nginx sie einhalten kann X-Send-File Richtlinien.

Ich habe hier eine Beispielkonfiguration für Apache und Nginx für meinen obigen Code.
https://github.com/infinity-next/infinity-next/wiki/Installation

Dies wurde auf stark frequentierten Websites getestet. Tun nicht Puffern Sie Medien über einen PHP-Daemon, es sei denn, Ihre Website hat so gut wie keinen Traffic oder Sie verbrauchen Ressourcen.

  • Ich würde das wirklich gerne implementieren, aber ich bin mir nicht sicher über die Sicherheit. Können Sie erklären, ob Sie verwenden X-Send-File das Risiko hinzufügt, dass die Datei nicht autorisierten Clients zugänglich gemacht wird?

    – Džuris

    3. Mai 2016 um 18:33 Uhr

  • Sie können damit Controller-Richtlinien verwenden, weshalb ich die Lösung so liebe. Sie sollten sich jedoch bewusst sein, dass nginx und möglicherweise CDNs wie Cloudflare die Datei zwischenspeichern und an jeden verteilen können, der die URL hat.

    – Josch

    3. Mai 2016 um 18:38 Uhr

Sie könnten versuchen, die StreamedResponse-Komponente direkt anstelle des Laravel-Wrappers dafür zu verwenden. Gestreamte Antwort

https://www.php.net/readfile

<?php
$file="monkey.gif";

if (file_exists($file)) {
    header('Content-Description: File Transfer');
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="'.basename($file).'"');
    header('Expires: 0');
    header('Cache-Control: must-revalidate');
    header('Pragma: public');
    header('Content-Length: ' . filesize($file));
    readfile($file);
    exit;
}
?>

1186700cookie-checkLaden Sie vom Laravel-Speicher herunter, ohne die gesamte Datei in den Speicher zu laden

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy