php 通過header下載中文文件名 壓縮包損壞或文件不存在的問題


開發中大家都是使用的utf8編碼,昨天遇到一個奇坑,本是一件很小的問題,解決也浪費了個吧小時。廢話不多說,植入正題:

文件下載方式:通過header二進制流文件下載
需求: 文件上傳保留文件名不變
數據字段file_url值:/public/upload/files/2019/04-29/中文測試包.rar

linux(Ubuntu 18.04.2 LTS )文件目錄:/home/wwwroot/web/public/upload/files/2019/04-29

windows10文件目錄:D:\web\public\upload\files\2019\04-29\中文測試包.rar

我們先看下,windows下的文件下載:

<?php
        $file_name = '/public/upload/files/2019/04-29/中文測試包.rar';
        //$file_name = iconv("utf-8","gbk//IGNORE",$file_name); // 特別注意!特別注意!特別注意這里,windows下必須開轉碼,不然直接文件不存

        $file_path = $_SERVER['DOCUMENT_ROOT'] . $file_name;// 比如windows下這里我的是 "D:/web/public/upload/files/2019/04-29/中文測試包.rar"
        //判斷如果文件存在,則跳轉到下載路徑
        if (!file_exists($file_path)) {
            die("文件不存在!");
        }

        $fp = fopen($file_path, "r+") or die('打開文件錯誤');   //下載文件必須要將文件先打開。寫入內存
        $file_size = filesize($file_path);
        //返回的文件流
        Header("Content-type:application/octet-stream");
        //按照字節格式返回
        Header("Accept-Ranges:bytes");
        //返回文件大小
        Header("Accept-Length:" . $file_size);
        //彈出客戶端對話框,對應的文件名
        Header("Content-Disposition:attachment;filename=" . substr($file_name, strrpos($file_name, '/') + 1));
        //防止服務器瞬間壓力增大,分段讀取
        $buffer = 1024;
        while (!feof($fp)) {
            $file_data = fread($fp, $buffer);
            echo $file_data;
        }
        fclose($fp);

        die("下載成功!");

?>

文件不存在?神馬玩意?。同樣的代碼ubutun生產環境下:

文件下載成功。神馬情況?
原因:windows 系統默認字符集是gbk,項目采用的是uft8編碼,中文文件名必須轉碼才能使用file_exists檢測文件,不然報找不到文件:

windows下的解決方式就是上面注釋的那一段開啟:

$file_name = iconv("utf-8","gbk//IGNORE",$file_name); // 特別注意!特別注意!特別注意這里,windows下必須開轉碼,不然直接文件不存

windows下再次執行后發現下載成功:

那么問題來了。開啟后的代碼是這樣的:

<?php
        $file_name = '/public/upload/files/2019/04-29/中文測試包.rar';
        $file_name = iconv("utf-8","gbk//IGNORE",$file_name); // 特別注意!特別注意!特別注意這里,windows下必須開轉碼,不然直接文件不存
        $file_path = $_SERVER['DOCUMENT_ROOT'] . $file_name;// 比如windows下這里我的是 "D:/web/public/upload/files/2019/04-29/中文測試包.rar"
        //判斷如果文件存在,則跳轉到下載路徑
        if (!file_exists($file_path)) {
            die("文件不存在!");
        }

        $fp = fopen($file_path, "r+") or die('打開文件錯誤');   //下載文件必須要將文件先打開。寫入內存
        $file_size = filesize($file_path);
        //返回的文件流
        Header("Content-type:application/octet-stream");
        //按照字節格式返回
        Header("Accept-Ranges:bytes");
        //返回文件大小
        Header("Accept-Length:" . $file_size);
        //彈出客戶端對話框,對應的文件名
        Header("Content-Disposition:attachment;filename=" . substr($file_name, strrpos($file_name, '/') + 1));
        //防止服務器瞬間壓力增大,分段讀取
        $buffer = 1024;
        while (!feof($fp)) {
            $file_data = fread($fp, $buffer);
            echo $file_data;
        }
        fclose($fp);

        die("下載成功!");
?>

在ubutun 服務器上我們執行:

是不是仿佛解決東牆補西牆。ubutun 下字符集可以通過:

