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

PHP 檔案下載完整實作:斷點續傳、大檔支援、安全防禦

     編輯:PHP 綜合知識

核心 HTTP 標頭解析

正確的下載回應需包含關鍵標頭:

標頭作用範例值
Content-TypeMIME 類型,application/octet-stream 強制下載application/octet-stream
Content-Disposition建議檔名,attachment 觸發下載而非預覽attachment; filename="report.pdf"
Content-Length檔案大小,支援進度條、斷點續傳104857600
Accept-Ranges宣告支援範圍請求 (bytes)bytes
Content-Transfer-Encoding傳輸編碼binary
Cache-Control / Pragma快取控制no-cache, must-revalidate

基礎版:完整檔案下載 (中文檔名編碼處理)

<?php
function downloadFile(string $filePath, string $fileName = ''): void {
    // 1. 檢查檔案存在
    if (!is_file($filePath) || !is_readable($filePath)) {
        http_response_code(404);
        exit('檔案不存在或不可讀');
    }

    $fileName = $fileName ?: basename($filePath);
    $fileSize = filesize($filePath);

    // 2. 中文檔名 RFC 5987 編碼 (支援現代瀏覽器)
    $encodedName = rawurlencode($fileName);
    $contentDisposition = "attachment; filename*=UTF-8''{$encodedName}; filename=\"{$fileName}\"";

    // 3. 發送標頭
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: ' . $contentDisposition);
    header('Content-Length: ' . $fileSize);
    header('Accept-Ranges: bytes');
    header('Cache-Control: no-cache, must-revalidate');
    header('Pragma: no-cache');

    // 4. 關閉輸出緩衝,避免記憶體溢出
    while (ob_get_level()) ob_end_clean();

    // 5. 分塊輸出 (每次 8KB)
    $handle = fopen($filePath, 'rb');
    if ($handle === false) {
        http_response_code(500);
        exit('無法開啟檔案');
    }

    while (!feof($handle) && connection_status() === CONNECTION_NORMAL) {
        echo fread($handle, 8192);
        flush(); // 立即發送給客戶端
    }
    fclose($handle);
    exit;
}

// 使用
downloadFile('/data/files/report_2024.pdf', '2024年報表.pdf');

進階版:支援 HTTP Range 斷點續傳 / 多執行緒下載

<?php
function downloadWithRange(string $filePath, string $fileName = ''): void {
    if (!is_file($filePath) || !is_readable($filePath)) {
        http_response_code(404);
        exit('File not found');
    }

    $fileName = $fileName ?: basename($filePath);
    $fileSize = filesize($filePath);
    $encodedName = rawurlencode($fileName);

    // 預設標頭
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename*=UTF-8\'\'' . $encodedName);
    header('Accept-Ranges: bytes');
    header('Cache-Control: no-cache, must-revalidate');

    // 處理 Range 標頭
    $rangeStart = 0;
    $rangeEnd = $fileSize - 1;
    $statusCode = 200;

    if (isset($_SERVER['HTTP_RANGE'])) {
        $range = $_SERVER['HTTP_RANGE'];
        if (preg_match('/bytes=(\d+)-(\d*)/', $range, $matches)) {
            $rangeStart = intval($matches[1]);
            $rangeEnd = $matches[2] !== '' ? intval($matches[2]) : $fileSize - 1;
            
            // 範圍驗證
            if ($rangeStart >= $fileSize || $rangeEnd >= $fileSize || $rangeStart > $rangeEnd) {
                header('Content-Range: bytes */' . $fileSize);
                http_response_code(416); // Range Not Satisfiable
                exit;
            }
            
            $statusCode = 206; // Partial Content
            header('Content-Range: bytes ' . $rangeStart . '-' . $rangeEnd . '/' . $fileSize);
        }
    }

    $contentLength = $rangeEnd - $rangeStart + 1;
    header('Content-Length: ' . $contentLength);
    http_response_code($statusCode);

    while (ob_get_level()) ob_end_clean();

    $handle = fopen($filePath, 'rb');
    fseek($handle, $rangeStart);
    
    $remaining = $contentLength;
    $bufferSize = 8192;
    
    while ($remaining > 0 && !feof($handle) && connection_status() === CONNECTION_NORMAL) {
        $readSize = min($bufferSize, $remaining);
        echo fread($handle, $readSize);
        flush();
        $remaining -= $readSize;
    }
    fclose($handle);
    exit;
}

大檔案 (GB 級) 記憶體安全策略

  • 分塊讀取:fread($handle, 8192) 固定緩衝區,記憶體佔用恆定 ~8KB
  • 關閉輸出緩衝:while (ob_get_level()) ob_end_clean(); 避免 PHP 緩衝整個檔案
  • 忽略使用者中斷:ignore_user_abort(true) 確保傳輸中斷不殘留進程 (可選)
  • 設定時間限制:set_time_limit(0) 移除執行時間限制

安全防禦清單

  1. 路徑遍歷防護:realpath($filePath) 驗證在允許目錄內
  2. 權限檢查:下載前驗證用戶身份、權限、檔案所有權
  3. MIME 嗅探防護:header('X-Content-Type-Options: nosniff')
  4. 檔名編碼:RFC 5987 filename*=UTF-8'' 相容所有現代瀏覽器
  5. 速率限制:防止佔滿頻寬 (可用 Nginx limit_rate 或 PHP 節流)

Nginx X-Accel-Redirect (高效能生產模式)

PHP 僅驗證權限,實際傳輸交給 Nginx (零拷貝、不佔用 PHP 進程):

<?php
// PHP 僅驗證權限並發送內部重導向標頭
if ($userCanDownload) {
    header('X-Accel-Redirect: /protected-files/' . $internalPath);
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename*=UTF-8\'\'' . rawurlencode($fileName));
    exit;
}

// Nginx 配置
location /protected-files/ {
    internal; # 僅允許內部重導向
    alias /data/files/; # 實際儲存路徑
    # 可加 limit_rate 1m; 限速
}

總結

基礎場景用分塊 fread 即可;需斷點續傳加 Range 處理;GB 級大檔、高併發生產環境強烈推薦 X-Accel-Redirect / X-Sendfile 卸載傳輸給 Web Server,PHP 僅負責授權。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved