實作檔案下載需正確設定 HTTP 標頭,並依檔案大小選擇適當讀取策略,避免記憶體耗盡。
readfile() 直接輸出(中小檔案)<?php
function downloadFile(string $filePath, string $downloadName = ''): void
{
if (!is_file($filePath) || !is_readable($filePath)) {
http_response_code(404);
exit('檔案不存在或不可讀');
}
$downloadName = $downloadName ?: basename($filePath);
$fileSize = filesize($filePath);
// 清除輸出緩衝,防止前有輸出導致標頭失效
if (ob_get_level()) ob_end_clean();
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . rawurlencode($downloadName) . '"');
header('Content-Length: ' . $fileSize);
header('Accept-Ranges: bytes');
header('Cache-Control: private, must-revalidate');
header('Pragma: public');
header('Expires: 0');
readfile($filePath); // 直接輸出至瀏覽器
exit;
}
// 使用
downloadFile('/var/www/files/report.pdf', 'monthly_report_' . date('Ymd') . '.pdf');
<?php
function downloadLargeFile(string $filePath, string $downloadName = '', int $chunkSize = 8192): void
{
if (!is_file($filePath) || !is_readable($filePath)) {
http_response_code(404);
exit('檔案不存在');
}
$downloadName = $downloadName ?: basename($filePath);
$fileSize = filesize($filePath);
if (ob_get_level()) ob_end_clean();
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . rawurlencode($downloadName) . '"');
header('Content-Length: ' . $fileSize);
header('Accept-Ranges: bytes');
$handle = fopen($filePath, 'rb');
if (!$handle) {
http_response_code(500);
exit('無法開啟檔案');
}
while (!feof($handle) && connection_status() === CONNECTION_NORMAL) {
echo fread($handle, $chunkSize);
@ob_flush();
@flush();
}
fclose($handle);
// 檢查連線是否中斷
if (connection_aborted()) {
// 記錄未完成下載...
}
exit;
}
| 標頭 | 用途 |
|---|---|
| Content-Type: application/octet-stream | 通用二進位串流,強制下載而非瀏覽器開啟 |
| Content-Disposition: attachment | 提示下載對話框,指定檔名 |
| Content-Length | 告知瀏覽器檔案大小,支援進度條 |
| Accept-Ranges: bytes | 支援斷點續傳 |
| Cache-Control: private | 避免 Proxy 快取敏感檔案 |
// 相容大多數瀏覽器
$encodedName = rawurlencode($chineseName); // RFC 5987 標準
header("Content-Disposition: attachment; filename*=UTF-8''{$encodedName}; filename=\"fallback.txt\"");
../../etc/passwd)Range 標頭實作斷點續傳set_time_limit(0) 防止大檔案傳輸超時