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

PHP 實現文件下載而非瀏覽器預覽:Content-Disposition 標頭正確用法

     編輯:PHP 綜合知識

默認情況下,瀏覽器對文本、圖片、PDF 等已註冊 MIME 類型會直接預覽。若需強制彈出「另存為」對話框,必須發送正確的 HTTP 響應頭。本文給出最小可用代碼與生產級封裝。

核心原理:兩個關鍵 Header

Content-Type: application/octet-stream        // 通用二進制流,阻止瀏覽器按 MIME 預覽
Content-Disposition: attachment; filename="xxx.pdf"  // 指示為附件,建議保存文件名

filename 必須經過 RFC 5987 編碼,兼容中文文件名:

filename="fallback.txt"; filename*=UTF-8''%E4%B8%AD%E6%96%87.txt

最小可用代碼

<?php
$filePath = '/data/files/report_202406.pdf';
$fileName = '2024年上半年報告.pdf';

if (!is_file($filePath)) {
    http_response_code(404);
    exit('文件不存在');
}

$size = filesize($filePath);
$encodedName = rawurlencode($fileName);

header('Content-Type: application/octet-stream');
header('Content-Length: ' . $size);
header("Content-Disposition: attachment; filename=\"{$encodedName}\"; filename*=UTF-8''{$encodedName}");
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');

readfile($filePath); // 直接輸出文件內容,不佔用 PHP 內存

生產級封裝:支援斷點續傳 (Range Request)

<?php
class FileDownloader
{
    public function send(string $filePath, string $downloadName = '', array $options = []): void
    {
        if (!is_file($filePath)) {
            http_response_code(404);
            throw new RuntimeException('文件不存在');
        }
        $downloadName = $downloadName ?: basename($filePath);
        $size = filesize($filePath);
        $mime = $this->getMime($filePath);
        $etag = md5($filePath . $size . filemtime($filePath));
        $lastModified = gmdate('D, d M Y H:i:s', filemtime($filePath)) . ' GMT';

        // 緩存驗證
        if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $etag) {
            http_response_code(304); return;
        }
        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= filemtime($filePath)) {
            http_response_code(304); return;
        }

        // Range 斷點續傳
        $start = 0; $end = $size - 1; $partial = false;
        if (isset($_SERVER['HTTP_RANGE'])) {
            if (preg_match('/bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $m)) {
                $start = (int)$m[1];
                $end = $m[2] !== '' ? (int)$m[2] : $size - 1;
                if ($start >= $size || $end >= $size || $start > $end) {
                    header('Content-Range: bytes */' . $size);
                    http_response_code(416); return;
                }
                $partial = true;
            }
        }

        // 響應頭
        header('Content-Type: ' . $mime);
        header('Accept-Ranges: bytes');
        header('Content-Length: ' . ($partial ? $end - $start + 1 : $size));
        header('ETag: ' . $etag);
        header('Last-Modified: ' . $lastModified);
        header('Cache-Control: private, max-age=3600');
        $encoded = rawurlencode($downloadName);
        header("Content-Disposition: attachment; filename=\"{$encoded}\"; filename*=UTF-8''{$encoded}");

        if ($partial) {
            http_response_code(206);
            header("Content-Range: bytes {$start}-{$end}/{$size}");
        }

        if ($_SERVER['REQUEST_METHOD'] === 'HEAD') return;

        // 輸出文件流
        $fp = fopen($filePath, 'rb');
        fseek($fp, $start);
        $buffer = 1024 * 8; // 8KB
        while (!feof($fp) && ($partial ? ftell($fp) <= $end : true)) {
            $read = min($buffer, $partial ? $end - ftell($fp) + 1 : $buffer);
            echo fread($fp, $read);
            flush();
        }
        fclose($fp);
    }

    private function getMime(string $path): string
    {
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mime = finfo_file($finfo, $path);
        finfo_close($finfo);
        return $mime ?: 'application/octet-stream';
    }
}

// 使用
$downloader = new FileDownloader();
$downloader->send('/data/files/report.pdf', '2024報告.pdf');

常見坑與解決

現象原因解決
中文文件名亂碼/截斷未做 RFC 5987 編碼同時發送 filename (URL 編碼) 和 filename* (UTF-8)
大文件記憶體耗盡用 file_get_contents 全讀入內存用 readfile() 或分塊 fread 輸出
無法斷點續傳未處理 Range 請求實現 206 Partial Content 邏輯
Nginx 代理截斷輸出proxy_buffering on 預設緩衝場所加 proxy_buffering off; 或 X-Accel-Redirect 內部重定向
Session 阻塞並發下載默認 Session 文件鎖下載前 session_write_close() 釋放鎖

高性能替代:X-Sendfile / X-Accel-Redirect

將文件傳輸卸載給 Web 服務器,PHP 僅發送內部重定向頭:

<?php
// Apache + mod_xsendfile
header('X-Sendfile: /absolute/path/to/file.pdf');

// Nginx
header('X-Accel-Redirect: /protected/files/file.pdf');
// 需 nginx.conf: location /protected/ { internal; alias /data/files/; }

此法可繞過 PHP 內存/執行時間限制,支援高併發大文件分發,是生產環境首選。

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