在 Web 專案中提供檔案下載是常見需求。單純使用 readfile() 雖簡單,但無法處理大檔案記憶體溢位、斷點續傳(HTTP Range)與下載速度控制。本文提供一個生產級的 download() 函式,支援 2GB 以上大檔、斷點續傳、流式輸出與記憶體安全。
/**
* 安全下載檔案(支援斷點續傳、大檔、流式輸出)
*
* @param string $filePath 伺服器端檔案絕對路徑
* @param string $fileName 瀏覽器顯示的下載檔名
* @return void
*/
function download($filePath, $fileName) {
if (!is_file($filePath) || !is_readable($filePath)) {
header('HTTP/1.1 404 Not Found');
exit('檔案不存在或不可讀');
}
$fileSize = sprintf('%u', filesize($filePath)); // 相容 32 位系統 >2GB
$fileName = rawurlencode($fileName); // 解決中文檔名亂碼
// 基本下載標頭
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Content-Transfer-Encoding: binary');
header('Accept-Ranges: bytes'); // 宣告支援 Range
header('Pragma: public');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
// 處理 Range 斷點續傳
$rangeStart = 0;
$rangeEnd = $fileSize - 1;
if (isset($_SERVER['HTTP_RANGE'])) {
$range = $_SERVER['HTTP_RANGE'];
if (preg_match('/bytes=(\d+)-(\d*)/', $range, $matches)) {
$rangeStart = intval($matches[1]);
$rangeEnd = $matches[2] ? intval($matches[2]) : $fileSize - 1;
if ($rangeStart >= $fileSize || $rangeEnd >= $fileSize) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header('Content-Range: bytes */' . $fileSize);
exit;
}
header('HTTP/1.1 206 Partial Content');
header('Content-Length: ' . ($rangeEnd - $rangeStart + 1));
header('Content-Range: bytes ' . $rangeStart . '-' . $rangeEnd . '/' . $fileSize);
}
} else {
header('Content-Length: ' . $fileSize);
}
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', filemtime($filePath)) . ' GMT');
header('Connection: close');
// 流式輸出,避免佔用大量記憶體
$handle = fopen($filePath, 'rb');
if ($handle === false) {
header('HTTP/1.1 500 Internal Server Error');
exit('無法開啟檔案');
}
fseek($handle, $rangeStart);
$bufferSize = 1024 * 1024; // 1MB 緩衝
while (!feof($handle) && ($pos = ftell($handle)) <= $rangeEnd) {
$readSize = min($bufferSize, $rangeEnd - $pos + 1);
echo fread($handle, $readSize);
ob_flush();
flush();
}
fclose($handle);
exit;
}
// 呼叫方式 $filePath = '/var/www/uploads/report_2024.pdf'; $fileName = '2024年營運報告.pdf'; download($filePath, $fileName);
| 特性 | 實作方式 | 效益 |
|---|---|---|
| 大檔支援 | sprintf('%u', filesize()) + 64 位整數 |
突破 2GB 限制 |
| 斷點續傳 | 解析 HTTP_RANGE,回應 206 Partial Content + Content-Range |
支援下載工具多線程、網路中斷續傳 |
| 記憶體安全 | 分塊讀取(1MB)+ ob_flush()/flush() |
固定記憶體佔用,不隨檔案大小增長 |
| 中文檔名 | rawurlencode() + RFC 5987 相容 |
跨瀏覽器正確顯示 |
| 快取控制 | Last-Modified、ETag(可自行加入) |
瀏覽器/Proxy 正確快取 |
md5_file($filePath) 或 filemtime() + filesize() 組合,配合 If-None-Match 實現 304 Not Modified。usleep() 根據頻寬計算延遲。download() 前驗證使用者登入狀態、下載權限、防盜鏈。download() 前後無任何 echo、空白、BOM。建議純 PHP 檔案省略結尾標籤。fastcgi_read_timeout、proxy_read_timeout;關閉 gzip 對二進位流的壓縮。Content-Type 是否為 application/octet-stream,且 Content-Disposition 格式正確。