2016-04-21 9 views
22

Ich benutze Laravel Storage und ich möchte Benutzer einige (größere als Speicherlimit) Dateien dienen. Mein Code wurde von einem post in SO inspiriert und es geht so:Download von Laravel Speicher ohne Laden der gesamten Datei im Speicher

$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.'"', 
    ]); 

Unfourtunately, ich laufe in einen Fehler für große Dateien:

[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, alle die laden Datei in den Speicher. Ich hatte erwartet, dass die Verwendung von Stream und Passhru dies nicht tun würde ... Gibt es etwas in meinem Code fehlt? Muss ich irgendwie die Chunkgröße angeben oder was?

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

+0

Das einzige Szenario, das ich mir vorstellen kann, wo 'fpassthru' in den Speicher zuweist, ist, wenn die Ausgabepufferung verwendet wird. Sie können daher eine Schleife auf 'fread' mit einem' echo' versuchen. – bishop

Antwort

14

Es scheint, dass die Pufferung der Ausgaben noch viel im Speicher aufbaut.

Try ob deaktivieren, bevor Sie die fpassthru tun:

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

Es könnte sein, dass es mehrere Ausgabepuffer aktiv, deshalb ist die während benötigt wird.

+0

Diese Antwort bezieht sich auf das eigentliche Problem, das bei meiner versuchten Implementierung Probleme verursacht hat, daher akzeptiere ich Sie und schenke Ihnen das Kopfgeld. Danke an alle für die anderen Antworten, die auch wertvolle Informationen sind! –

13

Anstatt die gesamte Datei sofort in den Speicher zu laden, versuchen Sie, fread zu verwenden, um sie Stück für Stück 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

Ein einfacher Weg, dies zu tun: nur

if (ob_get_level()) ob_end_clean(); 

aufrufen, bevor eine Antwort zurück.

Credit @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'] . '"', 
    ]); 
+1

Das ist genau das, wofür fpasstru ist, keine Notwendigkeit, die Dinge zu komplizieren. – Christiaan

+0

Ich glaube nicht .. Ich habe ein Experiment gemacht, '' 'fpassthru''' ergab genau den gleichen Fehler. Mit dieser Methode kann ich die Datei herunterladen. – Kevin

+0

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

4

könnten Sie versuchen, die StreamedResponse Komponente direkt verwenden, anstatt der Laravel Wrapper für sie. StreamedResponse

4

X-Send-File.

X-Send-File ist eine interne Direktive mit Varianten für Apache, nginx und lighthttpd. Es ermöglicht Ihnen vollständig zu überspringen, eine Datei über PHP zu verteilen und ist eine Anweisung, die dem Webserver mitteilt, was als Antwort anstelle der tatsächlichen Antwort vom FastCGI gesendet werden soll.

ich mit diesem, bevor sie auf ein persönliches Projekt beschäftigt haben, und wenn Sie die Summe meiner Arbeit sehen möchten, können Sie es hier zugreifen:
https://github.com/infinity-next/infinity-next/blob/master/app/Http/Controllers/Content/ImageController.php#L250-L450

Diese betrifft nicht nur die Verteilung von Dateien, aber Handhabung Streaming-Medien suchen. Sie können diesen Code verwenden.

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

Sie tun müssen Ihren Webserver bearbeiten und bestimmte Verzeichnisse als interne markieren für nginx mit X-Send-File Richtlinien einzuhalten.

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

Dies wurde auf stark frequentierten Websites getestet. Do nicht Puffermedien durch einen PHP-Daemon, es sei denn, Ihre Website hat fast keinen Datenverkehr oder Sie bluten Ressourcen.

+0

Ich würde das gerne umsetzen, aber ich bin mir nicht sicher über die Sicherheit. Können Sie erklären, ob die Verwendung von 'X-Send-File' das Risiko birgt, die Datei nicht autorisierten Clients zugänglich zu machen? –

+0

Sie können Controller-Richtlinien damit verwenden, weshalb ich die Lösung so sehr liebe. Sie sollten sich jedoch darüber im Klaren sein, dass nginx und möglicherweise CDNs wie Cloudflare die Datei zwischenspeichern und an jeden mit der URL verteilen können. – Josh

Verwandte Themen