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

PHP 檔案下載實作完整指南:支援大檔、中文檔名、斷點續傳

     編輯:PHP 相關知識

基本下載類別

<?php
class DownFile {
    private $basePath = './files/'; // 檔案實際儲存目錄

    /**
     * 輸出檔案供下載
     * @param string $fileName 用戶看到的下載檔名
     * @param string $saveAs   實體檔名(可不同)
     */
    public function download(string $fileName, string $saveAs = '') {
        $saveAs = $saveAs ?: $fileName;
        $filePath = $this->basePath . $saveAs;

        // 1. 檔案存在性檢查
        if (!file_exists($filePath) || !is_readable($filePath)) {
            header('HTTP/1.1 404 Not Found');
            exit('檔案不存在或不可讀');
        }

        // 2. 中文檔名編碼處理(相容 IE/Edge/Chrome/Firefox)
        $encodedName = $this->encodeFilename($fileName);

        // 3. 取得檔案資訊
        $fileSize = filesize($filePath);
        $mimeType = $this->getMimeType($filePath);

        // 4. 輸出標頭
        header('Content-Type: ' . $mimeType);
        header('Content-Disposition: attachment; filename="' . $encodedName . '"');
        header('Content-Length: ' . $fileSize);
        header('Accept-Ranges: bytes');          // 支援斷點續傳
        header('Cache-Control: no-cache, must-revalidate');
        header('Pragma: no-cache');
        header('Expires: 0');

        // 5. 斷點續傳支援
        $range = $this->parseRangeHeader($fileSize);
        if ($range) {
            header('HTTP/1.1 206 Partial Content');
            header('Content-Range: bytes ' . $range[0] . '-' . $range[1] . '/' . $fileSize);
            header('Content-Length: ' . ($range[1] - $range[0] + 1));
        }

        // 6. 串流輸出(避免大檔佔用記憶體)
        $this->streamFile($filePath, $range);
        exit;
    }

    /** RFC 5987 編碼:filename*=UTF-8''encoded_name */
    private function encodeFilename(string $name): string {
        $encoded = rawurlencode($name);
        // 替換保留字元
        $encoded = str_replace(['%', '+'], ['%25', '%20'], $encoded);
        return "UTF-8''{$encoded}";
    }

    /** 解析 Range 標頭 */
    private function parseRangeHeader(int $fileSize): ?array {
        $rangeHeader = $_SERVER['HTTP_RANGE'] ?? '';
        if (!$rangeHeader || !preg_match('/bytes=(\d+)-(\d*)/', $rangeHeader, $m)) {
            return null;
        }
        $start = (int)$m[1];
        $end = $m[2] ? (int)$m[2] : $fileSize - 1;
        $end = min($end, $fileSize - 1);
        return ($start <= $end) ? [$start, $end] : null;
    }

    /** 分塊讀取輸出 */
    private function streamFile(string $path, ?array $range) {
        $handle = fopen($path, 'rb');
        if (!$handle) return;

        if ($range) fseek($handle, $range[0]);

        $bufferSize = 1024 * 8; // 8KB
        $remaining = $range ? ($range[1] - $range[0] + 1) : filesize($path);

        while (!feof($handle) && $remaining > 0) {
            $readSize = min($bufferSize, $remaining);
            echo fread($handle, $readSize);
            $remaining -= $readSize;
            @ob_flush();
            @flush();
        }
        fclose($handle);
    }

    /** 簡易 MIME 判斷 */
    private function getMimeType(string $path): string {
        $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
        $map = [
            'pdf' => 'application/pdf',
            'zip' => 'application/zip',
            'rar' => 'application/x-rar-compressed',
            'doc' => 'application/msword',
            'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            'xls' => 'application/vnd.ms-excel',
            'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
            'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg',
            'png' => 'image/png', 'gif' => 'image/gif',
            'txt' => 'text/plain', 'csv' => 'text/csv',
            'mp4' => 'video/mp4', 'mp3' => 'audio/mpeg',
        ];
        return $map[$ext] ?? 'application/octet-stream';
    }
}

// 使用範例
$down = new DownFile();
$down->download('產品手冊_2024.pdf', 'manual_v2.pdf');
?>

關鍵技術細節

功能實作要點
中文檔名 RFC 5987 filename*=UTF-8''... 相容現代瀏覽器;舊版 IE 需額外 filename="..." 雙重寫入
大檔不佔記憶體 fread() 分塊讀取 + ob_flush()/flush() 即時輸出
斷點續傳 解析 HTTP_RANGE,回應 206 Partial Content + Content-Range
防止腳本超時 長時間傳輸前 set_time_limit(0); ignore_user_abort(true);
安全性 嚴格驗證路徑遍歷(basename()、realpath() 比對基底目錄)

Nginx/Apache 代理環境注意事項

  • Nginx proxy_buffering off; 避免緩衝導致大檔案記憶體暴增
  • Apache mod_xsendfile 或 Nginx X-Accel-Redirect 讓 Web Server 直接傳檔,PHP 僅驗證權限
  • 關閉 gzip 對二進位檔案壓縮(gzip_types 排除 application/octet-stream)

進階:X-Sendfile / X-Accel-Redirect 零拷貝下載

<?php
// 權限驗證通過後
$filePath = '/var/www/files/secret/manual.pdf';
$fileSize = filesize($filePath);

// Nginx 設定需在 location 內:
// location /protected/ { internal; alias /var/www/files/; }

header('X-Accel-Redirect: /protected/manual.pdf');
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="manual.pdf"');
header('Content-Length: ' . $fileSize);
header('Accept-Ranges: bytes');
exit;
// PHP 進程立即釋放,由 Nginx 高效傳輸檔案
?>

總結

  • 小檔、簡單場景:readfile() 或分塊 fread() 即可
  • 中文檔名:務必雙重編碼(RFC 5987 + 降級)
  • 大檔、高併發:改用 X-Sendfile/X-Accel-Redirect 卸載 PHP
  • 斷點續傳:實作 Range 請求處理,提升用戶體驗
  • 安全第一:路徑遍歷防禦、權限驗證、敏感目錄隔離
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved