2016-04-21 11 views
22

Sto utilizzando Laravel Storage e voglio servire agli utenti alcuni file (più grandi del limite di memoria). Il mio codice è stato ispirato da un post in SO e va in questo modo:Download dalla memoria di Laravel senza caricare l'intero file nella memoria

$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, mi imbatto in un errore per file di grandi dimensioni:

[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} 

sembra che tenta di caricare tutti i file in memoria. Mi aspettavo che l'uso di stream e passthru non lo farebbe ... C'è qualcosa che manca nel mio codice? Devo in qualche modo specificare la dimensione del blocco o cosa?

Le versioni che sto usando sono Laravel 5.1 e PHP 5.6.

+0

L'unico scenario che posso pensare a dove 'fpassthru' alloca in memoria è quando si utilizza il buffering dell'output. Potresti quindi provare un ciclo su 'fread' con un' echo'. – bishop

risposta

14

Sembra che il buffer di output stia ancora accumulando molta memoria.

Provare a disattivare ob prima di fare il fpassthru:

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

Potrebbe essere che ci sono più buffer di uscita attivo che è il motivo per cui è necessario il tempo.

+0

Questa risposta risolve il problema reale che stava causando problemi nel mio tentativo di implementazione, quindi accetto e ti premo la taglia. Grazie a tutti per le altre risposte che sono anche preziose informazioni! –

13

Invece di caricare l'intero file in memoria in una sola volta, provare a usare fread per leggere e inviarlo a blocchi.

Ecco un ottimo articolo: 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; 
?> 

Aggiornamento

Un modo più semplice per farlo: basta chiamare

if (ob_get_level()) ob_end_clean(); 

prima di ritornare una risposta.

credito al @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

Questo è esattamente ciò che serve a fpasstru, non c'è bisogno di complicare le cose. – Christiaan

+0

Io non la penso così .. Ho fatto un esperimento, '' 'fpassthru''' ha provocato esattamente lo stesso errore. Con questo metodo sono in grado di scaricare il file. – Kevin

+0

@Christiaan Ho aggiornato il codice nella mia risposta e potresti fare questo esperimento sul tuo computer. (basta generare un file grande da 20 GB) – Kevin

4

Si potrebbe provare a utilizzare il componente StreamedResponse direttamente, invece che l'involucro laravel per esso. StreamedResponse

4

X-Send-File.

X-Send-File è una direttiva interna che presenta varianti per Apache, nginx e lighthttpd. Ti permette di completamente saltare distribuendo un file tramite PHP ed è un'istruzione che dice al server web cosa inviare come risposta invece della risposta effettiva da FastCGI.

ho affrontato questo prima su un progetto personale e se si desidera visualizzare la somma del mio lavoro, è possibile accedere da qui:
https://github.com/infinity-next/infinity-next/blob/master/app/Http/Controllers/Content/ImageController.php#L250-L450

Si occupa non solo con la distribuzione di file, ma la manipolazione ricerca multimediale in streaming. Sei libero di usare quel codice.

Questa è la documentazione ufficiale di nginx su X-Send-File.
https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/

È fare devono modificare il proprio server web e segnare directory specifiche come interno per nginx di rispettare X-Send-File direttive.

Ho configurazione di esempio sia per Apache che per Nginx per il mio codice qui sopra.
https://github.com/infinity-next/infinity-next/wiki/Installation

Questo è stato testato su siti Web ad alto traffico. Do non media buffer attraverso un demone PHP a meno che il tuo sito non sia prossimo al traffico o stai sanguinando risorse.

+0

Mi piacerebbe davvero implementarlo, ma non sono sicuro della sicurezza. Puoi spiegare se l'uso di 'X-Send-File' aggiunge rischi di esposizione del file a client non autorizzati? –

+0

È possibile utilizzare le politiche del controllore con questo motivo per cui amo così tanto la soluzione. Tuttavia, dovresti sapere che nginx e potenzialmente CDN come Cloudflare possono memorizzare nella cache il file e distribuirlo a chiunque possieda l'URL. – Josh

Problemi correlati