<?php
// download.php?file=large.zip
$file = __DIR__ . '/files/' . basename($_GET['file'] ?? '');
if (!is_file($file)) {
http_response_code(404);
exit('File not found');
}
$size = filesize($file);
$filename = basename($file);
// 1. 基礎標頭
header('Cache-Control: public');
header('Accept-Ranges: bytes');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"');
// 2. 處理 Range 請求
$start = 0;
$end = $size - 1;
if (isset($_SERVER['HTTP_RANGE'])) {
// 解析 bytes=start-end
$range = $_SERVER['HTTP_RANGE'];
if (preg_match('/^bytes=(\d*)-(\d*)$/i', $range, $m)) {
$start = $m[1] !== '' ? (int)$m[1] : 0;
$end = $m[2] !== '' ? (int)$m[2] : $size - 1;
// 邊界修正
$start = max(0, min($start, $size - 1));
$end = max($start, min($end, $size - 1));
// 3. 範圍回應標頭
http_response_code(206);
header("Content-Range: bytes {$start}-{$end}/{$size}");
header("Content-Length: " . ($end - $start + 1));
}
} else {
// 全檔下載
header("Content-Length: {$size}");
}
// 4. 輸出檔案內容
$fp = fopen($file, 'rb');
fseek($fp, $start);
while (!feof($fp) && $start <= $end && !connection_aborted()) {
$readSize = min(8192, $end - $start + 1);
$data = fread($fp, $readSize);
if ($data === false || $data === '') break;
echo $data;
@flush();
$start += strlen($data);
}
fclose($fp);
basename() 防目錄遍歷,is_file() 確認存在Accept-Ranges: bytes 告訴瀏覽器支援斷點續傳bytes=start-end,支援 bytes=500- (從 500 到結尾) 和 bytes=-500 (最後 500 位元組)bytes start-end/total 格式,必須精確flush() 即時發送,connection_aborted() 檢測斷線# 完整下載
curl -O https://example.com/download.php?file=test.zip
# 只下載前 100KB
curl -H "Range: bytes=0-102399" -o part1.zip https://example.com/download.php?file=test.zip
# 續傳 (從 100KB 開始)
curl -H "Range: bytes=102400-" -o part2.zip https://example.com/download.php?file=test.zip
# 合併
cat part1.zip part2.zip > full.zip
// HTTP 支援多範圍: bytes=0-500,600-1000
// 回應需為 multipart/byteranges
// 簡化版通常不實現,單範圍已覆蓋 99% 場景
<?php
// 1. 實際路徑鎖定 (防目錄遍歷)
$baseDir = realpath(__DIR__ . '/files');
$requested = realpath($baseDir . '/' . basename($_GET['file'] ?? ''));
if (!$requested || strpos($requested, $baseDir) !== 0) {
http_response_code(403);
exit('Forbidden');
}
// 2. 權限檢查
if (!checkUserPermission($requested)) {
http_response_code(403);
exit('Access denied');
}
// 3. 限速 (可選)
$speedLimit = 1024 * 1024; // 1MB/s
// 在輸出迴圈加入 usleep 計算
// 4. 日誌記錄
logDownload($_SERVER['REMOTE_ADDR'], $requested, $start, $end);
// 5. 清理輸出緩衝 (防止前有空白/BOM)
while (ob_get_level()) ob_end_clean();
| 特性 | 簡易版 (本文) | 企業級類 |
|---|---|---|
| 代碼行數 | ~30 行 | ~150 行 |
| 錯誤處理 | 基礎 | 完整異常體系 |
| 檔名編碼 | rawurlencode | RFC 5987 完整支援 |
| 多範圍支援 | 無 | 可擴展 |
| 限速/權限/日誌 | 需自行加 | 內建介面 |
| 前端 JS 整合 | 無 | ResumableDownloader 類 |
| 適用場景 | 快速原型、內部工具 | 生產系統、開放下載站 |
將檔案傳輸完全卸載給 Nginx,PHP 僅負責權限驗證:
<?php
// PHP 只做驗證
if (!checkPermission($file)) exit('403');
header('X-Accel-Redirect: /protected/' . basename($file));
header('Content-Disposition: attachment; filename="' . rawurlencode(basename($file)) . '"');
header('Content-Type: application/octet-stream');
// 結束,Nginx 接管傳輸
# Nginx 配置
location /protected/ {
internal; # 禁止直接訪問
alias /var/www/files/; # 真實儲存路徑
sendfile on; # 零拷貝
tcp_nopush on;
aio threads; # 異步 I/O (大檔案)
output_buffers 16 256k; # 輸出緩衝
}
優勢:PHP 進程瞬間釋放、零記憶體佔用、內核級 sendfile、支援超大檔案、自動處理 Range/續傳。