Загрузите из хранилища Laravel без загрузки всего файла в память

Я использую Laravel Storage, и я хочу обслуживать пользователей некоторыми файлами (большими, чем память). Мой код был вдохновлен на сообщение в 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.'"', ]); 

К сожалению, я столкнулся с ошибкой для больших файлов:

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

Кажется, он пытается загрузить весь файл в память. Я ожидал, что использование stream и passthru не сделает этого … Есть ли что-то в моем коде? Должен ли я как-то указать размер куска или что?

Используемые мной версии – Laravel 5.1 и PHP 5.6.

Solutions Collecting From Web of "Загрузите из хранилища Laravel без загрузки всего файла в память"

Похоже, что буферизация вывода все еще сильно накапливается в памяти.

Попробуйте отключить ob перед выполнением fpassthru:

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

Возможно, что есть несколько активных буферов вывода, поэтому требуется время.

Вместо того, чтобы сразу загружать весь файл в память, попробуйте использовать fread для чтения и отправки куска куском.

Вот очень хорошая статья: 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. (eg 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; ?> 

Обновить

Простейший способ сделать это: просто позвонить

 if (ob_get_level()) ob_end_clean(); 

перед возвратом ответа.

Кредит @ 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'] . '"', ]); 

Вы можете попробовать напрямую использовать компонент StreamedResponse, а не оболочку Laravel для него. StreamedResponse

X-Send-File .

X-Send-File – это внутренняя директива, которая имеет варианты для Apache, nginx и lighthttpd. Это позволяет полностью пропускать распространение файла через PHP и является инструкцией, которая сообщает веб-серверу, что отправить в ответ, а не фактический ответ от FastCGI.

Я имел дело с этим раньше в личном проекте, и если вы хотите увидеть сумму моей работы, вы можете получить к ней доступ:
https://github.com/infinity-next/infinity-next/blob/master/app/Http/Controllers/Content/ImageController.php#L250-L450

Это касается не только распространения файлов, но и обработки потокового медиа-поиска. Вы можете использовать этот код.

Вот официальная документация nginx по X-Send-File .
https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/

Вам нужно отредактировать веб-сервер и пометить определенные каталоги как внутренние для nginx, чтобы соответствовать директивам X-Send-File .

У меня есть пример конфигурации для Apache и nginx для моего выше кода здесь.
https://github.com/infinity-next/infinity-next/wiki/Installation

Это было протестировано на сайтах с высоким трафиком. Не буферизуйте носители через PHP Daemon, если на вашем сайте нет трафика или у вас есть кровоточащие ресурсы.