剛接到這樣的需求,其實我是拒絕的。我甚至很有耐心地和pm商量,扔個csv不就好了么?
pm:對方需要一個csv打包成.tar.gz的包,他們是linux server,這是硬性要求。
然后我開始折騰之旅,里面小坑無數。
其實這里大致有兩條思路:
1.把生成好的csv利用System(),exec()函數去使用系統命令tar成包,比較蛋疼的是公司配的是windows,我只能在自己虛擬機ubuntu里面測試。
2.利用現成的工具類或者函數什么的, 去直接生成一個tar.gz,然后扔csv進去。
我特么先選第二條路。首先別人是要.tar.gz,我先去查了一下php的內置函數,貌似只有gzwrite,gzopen等文件操作函數,只能把文件弄成安全二進制的.gz文件。我發現如果繼續走下去就走到第1條路上了,只能利用系統命令去實現。
我嘗試性地在ubuntu下exec('tar -cxf do.tar.gz do.csv'),然而只有run script方式可以執行生成成功,而通過瀏覽器並沒有生成。沒得到tar.gz,何談后面利用一連串header去輸出下載文件呢?
所以第一條路是徹底死了,我繼續在第二條路上行走。
由於網絡不好,沒能連上vpn。我只能一直百度,而不能利用到google優質的資源。我搜索了相關php導出tar.gz的資料,國內一大堆抄襲的,基本結論是:少數人遇到這個需求,且遇到了下載出來的文件破損不能打開,無解。
你沒看錯,是:無解。國內沒有一個人解決了這個問題(至少在能搜索出來的page上來看)。我先試着曲線救國,先還是用gzwrite系列函數去創建一個.gz文件,代碼如下:
<?php $gz = gzopen('/tmp/do.gz', 'w9'); // 打開或創建一個.gz文件,linux下記得寫權限問題 gzwrite($gz, '33c,24ec,32q3');// 寫入內容到.gz文件 gzclose($gz); // 關閉文件句柄,釋放資源 header("Content-Type: application/x-gzip"); $lastPackName = '/tmp/do.gz'; //header("Content-type:application/octet-stream"); header("Accept-Ranges:bytes"); header("Accept-Length: ".filesize($lastPackName)); Header("Content-Disposition:attachment; filename=records.gz"); @readfile($lastPackName);
//下載文件之后,要刪除該壓縮包
@unlink($lastPackName);
相關函數的用法,可以自行查閱php.net上的相關函數。然后我發現單獨寫一個demo文件,是能下載下來能正常解壓的文件,而如果寫到公司項目中對應的Controller的function里面,則下載的包是破損的,這是我遇到的第一個坑。
然后我又在想,是否是環境問題。我把下載好的文件包,扔到我虛擬機(ubuntu)里面,也是解壓出來有問題。而在服務器上生成的原始文件.gz是完好無缺的。所以我遇到了和這位同學一樣的問題:http://bbs.csdn.net/topics/390226194
最后他的提問被說是header頭設置的問題,我又仔細排查了一下header設置沒有問題。
后面我知道pear有個Archive_Tar類可以直接打包tar文件,我分析了一下,需要安裝pear相應的包,成本較大,放棄了這樣的做法。
昨天整個下午就在不斷下載解壓失敗和查看國內千篇一律無解的技術資料上消耗完了。晚上回到家,我不甘心,連上vpn,怒用google,很快查到了這篇文章:
http://stackoverflow.com/questions/7004989/creating-zip-or-tar-gz-archive-without-exec
歪果仁果然會折騰,早就遇到類似打包的要求,並且沒有使用exec.可以使用php自帶的PharData類去創建tar文件,然后通過compress函數打包成tar.gz.關於PharData類和相關函數可以去php.net上查閱。
於是我使用如下代碼打包:
<?php $csvFile = '/tmp/do.csv'; $fp = fopen($csvFile , 'w+'); fwrite($fp, '232,c233,23dc'); fclose($fp); $tarFile = '/tmp/archive.tar'; $a = new PharData($tarFile); // ADD FILES TO archive.tar FILE $a->addFile($csvFile, 'do.csv'); $a->compress(Phar::GZ); unlink($tarFile); unlink($csvFile); header("Content-Type: application/x-gzip"); $lastPackName = $tarFile . '.gz'; //header("Content-type:application/octet-stream"); header("Accept-Ranges:bytes"); header("Accept-Length: ".filesize($lastPackName)); Header("Content-Disposition:attachment; filename=records.tar.gz"); @readfile($lastPackName); //下載文件之后,要刪除該壓縮包 @unlink($lastPackName);
我果真so easy地創建了一個包含xx.csv的tar.gz文件,但是下載解壓失敗依然存在。當然,如果寫一個小腳本直接用瀏覽器訪問是對的,下載的文件也是解壓正常。
時間不知不覺就到了今天上午,和部門leader一起排查,先排除了ngnix轉發輸出的問題(在測試服務器上測試某段代碼),也排除了生成文件部分的問題。代碼就那么十幾行,最后只剩下header輸出到最后readfile這部分。
readfile是把文件讀到緩沖區里面,而下載下來有問題,多半是buffer區在readfile之前有東西了。於是我bing了一下"php header gz broken"關鍵字,查到這篇討論:
http://stackoverflow.com/questions/22046020/php-downloading-tar-gz-file-increases-file-size-and-changes-md5
雖然並不是完全和我想要的內容一樣,不過給了我一點提示。我發現他在readfile之前執行了ob_clean(),清除緩沖區。這個函數保證了后面的輸出都是干凈的,新的。最終我的代碼如下:
$csvFile = '/tmp/downPrivilege.csv'; $fp = fopen($csvFile, 'w+') or die('cant not create file'); fwrite($fp, '23233,cw3,232'); fclose($fp); $tarFile = '/tmp/downPrivilege.tar'; $a = new PharData($tarFile); // ADD FILES TO archive.tar FILE $a->addFile($csvFile, 'downPrivilege.csv'); $a->compress(Phar::GZ); header("Content-Type: application/x-gzip"); $lastPackName = $tarFile . '.gz'; header("Accept-Ranges:bytes"); header("Accept-Length: " . filesize($lastPackName)); Header("Content-Disposition:attachment; filename=downPrivilege.tar.gz"); header("Expires: 0"); header("Cache-Control: must-revalidate, post-check=0, pre-check=0"); header("Cache-Control: private"); header("Pragma: public"); ob_clean(); // 清除緩沖區內容 @readfile($lastPackName); unlink($tarFile); unlink($csvFile); unlink($lastPackName);
下載之后終於正確能解壓出東西了。
總結:
1.不要想着別人能幫助你 ,特別是比較不常見的東西,只能自己專研。我以前覺得有團隊可以一起探討,但是實際工作中很少人願意花時間真的和你討論。
2.查資料真心不要百度,常見的問題都可能是錯誤解答,而且千篇一律。還是bing+google吧。
3.排查問題要自信,過濾掉的環節就不要反復折騰。
4.歪果仁往往已經遇到了那些非主流的情況,可以直接借鑒。
希望有做類似需求的朋友可以看到這篇文章,因為國內沒有一個人能正面回答這個問題。
