首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 強制文件下載源碼大全:從基礎 readfile 到斷點續傳、Nginx 加速

     編輯:PHP 相關知識

方案速覽表

方案核心函數適用場景關鍵特性
基礎版`readfile()`<10MB、低併發代碼最少、內部緩衝
分塊版`fread()` + `flush()`大文件、控制內存固定緩衝區、實時輸出
斷點續傳版解析 `HTTP_RANGE` + `fseek()`大文件、支援續傳/多線程206 Partial Content、Content-Range
Nginx 加速版`X-Accel-Redirect`高併發、大文件、生產標準零拷貝、PHP 瞬時釋放、Nginx 原生 Range

1. 基礎版:readfile() 一行搞定

<?php
function downloadBasic(string $filePath, string $fileName = '') {
    if (!is_file($filePath)) { http_response_code(404); exit('Not Found'); }
    $fileName = $fileName ?: basename($filePath);
    $encoded = rawurlencode($fileName);

    header('Content-Type: application/octet-stream');
    header("Content-Disposition: attachment; filename=\"{$encoded}\"; filename*=UTF-8''{$encoded}");
    header('Content-Length: ' . filesize($filePath));
    header('Accept-Ranges: bytes');
    header('Cache-Control: private, must-revalidate');
    header('Pragma: private');

    readfile($filePath);
    exit;
}
?>

2. 分塊版:大文件恆定內存

<?php
function downloadChunked(string $filePath, string $fileName = '', int $bufferSize = 1024*1024) {
    if (!is_file($filePath)) { http_response_code(404); exit('Not Found'); }
    $fileName = $fileName ?: basename($filePath);
    $encoded = rawurlencode($fileName);
    $fileSize = filesize($filePath);

    header('Content-Type: application/octet-stream');
    header("Content-Disposition: attachment; filename=\"{$encoded}\"; filename*=UTF-8''{$encoded}");
    header('Content-Length: ' . $fileSize);
    header('Accept-Ranges: bytes');
    header('Cache-Control: private, must-revalidate');
    header('Pragma: private');

    $fp = fopen($filePath, 'rb');
    while (!feof($fp) && connection_status() === CONNECTION_NORMAL) {
        echo fread($fp, $bufferSize);
        @ob_flush(); @flush();
    }
    fclose($fp);
    exit;
}
?>

3. 斷點續傳版:完整 Range 支持

<?php
function downloadResumable(string $filePath, string $fileName = '', int $bufferSize = 1024*1024) {
    if (!is_file($filePath)) { http_response_code(404); exit('Not Found'); }
    $fileName = $fileName ?: basename($filePath);
    $encoded = rawurlencode($fileName);
    $fileSize = filesize($filePath);

    // 解析 Range
    $start = 0; $end = $fileSize - 1; $partial = false;
    if (isset($_SERVER['HTTP_RANGE']) && preg_match('/bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $m)) {
        $start = (int)$m[1];
        $end = $m[2] !== '' ? (int)$m[2] : $fileSize - 1;
        $partial = true;
    }
    $start = max(0, min($start, $fileSize - 1));
    $end = max($start, min($end, $fileSize - 1));
    $length = $end - $start + 1;

    header('Content-Type: application/octet-stream');
    header("Content-Disposition: attachment; filename=\"{$encoded}\"; filename*=UTF-8''{$encoded}");
    header('Accept-Ranges: bytes');
    header('Cache-Control: private, must-revalidate');
    header('Pragma: private');

    if ($partial) {
        header('HTTP/1.1 206 Partial Content');
        header("Content-Range: bytes {$start}-{$end}/{$fileSize}");
        header("Content-Length: {$length}");
    } else {
        header("Content-Length: {$fileSize}");
    }

    $fp = fopen($filePath, 'rb');
    fseek($fp, $start);
    $remaining = $length;
    while (!feof($fp) && $remaining > 0 && connection_status() === CONNECTION_NORMAL) {
        $read = min($bufferSize, $remaining);
        echo fread($fp, $read);
        @ob_flush(); @flush();
        $remaining -= $read;
    }
    fclose($fp);
    exit;
}
?>

4. Nginx X-Accel-Redirect:生產環境黃金標準

PHP 控制器 (Laravel/Symfony/ThinkPHP/原生通用)

<?php
// 僅權限驗證,返回內部重定向頭
$file = StorageFile::findOrFail($id);
if (!$file->canDownload(auth()->user())) abort(403);

return response('', 200, [
    'X-Accel-Redirect' => '/protected-files/' . $file->storage_path,
    'Content-Type' => $file->mime_type,
    'Content-Disposition' => 'attachment; filename="' . rawurlencode($file->original_name) . '"',
    // 可選:Content-Length 由 Nginx 自動添加
]);
?>

Nginx 配置

location /protected-files/ {
    internal;                    # 禁止外部直接訪問
    alias /mnt/storage/;         # 文件實際存儲根目錄 (需以 / 結尾)
    sendfile on;                 # 零拷貝
    tcp_nopush on;
    # 可選限速
    # limit_rate_after 10m;
    # limit_rate 1m;
}

安全防護清單 (所有方案通用)

  1. 路徑遍歷:`$real = realpath($input); if (strpos($real, $allowedRoot) !== 0) abort(400);`
  2. 權限驗證:每次下載檢查用戶身份、文件所有權、下載配額
  3. 禁用輸出緩衝:`while (ob_get_level()) ob_end_clean(); ini_set('zlib.output_compression', 'Off');`
  4. 無限執行時間:`set_time_limit(0);`
  5. 審計日誌:記錄 user_id、file_id、ip、range、status、bytes_sent
  6. 防盜鏈:簽名 URL (JWT/HMAC) 含過期時間、Referer 檢查

中文文件名編碼最佳實踐

$encoded = rawurlencode($fileName); // RFC 3986
header("Content-Disposition: attachment; filename=\"{$encoded}\"; filename*=UTF-8''{$encoded}");
// 兼容:IE/Edge 老版本用 filename,現代瀏覽器用 filename* (RFC 5987)

常見問題 FAQ

問題原因解決
下載文件打開損壞/多出 HTML/空白腳本有 BOM、echo/var_dump、錯誤信息、HTML 輸出混入文件流下載腳本**純輸出文件**,無任何其他輸出;`ini_set('display_errors', 0)`
大文件下載中斷/超時PHP `max_execution_time`、Nginx `proxy_read_timeout`、瀏覽器超時`set_time_limit(0)`;Nginx `proxy_read_timeout 3600`;分片/斷點續傳
內存耗盡用 `readfile()`/`file_get_contents()` 讀大文件改用分塊 `fread` 或 `X-Accel-Redirect`
Range 請求返回 200 而非 206未正確解析 `HTTP_RANGE`、未發送 `Accept-Ranges: bytes`檢查解析邏輯、確保發送 `Accept-Ranges` 頭
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved