从 Laravel 存储下载而不将整个文件加载到内存中
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/36778167/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Download from Laravel storage without loading whole file in memory
提问by D?uris
I am using Laravel Storage and I want to serve users some (larger than memory limit) files. My code was inspired from a postin SO and it goes like this:
我正在使用 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.'"',
]);
Unfourtunately, I run into an error for large files:
不幸的是,我遇到了大文件的错误:
[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}
It seems that it tries to load all of the file into memory. I was expecting that usage of stream and passthru would not do this... Is there something missing in my code? Do I have to somehow specify chunk size or what?
它似乎试图将所有文件加载到内存中。我期待流和通路的使用不会这样做......我的代码中是否缺少某些东西?我是否必须以某种方式指定块大小或什么?
The versions I am using are Laravel 5.1 and PHP 5.6.
我使用的版本是 Laravel 5.1 和 PHP 5.6。
采纳答案by Christiaan
It seems that output buffering is still building up a lot in memory.
似乎输出缓冲仍在内存中积累了很多。
Try disabling ob before doing the fpassthru:
在执行 fpassthru 之前尝试禁用 ob:
function() use($stream) {
while(ob_get_level() > 0) ob_end_flush();
fpassthru($stream);
},
It could be that there are multiple output buffers active that is why the while is needed.
可能有多个输出缓冲区处于活动状态,这就是需要 while 的原因。
回答by Kevin
Instead of loading the whole file into memory at once, try to use fread to read and send it chunk by chunk.
不要一次将整个文件加载到内存中,而是尝试使用 fread 逐块读取和发送它。
Here is a very good article: http://zinoui.com/blog/download-large-files-with-php
这是一篇非常好的文章: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
更新
A simpler way to do this: just call
一个更简单的方法来做到这一点:只需调用
if (ob_get_level()) ob_end_clean();
before returning a response.
在返回响应之前。
Credit to @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'] . '"',
]);
回答by Josh
X-Send-File
.
X-Send-File
.
X-Send-File
is an internal directive that has variants for Apache, nginx, and lighthttpd. It allows you to completely skipdistributing a file through PHP and is an instruction that tells the webserver what to send as a response instead of the actual response from the FastCGI.
X-Send-File
是一个内部指令,具有 Apache、nginx 和 lighthttpd 的变体。它允许您完全跳过通过 PHP 分发文件,并且是一条指令,它告诉网络服务器将发送什么作为响应而不是来自 FastCGI 的实际响应。
I've dealt with this before on a personal project and if you want to see the sum of my work, you can access it here:
https://github.com/infinity-next/infinity-next/blob/master/app/Http/Controllers/Content/ImageController.php#L250-L450
我之前在一个个人项目中处理过这个问题,如果你想查看我的工作总和,你可以在这里访问:https:
//github.com/infinity-next/infinity-next/blob/master/app /Http/Controllers/Content/ImageController.php#L250-L450
This deals not only with distributing files, but handling streaming media seeking. You are free to use that code.
这不仅涉及分发文件,还处理流媒体搜索。您可以自由使用该代码。
Here is the official nginx documentation on X-Send-File
.
https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/
这是 nginx 的官方文档X-Send-File
。
https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/
You dohave to edit your webserver and mark specific directories as internal for nginx to comply with X-Send-File
directives.
你也必须修改你的网络服务器和标记特定的目录内部对nginx的遵守X-Send-File
指令。
I have example configuration for both Apache and nginx for my above code here.
https://github.com/infinity-next/infinity-next/wiki/Installation
我在此处为上面的代码提供了 Apache 和 nginx 的示例配置。
https://github.com/infinity-next/infinity-next/wiki/Installation
This has been tested on high-traffic websites. Do notbuffer media through a PHP Daemon unless your site has next to no traffic or you're bleeding resources.
这已经在高流量网站上进行了测试。难道不是通过PHP守护缓冲介质,除非你的网站有旁边没有流量或你在流血资源。
回答by overburn
You could try using the StreamedResponse component directly, instead of the Laravel wrapper for it. StreamedResponse
您可以尝试直接使用 StreamedResponse 组件,而不是使用 Laravel 包装器。流响应
回答by jpswade
<?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;
}
?>
回答by mike rigley
2020 Laravel 7 there is a better way:
2020 Laravel 7 还有更好的办法:
return response()->download($pathToFile);
From Laravel docs: "The download method may be used to generate a response that forces the user's browser to download the file at the given path. The download method accepts a file name as the second argument to the method, which will determine the file name that is seen by the user downloading the file. Finally, you may pass an array of HTTP headers as the third argument to the method:
来自Laravel 文档: “下载方法可用于生成强制用户浏览器在给定路径下载文件的响应。下载方法接受文件名作为该方法的第二个参数,这将确定文件名这是用户下载文件时看到的。最后,您可以将一组 HTTP 标头作为第三个参数传递给该方法:
return response()->download($pathToFile);
return response()->download($pathToFile, $name, $headers);
return response()->download($pathToFile)->deleteFileAfterSend();
We also have streamed downloads which may suit more: Laravel docs
我们也有可能更适合的流式下载:Laravel docs