cat /usr/share/i18n/SUPPORTED

說明系統支持中文字符,不然上傳的壓縮包怎么會顯示:“中文測試包.rar”。
問題描述:linux系統下驗證中文文件file_exists不能是中文,所以不能在上面轉碼成gbk.

那么問題來了: 如何做到兼容性?
我們知道PHP_OS是 php自帶的一個內置常量,返回的是服務器端的操作系統標示,值為(WINNT,WIN32等),比如這樣:

 echo strtoupper(substr(PHP_OS,0,3))==='WIN'?'windows 服務器':'不是 widnows 服務器';

另外一種通過系統分隔符DIRECTORY_SEPARATOR ,這個也是php自帶的一個內置常量,用來顯示系統分隔符的命令,

不需要任何定義與包含即可直接使用。在windows下路徑分隔符是\(當然/在部分系統上也是可以正常運行的),在linux上路徑的分隔符是/,

DIRECTORY_SEPARATOR 這個常量存在的意義就是會根據不同的操作系統來顯示不同的分隔符。

使用 DIRECTORY_SEPARATOR 判斷操作系統類型比如這樣:

echo DIRECTORY_SEPARATOR=='\\'?'windows 服務器':'不是 widnows 服務器';

還有一種方式:

PATH_SEPARATOR 也是一個常量,在linux系統中是一個" : "號,Windows上是一個";"號。

使用 PATH_SEPARATOR 判斷操作系統類型比如這樣:

echo PATH_SEPARATOR==';'?'windows 服務器':'不是 widnows 服務器';

代碼兼容性我們可以驗證系統類型,對windows下做判斷再決定是否轉碼操作。
這里重點說哈關於下載后文件打開提示“文件損壞”的問題,期初我也遇到。猜測肯定是在讀取文件字節流存在數據丟失,也就是沒讀取完整:

下面看下這段有問題的代碼:有興趣的朋友可以自己思考哈,問題在哪里?這里我就不說了,相信很多朋友也能找到問題點:

<?php
        $http_type = ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')) ? 'https://' : 'http://';

        $file_name = '/public/upload/files/2019/04-29/中文測試包.rar';

        //檢測文件是否存在,並且可讀
        if (!is_file($file_name) && is_readable($file_name)) {
            die("文件不存在或不可讀!");
        }

        //判斷如果文件存在,則跳轉到下載路徑
        $file_path = $_SERVER['DOCUMENT_ROOT'] . $file_name;// 比如windows下這里我的是 "D:/web/public/upload/files/2019/04-29/中文測試包.rar"
        //判斷如果文件存在,則跳轉到下載路徑
        if (!file_exists($file_path)) {
            die("文件不存在!");
        }

        //設置腳本的最大執行時間,設置為0則無時間限制
        set_time_limit(0);
        ini_set('max_execution_time', '0');
        //通過header()發送頭信息
        //因為不知道文件是什么類型的,告訴瀏覽器輸出的是字節流
        header('content-type:application/octet-stream');
        //告訴瀏覽器返回的文件大小類型是字節
        header('Accept-Ranges:bytes');

        //獲得文件大小
        //$filesize = filesize($filename);//此方法無法獲取到遠程文件大小

        $header_array = get_headers($http_type . $_SERVER['HTTP_HOST'] . $file_name, true);
        $filesize = $header_array['Content-Length'];
        //告訴瀏覽器返回的文件大小
        header('Accept-Length:' . $filesize);

        //告訴瀏覽器文件作為附件處理並且設定最終下載完成的文件名稱
        header('content-disposition:attachment;filename=' . substr($file_name, strrpos($file_name, '/') + 1));

        //針對大文件,規定每次讀取文件的字節數為4096字節,直接輸出數據
        $buffer = 4096;
        $fp = fopen($file_path, 'rb');
        //總的緩沖的字節數
        $sum_buffer = 0;

        //只要沒到文件尾,就一直讀取
        while (!feof($fp) && $sum_buffer < $filesize) {
            echo fread($fp, $buffer);
            $sum_buffer += $buffer;
        }

        //記錄下載
        die("下載成功!");

?>

 

 有興趣的朋友可以找下bug,哈哈


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM