瀏覽器預設根據 Content-Type 決定顯示方式(如 PDF 內嵌顯示)。若要強制下載,需發送以下 Header 組合:
Content-Type: application/octet-stream 或 application/force-download:宣告為二進位串流,勿嘗試解析顯示。Content-Disposition: attachment; filename="...":指示為附件下載,並建議檔名。Content-Length: ...:告知檔案大小,支援進度條與斷點續傳。Cache-Control / Pragma:防止快取導致下載舊檔或空檔(特別是 IE)。<?php
/**
* 強制下載檔案
*
* @param string $filePath 檔案絕對路徑
* @param string $fileName 下載顯示檔名(可選,預設取原檔名)
* @return bool 成功輸出 true,檔案不存在 false
*/
function forceDownload($filePath, $fileName = '') {
// 1. 檔案存在性檢查
if (!is_file($filePath) || !is_readable($filePath)) {
http_response_code(404);
return false;
}
// 2. 取得檔案資訊
$fileName = $fileName ?: basename($filePath);
$fileSize = filesize($filePath);
$mimeType = mime_content_type($filePath) ?: 'application/octet-stream';
// 3. 清除輸出緩衝(防止前有輸出導致 Header 失效)
if (ob_get_level()) {
ob_end_clean();
}
// 4. 發送 Header
header('Content-Type: ' . $mimeType);
header('Content-Disposition: attachment; filename="' . rawurlencode($fileName) . '"');
header('Content-Length: ' . $fileSize);
header('Content-Transfer-Encoding: binary');
header('Accept-Ranges: bytes'); // 支援斷點續傳
// IE 相容快取控制
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
if (strpos($ua, 'MSIE') !== false || strpos($ua, 'Trident') !== false) {
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
} else {
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');
}
// 5. 分塊輸出(避免大檔案佔滿記憶體)
$handle = fopen($filePath, 'rb');
if ($handle) {
while (!feof($handle)) {
echo fread($handle, 8192); // 8KB 緩衝
flush(); // 立即推送給瀏覽器
}
fclose($handle);
}
return true;
}
// 使用範例
forceDownload('/var/www/files/report.pdf', '2024_Q1_Report.pdf');
?>
大檔案下載需支援 Range Header,允許瀏覽器/下載工具分段請求:
<?php
function forceDownloadWithRange($filePath, $fileName = '') {
if (!is_file($filePath)) { http_response_code(404); return; }
$fileSize = filesize($filePath);
$fileName = $fileName ?: basename($filePath);
// 解析 Range Header
$range = isset($_SERVER['HTTP_RANGE']) ? $_SERVER['HTTP_RANGE'] : '';
$start = 0;
$end = $fileSize - 1;
$statusCode = 200;
if (preg_match('/bytes=(\d+)-(\d*)/', $range, $matches)) {
$start = intval($matches[1]);
$end = $matches[2] ? intval($matches[2]) : $fileSize - 1;
if ($start > $end || $start >= $fileSize) {
header('HTTP/1.1 416 Range Not Satisfiable');
header('Content-Range: bytes */' . $fileSize);
return;
}
$statusCode = 206; // Partial Content
}
// Header
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . rawurlencode($fileName) . '"');
header('Content-Length: ' . ($end - $start + 1));
header('Accept-Ranges: bytes');
if ($statusCode === 206) {
header('HTTP/1.1 206 Partial Content');
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $fileSize);
}
// 輸出指定區間
$handle = fopen($filePath, 'rb');
fseek($handle, $start);
$remaining = $end - $start + 1;
$bufferSize = 8192;
while ($remaining > 0 && !feof($handle)) {
$read = min($bufferSize, $remaining);
echo fread($handle, $read);
$remaining -= $read;
flush();
}
fclose($handle);
}
?>
basename()、白名單驗證、或對應資料庫 ID 解析真實路徑。fread() 分塊輸出,禁用 readfile() 或 file_get_contents() 一次載入記憶體。max_execution_time,可在函數開頭 set_time_limit(0) 移除限制。強制下載的關鍵在於正確的 HTTP Header 組合與分塊輸出機制。封裝通用 forceDownload() 函數並嚴格執行安全檢查,即可滿足 PDF、Excel、圖片等各類檔案的下載需求,並支援斷點續傳提升大檔案下載體驗。