實現檔案下載的關鍵在於發送正確的 HTTP Header,告訴瀏覽器「這是一個附件,請彈出下載對話框」而非直接顯示。核心 Header 包含:
Content-Type: application/octet-stream —— 通用二進位流,強制下載Content-Disposition: attachment; filename="xxx" —— 指定下載檔名Content-Length: 檔案大小 —— 支援續傳與進度條readfile() 一行搞定(推薦中小檔案)PHP 內建 readfile() 直接將檔案輸出至輸出緩衝,記憶體效率高,程式碼最簡潔:
<?php
function downloadFile($filePath, $downloadName = '') {
if (!is_file($filePath)) { http_response_code(404); exit('檔案不存在'); }
$downloadName = $downloadName ?: basename($filePath);
$fileSize = filesize($filePath);
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $downloadName . '"');
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);
readfile($filePath);
exit;
}
// 使用範例
downloadFile('/path/to/report.pdf', '2024_Q1_Report.pdf');
?>
fread()(大檔案、低記憶體環境)避免一次性將大檔案載入記憶體,適合幾百 MB 以上檔案:
<?php
function downloadLargeFile($filePath, $downloadName = '') {
if (!is_file($filePath)) { http_response_code(404); exit('檔案不存在'); }
$downloadName = $downloadName ?: basename($filePath);
$fileSize = filesize($filePath);
$handle = fopen($filePath, 'rb');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $downloadName . '"');
header('Content-Length: ' . $fileSize);
header('Accept-Ranges: bytes'); // 支援斷點續傳
while (!feof($handle) && connection_status() === CONNECTION_NORMAL) {
echo fread($handle, 8192); // 8KB 分塊輸出
@ob_flush(); @flush(); // 立即推送給瀏覽器
}
fclose($handle);
exit;
}
?>
關鍵細節:connection_status() 檢測用戶是否中斷下載,避免無謂資源佔用;ob_flush() + flush() 確保資料即時傳輸。
將檔案傳輸交給 Web Server(Nginx/Apache)處理,PHP 僅發送指令,完全不佔用 PHP 程序記憶體與執行緒:
<?php
// Nginx 需在 location 區塊啟用:proxy_request_buffering off; + X-Accel-Redirect
// Apache 需載入 mod_xsendfile 並啟用 XSendFile On
function downloadViaWebServer($internalPath, $downloadName = '') {
$downloadName = $downloadName ?: basename($internalPath);
header('Content-Disposition: attachment; filename="' . $downloadName . '"');
// Nginx 使用內部路徑(需對應 internal location)
header('X-Accel-Redirect: ' . $internalPath);
// Apache 使用
// header('X-Sendfile: ' . realpath($internalPath));
exit;
}
?>
優勢:極高併發、支援斷點續傳、PHP 進程瞬間釋放。注意:需在 Web Server 端配置對應模組與權限。
| 問題 | 原因 | 解決 |
|---|---|---|
| 下載檔案損壞/多出空白行 | PHP 標籤前後有輸出、BOM、錯誤訊息混入 | 純 PHP 檔案省略 ?>;開啟 ob_start();確保無 BOM |
| 中文檔名亂碼 | Header 編碼不一致 | 使用 RFC 5987 格式:filename*=UTF-8'' . rawurlencode($name) |
| 大檔案記憶體耗盡 | 使用 file_get_contents() 或變數儲存整個檔案 | 改用 readfile() 或分塊 fread() |
| 無法續傳 | 缺少 Accept-Ranges、Content-Range 處理 | 實作 Range Request 解析或直接用 X-Sendfile |
readfile() 方案最平衡X-Accel-Redirect