Загрузите из хранилища 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.
Ответы
Ответ 1
Кажется, что буферизация вывода все еще много увеличивает память.
Попробуйте отключить ob перед выполнением fpassthru:
function() use($stream) {
while(ob_end_flush());
fpassthru($stream);
},
Возможно, существует несколько активных буферов вывода, поэтому требуется время.
Ответ 2
Вместо того, чтобы сразу загружать весь файл в память, попробуйте использовать 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.
(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;
?>
Update
Простейший способ сделать это: просто вызовите
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'] . '"',
]);
Ответ 3
Вы можете попробовать напрямую использовать компонент StreamedResponse, а не оболочку Laravel для него. StreamedResponse
Ответ 4
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-демон, если у вашего сайта нет трафика или у вас кровоточат ресурсы.