PHP Conference Japan 2024

readfile

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

readfile輸出檔案

描述

readfile(string $filename, bool $use_include_path = false, ?resource $context = null): int|false

讀取檔案並將其寫入輸出緩衝區。

參數

filename

正在讀取的檔案名稱。

use_include_path

您可以使用選用的第二個參數,並將其設定為 true,如果您也想在 include_path 中搜尋該檔案。

context

內容串流 resource

傳回值

成功時傳回從檔案讀取的位元組數,失敗時傳回 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 則註解

66
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
60
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!');
}

?>
21
marro at email dot cz
16 年前
我的腳本在 IE6 和 Firefox 2 上能正確運作,適用於任何類型的檔案 (我希望是這樣 :))

function DownloadFile($file) { // $file = 包含路徑
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 上執行
8
levhita at gmail dot com
16 年前
關於 gaosipov 的 smartReadFile 函式的注意事項

將 preg_match 匹配的索引值更改為

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

否則,$begin 會被設定為整個匹配到的區段,而 $end 會被設定為應該是 begin 的值。

有關此詳細資訊,請參閱 preg_match。
12
Hayley Watson
17 年前
為了避免透過竄改請求,例如在 "filename" 中插入 "../" 等方式,讓使用者自行選擇下載的檔案,請記住 URL 不是檔案路徑,而且它們之間的映射關係不必如此直接,像 "download.php?file=thingy.mpg" 就會導致下載檔案 "thingy.mpg"。

這是你的腳本,你可以完全控制如何將檔案請求映射到檔案名稱,以及哪些請求會檢索哪些檔案。

但即便如此,一如既往,永遠不要相信請求中的任何內容。這是第一天上學的基本安全原則。
12
flobee at gmail dot com
19 年前
關於 php5
我發現 @php-dev 上已經有關於 readfile() 和 fpassthru() 的討論,其中只會傳送剛好 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;

}
?>
15
TimB
16 年前
對於任何在將大型檔案讀入記憶體時遇到 Readfile() 問題的人,問題不在於 Readfile() 本身,而是因為你啟用了輸出緩衝。只需在呼叫 Readfile() 之前立即關閉輸出緩衝即可。使用類似 ob_end_flush() 的方法。
5
Paulinator
6 年前
始終使用 MIME 類型 'application/octet-stream' 並非最佳做法。大多數(如果不是全部)瀏覽器都會直接以下載方式處理這種檔案類型。

如果你使用正確的 MIME 類型(和內嵌的 Content-Disposition),瀏覽器對於某些檔案會有更好的預設動作。例如,對於圖片,瀏覽器會顯示它們,這可能正是你想要的。

要使用正確的 MIME 類型傳送檔案,最簡單的方法是使用

header('Content-Type: ' . mime_content_type($file));
header('Content-Disposition: inline; filename="'.basename($file).'"');
6
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 位元組,則這將不會有任何成效。
1
jorensmerenjanu at gmail dot com
3 年前
對於任何遇到 HTML 頁面輸出到下載檔案中的問題,請在 readfile() 之前呼叫 ob_clean() 和 flush() 函式
3
daren -remove-me- schwenke
13 年前
如果你夠幸運,不是在使用共用主機,並且有 Apache,請考慮安裝 mod_xsendfile。
這是我發現唯一能保護和傳輸大型檔案(數 GB)的 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;
}
?>
3
chrisputnam at gmail dot com
19 年前
回覆 flowbee@gmail.com --

