2024年PHP日本大会

readfile

(PHP 4, PHP 5, PHP 7, PHP 8)

readfile输出文件

描述

readfile(字符串 $filename, 布尔值 $use_include_path = false, ?资源 $context = null): 整数|false

读取文件并将其写入输出缓冲区。

参数

filename

要读取的文件名。

use_include_path

如果要搜索include_path中的文件,可以使用可选的第二个参数并将其设置为true

context

一个上下文流资源

返回值

成功时返回从文件中读取的字节数,失败时返回false

错误/异常

失败时,会发出E_WARNING

示例

示例 #1 使用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;
}
?>

上面的例子将输出类似于

Open / Save dialogue

注释

注意:

readfile() 本身不会出现内存问题,即使发送大型文件也是如此。如果遇到内存不足错误,请确保使用ob_get_level()关闭输出缓冲。

提示

如果启用了fopen 封装器,则可以使用 URL 作为此函数的文件名。有关如何指定文件名的更多详细信息,请参见fopen()。请参阅支持的协议和封装器,以获取有关各种封装器的功能、用法说明以及它们可能提供的任何预定义变量的信息的链接。

参见

添加注释

用户贡献的注释 24 条注释

riksoft at gmail dot com
10年前
对于那些在名称包含空格(例如“test test.pdf”)时遇到问题的用户,这是一个提示。

在示例中(99% 的情况下),您可以找到
header('Content-Disposition: attachment; filename='.basename($file));

但设置文件名的正确方法是使用引号(双引号)
header('Content-Disposition: attachment; filename="'.basename($file).'"' );

某些浏览器可能无需引号即可工作,但肯定不包括 Firefox,正如 Mozilla 所解释的那样,content-disposition 中的文件名引号符合 RFC
http://kb.mozillazine.org/Filenames_with_spaces_are_truncated_upon_download
yura_imbp at mail dot ru
16年前
如果您需要限制下载速率,请使用此代码

<?php
$local_file
= 'file.zip';
$download_file = 'name.zip';

// 设置下载速率限制 (=> 20.5 kb/s)
$download_rate = 20.5;
if(
file_exists($local_file) && is_file($local_file))
{
header('Cache-control: private');
header('Content-Type: application/octet-stream');
header('Content-Length: '.filesize($local_file));
header('Content-Disposition: filename='.$download_file);

flush();
$file = fopen($local_file, "r");
while(!
feof($file))
{
// 将当前文件部分发送到浏览器
print fread($file, round($download_rate * 1024));
// 将内容刷新到浏览器
flush();
// 休眠一秒钟
sleep(1);
}
fclose($file);}
else {
die(
'Error: The file '.$local_file.' does not exist!');
}

?>
marro at email dot cz
16年前
我的脚本在 IE6 和 Firefox 2 上可以正确运行任何类型的文件(我希望如此 :))

function DownloadFile($file) { // $file = include path
if(file_exists($file)) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename='.basename($file));
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . filesize($file));
ob_clean();
flush();
readfile($file);
exit;
}

}

在 Apache 2 (WIN32) PHP5 上运行
levhita at gmail dot com
16年前
关于 gaosipov 的 smartReadFile 函数的一个说明

更改 preg_match 匹配的索引为

$begin = intval($matches[1]);
if( !empty($matches[2]) ) {
$end = intval($matches[2]);
}

否则 $begin 将设置为匹配的整个部分,而 $end 将设置为应该是开始的部分。

有关此的更多详细信息,请参阅 preg_match。
Hayley Watson
17年前
为了避免用户通过篡改请求(例如在“文件名”中插入"../")来自行选择下载文件,只需记住URL并非文件路径,两者之间的映射无需像"download.php?file=thingy.mpg"导致下载"thingy.mpg"文件那样字面化。

这是你的脚本,你可以完全控制它如何将文件请求映射到文件名,以及哪些请求检索哪些文件。

但即使如此,一如既往,永远不要信任请求中的任何内容。这是安全的基本原则,如同第一天入学时学习的那样。
flobee at gmail dot com
19年前
关于php5
我发现php-dev上已经讨论过readfile()和fpassthru(),其中只会传送 exactly 2 MB 的数据。

