瀏覽器預設會嘗試直接顯示圖片、HTML、PDF 等已知 MIME 類型。透過發送特定 Header 強制「另存新檔」對話框:
header('Content-Type: application/octet-stream'); // 通用二進位流
header('Content-Disposition: attachment; filename="test.gif"'); // 附件模式+檔名
header('Content-Length: ' . filesize($file)); // 檔案大小(支援進度條)
header('Accept-Ranges: bytes'); // 支援斷點續傳
function downloadFile(string $path, string $filename = ''): void
{
if (!file_exists($path)) {
http_response_code(404);
exit('檔案不存在');
}
$filename = $filename ?: basename($path);
$size = filesize($path);
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Length: ' . $size);
header('Cache-Control: private, must-revalidate'); // 避免代理快取干擾
header('Pragma: public');
readfile($path); // 直接輸出檔案內容
exit;
}
// 使用
downloadFile('/data/uploads/report.pdf', '2024_Q1_Report.pdf');
RFC 5987 標準支援 UTF-8 編碼檔名,舊版 IE 需要 fallback:
function setDownloadFilename(string $filename): void
{
$encoded = rawurlencode($filename);
// 現代瀏覽器:UTF-8 編碼
// 舊版 IE:不支援 encoding,需用 urlencode
header("Content-Disposition: attachment; filename*=UTF-8''{$encoded}; filename=\"{$encoded}\"");
}
// 測試
setDownloadFilename('測試報告_2024.pdf');
readfile() 會將整個檔案讀入記憶體,大檔案需分塊輸出:
function downloadLargeFile(string $path, string $filename = ''): void
{
if (!file_exists($path)) { http_response_code(404); exit('檔案不存在'); }
$filename = $filename ?: basename($path);
$size = filesize($path);
$handle = fopen($path, 'rb');
if (!$handle) { http_response_code(500); exit('無法開啟檔案'); }
// Header
header('Content-Type: application/octet-stream');
setDownloadFilename($filename);
header('Content-Length: ' . $size);
header('Accept-Ranges: bytes');
// 斷點續傳支援
if (isset($_SERVER['HTTP_RANGE'])) {
$range = $_SERVER['HTTP_RANGE'];
if (preg_match('/bytes=(\d+)-(\d*)/', $range, $matches)) {
$start = (int)$matches[1];
$end = $matches[2] ? (int)$matches[2] : $size - 1;
$length = $end - $start + 1;
fseek($handle, $start);
header('HTTP/1.1 206 Partial Content');
header("Content-Range: bytes $start-$end/$size");
header('Content-Length: ' . $length);
}
}
// 分塊輸出 (8KB per chunk)
while (!feof($handle) && connection_status() === CONNECTION_NORMAL) {
echo fread($handle, 8192);
@ob_flush();
@flush();
}
fclose($handle);
exit;
}
生產環境建議讓 Nginx/Apache 直接傳檔,PHP 僅驗證權限:
// Nginx 設定 (nginx.conf)
location /protected/ {
internal; // 僅允許內部重導向
alias /data/files/; // 實體檔案路徑
}
// PHP 端
function sendFileViaNginx(string $internalPath, string $filename): void
{
// 權限檢查...
header('X-Accel-Redirect: /protected/' . $internalPath);
header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"');
header('Content-Type: application/octet-stream');
exit;
}
Cache-Control: private 避免代理快取敏感檔案