我正在使用Laravel Storage,我想为用户提供一些(大于内存限制)文件。我的代码的灵感来自SO中的post,它是这样的:
$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。
答案 0 :(得分:16)
似乎输出缓冲仍在内存中积累很多。
尝试在执行fpassthru之前禁用ob:
function() use($stream) {
while(ob_get_level() > 0) ob_end_flush();
fpassthru($stream);
},
可能有多个输出缓冲区处于活动状态,这就是为什么需要while。
答案 1 :(得分:13)
不要一次将整个文件加载到内存中,而是尝试使用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;
?>
更简单的方法:只需调用
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'] . '"',
]);
答案 2 :(得分:5)
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
这不仅涉及分发文件,还涉及处理流媒体搜索。您可以自由使用该代码。
以下是关于X-Send-File
的官方nginx文档
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守护进程不缓冲媒体,除非您的网站没有任何流量,或者您正在流失资源。
答案 3 :(得分:4)
您可以尝试直接使用StreamedResponse组件,而不是Laravel包装器。 StreamedResponse
答案 4 :(得分:0)
<?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;
}
?>
答案 5 :(得分:0)
2020 Laravel 7有更好的方法:
return response()->download($pathToFile);
来自package file: “下载方法可用于生成强制用户浏览器在给定路径上下载文件的响应。下载方法接受文件名作为该方法的第二个参数,该参数将确定可以看到用户正在下载文件。最后,您可以将HTTP标头数组作为方法的第三个参数传递:
return response()->download($pathToFile);
return response()->download($pathToFile, $name, $headers);
return response()->download($pathToFile)->deleteFileAfterSend();
我们还流式传输了可能更适合的下载内容:Laravel docs