預設瀏覽器會直接顯示圖片、PDF。若需強制下載(如相簿打包、報表匯出),必須送出正確的 HTTP 標頭。本文提供完整、安全的下載實作。
<?php
// download.php
$file = __DIR__ . '/files/' . basename($_GET['file'] ?? ''); // 僅允許檔名,防目錄遍歷
if (!is_file($file)) { http_response_code(404); exit('File not found'); }
$filename = basename($file);
$size = filesize($file);
$mime = mime_content_type($file) ?: 'application/octet-stream';
header('Content-Description: File Transfer');
header('Content-Type: ' . $mime);
header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"; filename*=UTF-8\'\'' . rawurlencode($filename));
header('Content-Length: ' . $size);
header('Content-Transfer-Encoding: binary');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Expires: 0');
readfile($file); // PHP 內建分塊輸出,不佔記憶體
exit;
?>
<a href="download.php?file=photo.jpg">下載照片</a> <a href="download.php?file=report.pdf">下載報表</a>
| 標頭 | 用途 | 關鍵值 |
|---|---|---|
| Content-Disposition | 指示瀏覽器處理方式 | attachment; filename="..." |
| Content-Type | MIME 類型 | mime_content_type() 自動偵測 |
| Content-Length | 檔案大小,支援進度條 | filesize() |
| filename* | RFC 5987 編碼,支援中文檔名 | UTF-8''encoded_name |
readfile() 內建 8KB 緩衝區串流輸出,**不會**將整個檔案載入記憶體。fopen() + fread() 迴圈 + ob_flush()/flush()。@ini_set('output_buffering', 'Off'); @ini_set('zlib.output_compression', 'Off');。basename()、realpath() 確認在允許目錄下。jpg,png,pdf,zip 等,拒絕 .php,.sh,.exe。Referer 或簽名 URL (簽名含過期時間)。極大檔案 (GB 級) 由 Web Server 直接傳送,PHP 僅驗證權限:
// PHP
header('X-Accel-Redirect: /protected/files/' . $filename); // Nginx
header('X-Sendfile: /var/www/protected/files/' . $filename); // Apache (mod_xsendfile)
header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"');
exit;
// Nginx 設定
location /protected/ {
internal;
alias /var/www/protected/;
}
優點:零 PHP 記憶體/CPU 佔用、支援斷點續傳、高併發。
| 現象 | 原因 | 解決 |
|---|---|---|
| 瀏覽器仍預覽 | Content-Type 為 image/*, 且無 attachment | 確認送出 Content-Disposition: attachment |
| 中文檔名亂碼 | 未用 RFC 5987 filename* | 雙重提供 filename 與 filename* |
| 下載檔案 0 bytes | 路徑錯誤、權限不足、readfile 失敗 | 檢查 is_file()、is_readable() |
| 記憶體耗盡 | 用 file_get_contents() 載入大檔 | 改用 readfile() 或 X-Sendfile |