The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

使用 DotNetZip 解壓縮為 Stream 時讀不到檔案內容的陷阱

我們有個案子,因為會需要製作檔案上傳功能,但商品的圖片很多,除了單張圖片上傳外,也允許讓客戶批次上傳圖片,只要客戶先把大量圖片壓縮成 *.zip 檔案,上傳到我們製作的後台後,就會利用 DotNetZip 套件,將客戶上傳的壓縮檔 ( *.zip ) 解壓縮,並將檔案一一上傳到 Windows Azure 雲端的 儲存體 (Storage) 上。基本上,這功能很簡單,隨便 Google 一下都有得抄 code,同事也很自然的抄了一段 code,測試無誤就放上,誰知道,同一段看似沒問題的 code,在不同的使用情境下,還是會出現 Bug,魔鬼總在細節裡,讓我們繼續斬妖除魔去。

首先,我們先看看將檔案寫入到 Windows Azure 的程式碼範例:

private void SaveImageToBlob(string container, string filename, Stream stream)
{
    CloudStorageAccount storageAccount = CloudStorageAccount.Parse(
        CloudConfigurationManager.GetSetting("StorageConnectionString"));
    CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
    CloudBlobContainer container = blobClient.GetContainerReference(container);
    container.CreateIfNotExists();
    CloudBlockBlob blockBlob = container.GetBlockBlobReference(filename);
    blockBlob.Properties.ContentType = "image/jpeg";
    blockBlob.UploadFromStream(stream);
}

我想,這段 code 應該很單純,就只是把傳入的 stream 資料上傳到 Blob Storage 而已,測試過單一檔案上傳也的確沒問題。我們是用 ASP.NET MVC 的 Action 方法,透過 HttpPostedFileBase 類別來接收檔案,參數名稱為 file,然後直接取得 file.InputStream 就可以得到 Stream 型別的資料,並傳入上述程式碼片段的 SaveImageToBlob 方法,正確無誤!

然後,後來加上了「批次匯入」機制,程式碼也很簡單,如下範例:

using (ZipFile zip = ZipFile.Read(file.InputStream))
{
    foreach (var entry in zip.Entries)
    {
        using (var ms = new MemoryStream())
        {
            entry.Extract(ms);
 
            SaveImageToBlob("UploadImages", entry.FileName, ms);
        }
    }
}

這段程式,用人眼看,一眼就可以看出:「嗯,沒問題!而且範例都是從官網抄的,肯定沒問題吧!

官網的範例如下:http://dotnetzip.codeplex.com/wikipage?title=CS-Examples&referringTitle=Examples

所以,工程師連測都沒測,直接發版給客戶測試,然後被抓包,客戶說:「上傳單張照片可以,但批次上傳圖檔後,所有圖片都會自動消失。

其實,我剛看到這段 code 時,也直覺認為沒問題,但親自測試之後發現,還真的會這樣,利用單步偵錯後發現,解壓縮有成功,而且如上範例程式,執行 entry.Extract(ms); 之後,ms 也的確有內容,但是上傳到 Windows Azure 後,檔案長度為 0  (檔案內容完全是空的)!

WTF,又遇到魔鬼! ( 此時,空氣中傳達著寧靜,但耳邊確若有似無的傳來聲音說:「這怎麼可能!」 )

然後,我自己寫了一支小程式,想研究出為什麼會這樣,程式碼依然簡單,如下:

using Ionic.Zip;
using System.IO;

namespace DotNetZipPitfallConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            using (ZipFile zip = ZipFile.Read("catalog.zip"))
            {
                foreach (ZipEntry entry in zip.Entries)
                {
                    using (var ms = new MemoryStream())
                    {
                        entry.Extract(ms);

                        using (BinaryReader reader = new BinaryReader(ms))
                        {
                            File.WriteAllBytes(
                                Path.Combine(@"G:\Temp", entry.FileName),
                                reader.ReadBytes((int)ms.Length));
                        }
                    }
                }
            }
        }
    }
}

測試後的結果發現,果不其然,真的透過 DotNetZip 解壓縮出來的 MemoryStream 真的無法讀取任何資料出來,透過 File.WriteAllBytes 所寫入的檔案,真的檔案大小為 0,而且利用中斷點去看 ms 的內容,還真的有長度、有資料,這啥鬼!雖然農曆七月還剩下幾天 (今天才最後一天),也不用這樣嚇我吧!

解題的當下,已經逼近午夜 12:00 整,而且因為此案急需上線,客戶窗口當下就直接坐在我的旁邊,看著我把這問題給解掉! =_=

當下,我說了一句:「我此時需要靈感」然後跟客戶一起切換到 YouTube 頻道,看起了美國爆笑喜劇"天外飛來一句"( Whose Line Is It Anyway? ),這是一系列美國知名電視幽默短劇,真的超好笑,一定要看!

在觀看的過程狂笑之後 (真的笑到流淚),回來面對現實,繼續看 Code,看能不能想出到底是甚麼問題,此時,靈光乍現,我想到所有的 Stream 類別,都有個 Position 屬性,用來定義接下來要讀取 Stream 的位置為何,然後我把程式碼改成以下這樣,多加上一行 ms.Position = 0; 這段:

using Ionic.Zip;
using System.IO;

namespace DotNetZipPitfallConsoleApplication
{
  class Program
  {
    static void Main(string[] args)
    {
      using (ZipFile zip = ZipFile.Read("catalog.zip"))
      {
        foreach (ZipEntry entry in zip.Entries)
        {
          using (var ms = new MemoryStream())
          {
            entry.Extract(ms);

            // 使用 DotNetZip 會導致 MemoryStream 的 Position 不在最前面
            // 這會導致後續抓不到檔案,所以讀取後必須把 Position 歸零!
            ms.Position = 0;

            using (BinaryReader reader = new BinaryReader(ms))
            {
              File.WriteAllBytes(
                  Path.Combine(@"G:\Temp", entry.FileName), 
                  reader.ReadBytes((int)ms.Length));
            }
          }
        }
      }
    }
  }
}

還真的,問題解決了!

 

這個故事告訴我們,就算客戶就坐在你身邊,死線就在眼前,放鬆心情,緩和壓力,除錯時,搞不好你可以想到更棒的解法! ^_^

 

註: 完整範例我已經放上 GitHub,各位可下載:https://github.com/doggy8088/DotNetZipPitfall

相關連結