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

我們有個案子，因為會需要製作檔案上傳功能，但商品的圖片很多，除了單張圖片上傳外，也允許讓客戶批次上傳圖片，只要客戶先把大量圖片壓縮成 \*.zip 檔案，上傳到我們製作的後台後，就會利用 [DotNetZip](https://github.com/haf/DotNetZip.Semverd) 套件，將客戶上傳的壓縮檔 ( \*.zip ) 解壓縮，並將檔案一一上傳到 [Windows Azure](http://www.windowsazure.com/zh-tw/) 雲端的 **儲存體** (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](http://msdn.microsoft.com/zh-tw/library/system.web.httppostedfilebase.aspx) 類別來接收檔案，參數名稱為 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);
        }
    }
}
```

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

官網的範例如下：[https://documentation.help/DotNetZip/](https://documentation.help/DotNetZip/)

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/2eeb95052ba0b1f95cd3-image_2285.png)

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

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

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

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

```csharp
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](https://github.com/haf/DotNetZip.Semverd) 解壓縮出來的 MemoryStream 真的無法讀取任何資料出來，透過 File.WriteAllBytes 所寫入的檔案，真的檔案大小為 0，而且利用中斷點去看 ms 的內容，還真的有長度、有資料，這啥鬼！雖然農曆七月還剩下幾天 (今天才最後一天)，也不用這樣嚇我吧！

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

當下，我說了一句：「我此時需要靈感」然後跟客戶一起切換到 YouTube 頻道，看起了美國爆笑喜劇＂[天外飛來一句](https://www.youtube.com/results?search_query=天外飛來一句&oq=天外飛來一句&gs_l=youtube.3..0l2.5071.7880.0.9039.18.16.0.2.2.0.91.713.16.16.0...0.0...1ac.1j4.11.youtube.c9fPi2Jkid4)＂( [Whose Line Is It Anyway?](http://en.wikipedia.org/wiki/Whose_Line_Is_It_Anyway%3F) )，這是一系列美國知名電視幽默短劇，真的超好笑，一定要看！

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

```csharp
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](https://github.com/doggy8088/DotNetZipPitfall "https://github.com/doggy8088/DotNetZipPitfall")

**相關連結**

-   [本篇文章的範例程式: DotNetZipPitfall](https://github.com/doggy8088/DotNetZipPitfall) ( GitHub )
-   [DotNetZip - Zip and Unzip in C#, VB, any .NET language](https://github.com/haf/DotNetZip.Semverd)
-   [Whose Line Is It Anyway? - Wikipedia, the free encyclopedia](http://en.wikipedia.org/wiki/Whose_Line_Is_It_Anyway%3F)
