简体   繁体   English

从 Laravel 存储下载而不将整个文件加载到内存中

[英]Download from Laravel storage without loading whole file in memory

I am using Laravel Storage and I want to serve users some (larger than memory limit) files.我正在使用 Laravel Storage,我想为用户提供一些(大于内存限制)文件。 My code was inspired from a post in SO and it goes like this:我的代码灵感来自 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。

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 的原因。

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'] . '"',
    ]);

X-Send-File . X-Send-File

X-Send-File is an internal directive that has variants for Apache, nginx, and lighthttpd. X-Send-File是一个内部指令,具有适用于 Apache、nginx 和 lighthttpd 的变体。 It allows you to completely skip distributing 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.它允许您完全跳过通过 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 .这是关于X-Send-File的官方 nginx 文档。
https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/ https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/

You do have 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.我在此处为上面的代码提供了 Apache 和 nginx 的示例配置。
https://github.com/infinity-next/infinity-next/wiki/Installation https://github.com/infinity-next/infinity-next/wiki/Installation

This has been tested on high-traffic websites.这已经在高流量网站上进行了测试。 Do not buffer media through a PHP Daemon unless your site has next to no traffic or you're bleeding resources.不要通过PHP守护缓冲介质,除非你的网站有旁边没有流量或你在流血资源。

You could try using the StreamedResponse component directly, instead of the Laravel wrapper for it.您可以尝试直接使用 StreamedResponse 组件,而不是 Laravel 包装器。 StreamedResponse 流响应

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM