从 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

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-14 13:38:58  来源:igfitidea点击:

Download from Laravel storage without loading whole file in memory

phplaravel

提问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.c​​om/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

感谢@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-Fileis 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-Filedirectives.

必须修改你的网络服务器和标记特定的目录内部对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

https://www.php.net/readfile

https://www.php.net/readfile

<?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