使用PHP提供大文件服务

14

我正在尝试通过PHP脚本提供大文件的服务,这些文件不在Web可访问目录中,所以这是我能想到的最佳方式。

我一开始能够想到的唯一方法是将其加载到内存中(fopen、fread等),将头数据设置为正确的MIME类型,然后只需回显整个文件的内容。

问题在于,我必须一次性将这些大约700MB的文件全部加载到内存中,并保持其在下载完成之前一直存在。如果我可以在下载时按需流式传输需要的部分,那就太好了。

有什么好主意吗?


它们不可访问是因为身份验证吗? - mark
https://dev59.com/5HA65IYBdhLWcg3wqQc8 - Keyne Viana
9个回答

27

你不需要阅读整个内容 - 只需进入循环读取它,比如每次读取32Kb并将其发送为输出。更好的选择是使用fpassthru,它可以为您完成类似的操作....

$name = 'mybigfile.zip';
$fp = fopen($name, 'rb');

// send the right headers
header("Content-Type: application/zip");
header("Content-Length: " . filesize($name));

// dump the file and stop the script
fpassthru($fp);
exit;

如果您使用readfile,则可以更少地编写代码行,因为它不需要调用fopen...
$name = 'mybigfile.zip';

// send the right headers
header("Content-Type: application/zip");
header("Content-Length: " . filesize($name));

// dump the file and stop the script
readfile($name);
exit;

如果你想让内容更加可爱,你可以支持Content-Range头部,它允许客户端请求文件的特定字节范围。这对于向Adobe Acrobat提供PDF文件非常有用,因为它只请求需要呈现当前页面的文件块。这有点复杂,但是请参考此示例

谢谢!我没想到我可以在Adobe中使用Content-Range。 - David
fpassthru()被报告为内存占用量大,因为它将整个文件加载到内存中。对于大文件来说不好。 - cnvzmxcvmcx
无论是readfile还是fpassthru都会出现内存相关错误:Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 1883504640 bytes)... - Ionică Bizău

11

使用PHP发送大文件的最佳方法是使用X-Sendfile头。它允许Web服务器通过零拷贝机制(如sendfile(2))更快地提供文件。它受到了lighttpd和apache的支持,需要安装插件

示例:

$file = "/absolute/path/to/file"; // can be protected by .htaccess
header('X-Sendfile: '.$file);
header('Content-type: application/octet-stream');
header('Content-Disposition: attachment; filename="'.basename($file).'"');
// other headers ...
exit;
服务器读取 X-Sendfile 标头并发送文件。

6

虽然在过去,fpassthru()一直是我的首选,但实际上PHP手册推荐*使用readfile(),如果你只是将文件原样转储到客户端。

* "如果您只想将文件内容转储到输出缓冲区,而不是先修改它或寻找特定的偏移量,则可能希望使用readfile(),这样可以避免fopen()调用。" ——PHP手册


2
如果您的文件因路径不在 Web 服务器的 Web 服务目录(htdocs)中而无法访问,则可以在 Web 服务目录中创建符号链接(symlink)以将该文件夹链接到避免将所有流量通过 php 传递。
您可以执行以下操作:
ln -s /home/files/big_files_folder /home/www/htdocs

如果您有高流量,使用PHP来提供静态文件会慢得多,内存消耗将非常大,并且可能无法处理大量的请求。


1

奇怪的是,无论是fpassthru()还是readfile()都不能满足我的需求,总是出现内存错误。 我只好使用没有'f'的passthru()函数:

$name = 'mybigfile.zip';
// send the right headers
header("Content-Type: application/zip");
header("Content-Length: " . filesize($name));
// dump the file and stop the script
passthru('/bin/cat '.$filename);
exit;

这个程序执行Unix命令'cat'并将其输出发送到浏览器。

给slim的评论:你不能只是在webspace上放置一个符号链接,原因是安全性问题。


1

请查看fpassthru()。如this comment所述,在PHP的更新版本中,这应该在不将文件保存在内存中的情况下提供文件。


1

如果你想做得正确,仅仅使用PHP是不够的。你应该使用Nginx的X-Accel-Redirect (推荐) 或者Apache的X-Sendfile来提供文件服务,这些功能正是为此目的而设计的。

我会在这个回答中包含一些在这篇文章中找到的文本。

为什么不用PHP来提供文件服务:

  • 如果无脑操作,文件将被读入内存,然后被服务。如果文件很大,这可能会导致服务器内存不足。
  • 缓存头通常设置不正确。即使文件没有改变,这会导致Web浏览器多次重新下载文件。
  • 通常不会自动支持HEAD请求和范围请求。如果文件很大,提供此类文件会占用工作进程或线程。如果可用的工作者有限,则可能导致饥饿。增加工作者数量可能会导致服务器内存不足。

NGINX可以正确处理所有这些问题。因此,让我们在应用程序中处理权限检查,并让NGINX服务实际文件。这就是内部重定向的作用。其思想很简单:您可以像通常一样配置位置条目来服务常规文件。

将以下内容添加到nginx服务器块:

location /protected_files/ {
    internal;
    alias /var/www/my_folder_with_protected_files/;
}

在您的项目中,需要HTTP Foundation包:
composer require symfony/http-foundation
使用Nginx以PHP方式提供文件:
use Symfony\Component\HttpFoundation\BinaryFileResponse;

$real_path = '/var/www/my_folder_with_protected_files/foo.pdf';
$x_accel_redirect_path = '/protected_files/foo.pdf';

BinaryFileResponse::trustXSendfileTypeHeader();
$response = new BinaryFileResponse( $real_path );
$response->headers->set( 'X-Accel-Redirect', $accel_file );
$response->sendHeaders();
exit;

这应该是你开始的基础。

以下是一个更完整的示例,用于服务内联PDF:

use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;

$real_path = '/var/www/my_folder_with_protected_files/foo.pdf';
$x_accel_redirect_path = '/protected_files/foo.pdf';

$file = new File( $file_path );

BinaryFileResponse::trustXSendfileTypeHeader();
$response = new BinaryFileResponse( $file_path );
$response->setImmutable( true );
$response->setPublic();
$response->setAutoEtag();
$response->setAutoLastModified();
$response->headers->set( 'Content-Type', 'application/pdf' );
$response->headers->set( 'Content-Length', $file->getSize() );
$response->headers->set( 'X-Sendfile-Type', 'X-Accel-Redirect' );
$response->headers->set( 'X-Accel-Redirect', $accel_file );
$response->headers->set( 'X-Accel-Expires', 60 * 60 * 24 * 90 ); // 90 days
$response->headers->set( 'X-Accel-Limit-Rate', 10485760 ); // 10mb/s
$response->headers->set( 'X-Accel-Buffering', 'yes' );
$response->setContentDisposition( ResponseHeaderBag::DISPOSITION_INLINE, basename( $file_path ) ); // view in browser. Change to DISPOSITION_ATTACHMENT to download
$response->sendHeaders();
exit;

0

fpassthru() 的一个好处是,该函数不仅可以与文件一起使用,还可以与任何有效的句柄一起使用。例如套接字。

如果可能的话(就像 file_get_contents() 一样),readfile() 必须更快,因为它使用了操作系统缓存机制。

还有一个提示。fpassthru() 保持句柄打开,直到客户端获取内容(在慢速连接上可能需要相当长的时间),因此如果对该文件进行并行写入,则必须使用某些锁定机制。


0

Python的答案都很好。但是你不能制作一个可通过网络访问的目录,其中包含指向实际文件的符号链接吗?这可能需要一些额外的服务器配置,但应该可以工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接