因此,你可以在php5中使用以下方法来获取更大的文件
<?php
function readfile_chunked($filename,$retbytes=true) {
$chunksize = 1*(1024*1024); // 每块多少字节
$buffer = '';
$cnt =0;
// $handle = fopen($filename, 'rb');
$handle = fopen($filename, 'rb');
if (
$handle === false) {
return
false;
}
while (!
feof($handle)) {
$buffer = fread($handle, $chunksize);
echo
$buffer;
if (
$retbytes) {
$cnt += strlen($buffer);
}
}
$status = fclose($handle);
if (
$retbytes && $status) {
return
$cnt; // 像 readfile() 一样返回传送的字节数。
}
return
$status;

}
?>
TimB
16年前
对于任何在使用Readfile()读取大文件时遇到内存问题的用户来说,问题不在于Readfile()本身,而是因为你启用了输出缓冲。只需在调用Readfile()之前立即关闭输出缓冲即可。使用类似ob_end_flush()的函数。
Paulinator
6年前
始终使用MIME类型'application/octet-stream'并非最佳选择。大多数浏览器都会简单地下载此类型的文件。

如果使用正确的MIME类型(以及内联的Content-Disposition),浏览器将对其中一些类型有更好的默认操作。例如,对于图像,浏览器会显示它们,这可能是你想要的。

要使用正确的MIME类型传送文件,最简单的方法是使用

header('Content-Type: ' . mime_content_type($file));
header('Content-Disposition: inline; filename="'.basename($file).'"');
gaosipov at gmail dot com
16年前
发送支持HTTPRange的文件(部分下载)

<?php
function smartReadFile($location, $filename, $mimeType='application/octet-stream')
{ if(!
file_exists($location))
{
header ("HTTP/1.0 404 Not Found");
return;
}

$size=filesize($location);
$time=date('r',filemtime($location));

$fm=@fopen($location,'rb');
if(!
$fm)
{
header ("HTTP/1.0 505 Internal server error");
return;
}

$begin=0;
$end=$size;

if(isset(
$_SERVER['HTTP_RANGE']))
{ if(
preg_match('/bytes=\h*(\d+)-(\d*)[\D.*]?/i', $_SERVER['HTTP_RANGE'], $matches))
{
$begin=intval($matches[0]);
if(!empty(
$matches[1]))
$end=intval($matches[1]);
}
}

if(
$begin>0||$end<$size)
header('HTTP/1.0 206 Partial Content');
else
header('HTTP/1.0 200 OK');

header("Content-Type: $mimeType");
header('Cache-Control: public, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Accept-Ranges: bytes');
header('Content-Length:'.($end-$begin));
header("Content-Range: bytes $begin-$end/$size");
header("Content-Disposition: inline; filename=$filename");
header("Content-Transfer-Encoding: binary\n");
header("Last-Modified: $time");
header('Connection: close');

$cur=$begin;
fseek($fm,$begin,0);

while(!
feof($fm)&&$cur<$end&&(connection_status()==0))
{ print
fread($fm,min(1024*16,$end-$cur));
$cur+=1024*16;
}
}
?>

用法

<?php
smartReadFile
("/tmp/filename","myfile.mp3","audio/mpeg")
?>

对于大型文件,使用fread读取可能会很慢,但这是一种在严格范围内读取文件的方法。你可以修改它并添加fpassthru代替fread和while循环,但这会发送所有从开始处的数据——如果请求是从100MB文件中请求100到200字节的数据,这将没有意义。
jorensmerenjanu at gmail dot com
3年前
对于任何遇到下载文件中包含html页面输出问题的用户:在readfile()之前调用ob_clean()和flush()函数。
daren -remove-me- schwenke
13年前
如果你足够幸运,没有使用共享主机并且拥有apache,请考虑安装mod_xsendfile。
这是我发现的唯一一种既能保护又能使用PHP传输超大文件(千兆字节)的方法。
对于基本上任何文件,它也被证明要快得多。
自从关于此的其他说明以来,可用的指令已更改,XSendFileAllowAbove已替换为XSendFilePath,以允许对webroot外部的文件进行更多访问控制。

下载源代码。

使用以下命令安装:apxs -cia mod_xsendfile.c

将相应的配置指令添加到你的.htaccess或httpd.conf文件中
启用它
XSendFile on
# 将目标目录列入白名单。
XSendFilePath /tmp/blah