當使用這裡提到的 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();。請務必包含它!
0
simbiat at outlook dot com
3 年前
flobee.at.gmail.dot.com 分享了 "readfile_chunked" 函數。它確實有效,但您可能會在使用 "fread" 時遇到記憶體耗盡的問題。同時,"stream_copy_to_stream" 似乎使用了與 "readfile" 相同的記憶體量。至少,當我在 256M 記憶體限制下,針對我的 https://github.com/Simbiat/HTTP20 函式庫測試 1.5G 檔案的「下載」功能時,情況是如此:「fread」我得到了約 240M 的峰值記憶體使用量,而使用 "stream_copy_to_stream" 則約為 150M。
這並不意味著您可以完全避免記憶體耗盡的問題:如果您一次讀取太多,仍然可能會遇到。這就是為什麼在我的函式庫中,我使用輔助函式 ("speedLimit") 來計算選定的速度限制是否符合可用的記憶體 (同時允許一些空間)。
您可以閱讀程式碼中的註解以取得更多詳細資訊,並針對該函式庫提出問題 (如果您認為有任何地方不正確,尤其是因為它在撰寫本文時仍在開發中),但到目前為止,我能夠使用它獲得一致的行為。
0
mAu
18 年前
不要使用
<?php
header
('Content-Type: application/force-download');
?>
使用
<?php
header
('Content-Type: application/octet-stream');
?>
有些瀏覽器在強制下載方面存在問題。
-2
antispam [at] rdx page [dot] com
19 年前
只是一個注意事項:如果您使用 bw_mod (目前版本 0.6) 在 Apache 2 中限制頻寬,它在 readfile 事件期間*不會*限制頻寬。
-5
Brian
10 年前
如果您正在尋找一種允許您下載 (強制下載) 大型檔案的演算法,也許這一個可以幫助您。

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

// 關閉 session 以防止使用者等待
// 直到下載完成 (如果需要,請取消註解)
//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;
-2
Anonymous
5 年前
為了避免錯誤,
請小心 $file_name 參數開頭是否允許使用斜線 "/"。

在我的情況下,嘗試在存取記錄後透過 PHP 發送 PDF 檔案,
在 PHP 7.1 中必須移除開頭的 "/"。
-4
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 中加入了一個點擊 (下載) 計數器。當然,您需要設定資料庫、表格和欄位。請透過電子郵件聯絡我以取得完整的設定// Session 標記也是一種安全/記錄選項
用於連結的上下文中
http://www.yourdomain.com/download.php?id=xx&hit=1

[由 sp@php.net 編輯:新增了防止 SQL 注入的保護]
-4
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
-4
Thomas Jespersen
20 年前
請記住,如果您建立一個像下面提到的「強制下載」腳本,您需要對您的輸入進行清理!

我已經看過很多沒有測試的下載腳本,所以您可以在伺服器上下載任何您想要的東西。

特別要測試像 ".." 這樣的字串,這可能會導致目錄遍歷。如果可能,只允許字元 a-z、A-Z 和 0-9,並讓它只能從一個「下載資料夾」下載。
-3
TheDayOfCondor
19 年前
注意 - Rob Funk 建議的分塊讀取檔案很容易超過您最大腳本執行時間 (預設為 30 秒)。

我建議您在 while 迴圈內使用 set_time_limit 函數來重置 php 監視。
-5
Zambz
14 年前
如果您使用本文概述的程序強制將檔案傳送給使用者,您可能會發現某些伺服器上未傳送「Content-Length」標頭。

發生這種情況的原因是一些伺服器預設設定為啟用 gzip 壓縮,這會針對此類操作傳送額外的標頭。此額外的標頭是 "Transfer-Encoding: chunked",它基本上會覆蓋 "Content-Length" 標頭並強制進行分塊下載。當然,如果您使用本文中 readfile 的智慧型版本,則不需要此操作。

缺少 Content-Length 標頭表示以下情況

1) 您的瀏覽器不會在下載時顯示進度列,因為它不知道下載的長度
2) 如果您在 readfile 函數之後輸出任何內容 (例如,空白字元) (錯誤地),瀏覽器會將其添加到下載結尾,導致資料損毀。

停用此行為的最簡單方法是使用以下 .htaccess 指令。

SetEnv no-gzip dont-vary
-5
anon
8 年前
在 C 原始碼中,此函數只是以讀取+二進位模式開啟路徑,不帶鎖定,並使用 fpassthru()

如果您需要鎖定的讀取,請直接使用 fopen()、flock(),然後使用 fpassthru()。
-5
TheDayOfCondor
19 年前
我認為 readfile 會受到最大腳本執行時間的限制。即使超過預設的 30 秒限制,readfile 仍然會完成,然後腳本才會中止。
請注意,不僅在大檔案上,即使在使用者連線速度緩慢的情況下,小檔案也可能會出現非常奇怪的行為。

最好的做法是在使用 readfile 之前使用

<?
set_time_limit(0);
?>

,如果你打算使用 readfile 呼叫來傳輸檔案給使用者,這會完全禁用看門狗。
To Top