然后在你的脚本中使用它
<?php
$file
= '/tmp/blah/foo.iso';
$download_name = basename($file);
if (
file_exists($file)) {
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename='.$download_name);
header('X-Sendfile: '.$file);
exit;
}
?>
chrisputnam at gmail dot com
19年前
回复 [email protected] --

当使用此处提到的 readfile_chunked 函数处理大于约 10MB 的文件时,我仍然遇到内存错误。这是因为编写者遗漏了每次读取后的重要 flush() 函数。因此,这是正确的分块读取文件函数(实际上根本不是 readfile 函数,可能应该交叉发布到 passthru()、fopen() 和 popen(),以便浏览器可以找到此信息)

<?php
function readfile_chunked($filename,$retbytes=true) {
$chunksize = 1*(1024*1024); // 每块多少字节
$buffer = '';
$cnt =0;
// $handle = fopen($filename, 'rb');
$handle = fopen($filename, 'rb');
if (
$handle === false) {
return
false;
}
while (!
feof($handle)) {
$buffer = fread($handle, $chunksize);
echo
$buffer;
ob_flush();
flush();
if (
$retbytes) {
$cnt += strlen($buffer);
}
}
$status = fclose($handle);
if (
$retbytes && $status) {
return
$cnt; // 像 readfile() 一样返回已传送的字节数。
}
return
$status;

}
?>

我所做的只是在 echo 行之后添加了一个 flush();。请务必包含这个!
simbiat at outlook dot com
3年前
flobee.at.gmail.dot.com 分享了 "readfile_chunked" 函数。它确实有效,但是使用 "fread" 可能会遇到内存耗尽的情况。同时 "stream_copy_to_stream" 似乎使用了与 "readfile" 相同数量的内存。至少,当我测试我的 https://github.com/Simbiat/HTTP20 库中 1.5G 文件的 "下载" 函数时,在 256M 内存限制下是这样的情况:"fread" 的峰值内存使用量约为 ~240M,而使用 "stream_copy_to_stream" 则为 ~150M。
但这并不意味着你可以完全避免内存耗尽:如果你一次读取太多数据,你仍然可能会遇到这种情况。这就是为什么在我的库中我使用一个辅助函数 ("speedLimit") 来计算所选的速度限制是否适合可用内存(同时允许一些剩余空间)。
你可以阅读代码本身中的注释以了解更多详情,如果认为代码中存在错误(尤其是在撰写本文时它还是工作进度中),可以为该库提出问题,但到目前为止,我能够获得其一致的行为。
mAu
18年前
不要使用
<?php
header
('Content-Type: application/force-download');
?>
而使用
<?php
header
('Content-Type: application/octet-stream');
?>
有些浏览器在强制下载方面存在问题。
antispam [at] rdx page [dot] com
19年前
注意:如果你使用 bw_mod(当前版本 0.6)来限制 Apache 2 中的带宽,它将*不会*限制 readfile 事件期间的带宽。
Brian
10年前
如果你正在寻找一种允许你下载(强制下载)大型文件的算法,以下算法可能对你有帮助。

$filename = "file.csv";
$filepath = "/path/to/file/" . $filename;

// 关闭会话以防止用户等待
// 下载完成(如果需要,取消注释)
//session_write_close();

set_time_limit(0);
ignore_user_abort(false);
ini_set('output_buffering', 0);
ini_set('zlib.output_compression', 0);

$chunk = 10 * 1024 * 1024; // 每块字节数 (10 MB)

$fh = fopen($filepath, "rb");

if ($fh === false) {
echo "无法打开文件";
}

header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($filepath));

// 重复读取直到 EOF
while (!feof($fh)) {
echo fread($handle, $chunk);

ob_flush(); // 刷新输出
flush();
}

exit;
Anonymous
5年前
为避免错误,
请注意 $file_name 参数开头是否允许斜杠 "/"。

在我的案例中,尝试在访问日志记录后通过 PHP 发送 PDF 文件,
在 PHP 7.1 中必须删除开头的 "/"。
planetmaster at planetgac dot com
19年前
使用强制下载脚本的部分代码,添加 MySQL 数据库函数,并隐藏文件位置以增强安全性,这正是我们需要的,可以从会员创作中下载 wmv 文件,而无需提示媒体播放器,同时也保护文件本身,并且只使用数据库查询。如下所示,可以高度定制以实现私有访问、远程文件和在线媒体的排序。

<?
# 保护脚本免受 SQL 注入攻击
$fileid=intval($_GET[id]);
# 设置 SQL 语句
$sql = " SELECT id, fileurl, filename, filesize FROM ibf_movies WHERE id=' $fileid' ";

# 执行 SQL 语句
$res = mysql_query($sql);

# 显示结果
while ($row = mysql_fetch_array($res)) {
$fileurl = $row['fileurl'];
$filename= $row['filename'];
$filesize= $row['filesize'];

$file_extension = strtolower(substr(strrchr($filename,"."),1));

switch ($file_extension) {
case "wmv": $ctype="video/x-ms-wmv"; break;
default: $ctype="application/force-download";
}

// IE 所需,否则忽略 Content-disposition
if(ini_get('zlib.output_compression'))
ini_set('zlib.output_compression', 'Off');

header("Pragma: public");
header("Expires: 0");
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Cache-Control: private",false);
header("Content-Type: video/x-ms-wmv");
header("Content-Type: $ctype");
header("Content-Disposition: attachment; filename=\"".basename($filename)."\";");
header("Content-Transfer-Encoding: binary");
header("Content-Length: ".@filesize($filename));
set_time_limit(0);
@readfile("$fileurl") or die("文件未找到。");

}

$donwloaded = "downloads + 1";

if ($_GET["hit"]) {
mysql_query("UPDATE ibf_movies SET downloads = $donwloaded WHERE id=' $fileid'");

}

?>

顺便说一下,我在 download.php 中添加了一个点击(下载)计数器。当然,你需要设置数据库、表和列。请发邮件给我以获取完整的设置 // 会话标记也是一个安全/日志记录选项
用于链接的上下文
http://www.yourdomain.com/download.php?id=xx&hit=1

[[email protected] 编辑:添加了针对 SQL 注入的保护]
peavey at pixelpickers dot com
19年前
也可以通过使用以下方法进行独立于 MIME 类型的强制下载

<?
(...)
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); // 过去某一天
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Content-type: application/x-download");
header("Content-Disposition: attachment; filename={$new_name}");
header("Content-Transfer-Encoding: binary");
?>

干杯!

Peavey
Thomas Jespersen
20年前
请记住,如果你创建了如下所述的“强制下载”脚本,请**清理你的输入!**

我见过很多下载脚本没有进行测试,因此你可以下载服务器上的任何文件。

尤其要测试诸如 ".." 之类的字符串,因为这可能会导致目录遍历。如果可能,只允许 a-z、A-Z 和 0-9 字符,并使其只能从一个“下载文件夹”下载。
TheDayOfCondor
19年前
注意 - Rob Funk 建议的分块 readfile 可能会轻松超过你的最大脚本执行时间(默认为 30 秒)。

我建议你在 while 循环内使用 set_time_limit 函数来重置 php 监视程序。
Zambz
14年前
如果你正在使用本文中概述的流程强制将文件发送给用户,你可能会发现某些服务器上没有发送“Content-Length”标头。

发生这种情况的原因是某些服务器默认情况下设置为启用 gzip 压缩,这会为此类操作发送额外的标头。这个额外的标头是“Transfer-Encoding: chunked”,它实际上会覆盖“Content-Length”标头并强制进行分块下载。当然,如果你使用本文中智能版本的 readfile,则不需要这样做。

缺少 Content-Length 标头意味着以下几点:

1) 你的浏览器不会显示下载进度条,因为它不知道文件的长度。
2) 如果你在 readfile 函数之后输出任何内容(例如空格)(错误操作),浏览器会将其添加到下载文件的末尾,导致数据损坏。

禁用此行为最简单的方法是使用以下 .htaccess 指令。

SetEnv no-gzip dont-vary
anon
8年前
在 C 源代码中,此函数只是以读+二进制模式打开路径,不加锁,并使用 fpassthru()。

如果你需要加锁读取,请直接使用 fopen()、flock(),然后使用 fpassthru()。
TheDayOfCondor
19年前
我认为 readfile 受最大脚本执行时间的限制。即使 readfile 超过了默认的 30 秒限制,也会完成,然后脚本被中止。
请注意,不仅在大文件上,即使在小文件上,如果用户连接速度慢,你也会遇到非常奇怪的行为。

最好的办法是使用

<?
set_time_limit(0);
?>

在 readfile 之前,如果你打算使用 readfile 调用将文件传输给用户,则完全禁用监视程序。
To Top