.Net ZipArchive更新到基础流的方法

简介

ZipArchive是.Net中常用的ZIP文档操作类,可以用来对ZIP压缩文档进行各种操作

例如,我们用它来解压sample.zip文件:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
using (FileStream fs = new FileStream("sample.zip", FileMode.Open))
{
using (ZipArchive zipArchive = new ZipArchive(fs, ZipArchiveMode.Read))
{
zipArchive.ExtractToDirectory("extract");
}
}
using (FileStream fs = new FileStream("sample.zip", FileMode.Open)) { using (ZipArchive zipArchive = new ZipArchive(fs, ZipArchiveMode.Read)) { zipArchive.ExtractToDirectory("extract"); } }
using (FileStream fs = new FileStream("sample.zip", FileMode.Open))
{
    using (ZipArchive zipArchive = new ZipArchive(fs, ZipArchiveMode.Read))
    {
        zipArchive.ExtractToDirectory("extract");
    }
}

或者在ZIP文件中创建一个新文件:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
using (FileStream fs = new FileStream("sample.zip", FileMode.Open))
{
using (ZipArchive zipArchive = new ZipArchive(fs, ZipArchiveMode.Update))
{
ZipArchiveEntry entry = zipArchive.CreateEntry("NewFile.txt");
using (StreamWriter sw = new StreamWriter(entry.Open()))
{
sw.WriteLine("hello world");
}
}
}
using (FileStream fs = new FileStream("sample.zip", FileMode.Open)) { using (ZipArchive zipArchive = new ZipArchive(fs, ZipArchiveMode.Update)) { ZipArchiveEntry entry = zipArchive.CreateEntry("NewFile.txt"); using (StreamWriter sw = new StreamWriter(entry.Open())) { sw.WriteLine("hello world"); } } }
using (FileStream fs = new FileStream("sample.zip", FileMode.Open))
{
    using (ZipArchive zipArchive = new ZipArchive(fs, ZipArchiveMode.Update))
    {
        ZipArchiveEntry entry = zipArchive.CreateEntry("NewFile.txt");
        using (StreamWriter sw = new StreamWriter(entry.Open()))
        {
            sw.WriteLine("hello world");
        }
    }
}

另一个场景

假设我们需要从内存中获取ZIP文件(byte[]形式),希望在里面创建/重命名/删除某些文件,随后再将操作写回到内存中(也是byte[]),例如从远端下载二进制ZIP,在内存中进行操作后再保存,应当如何操作呢。

你可能会不假思索地写出如下代码:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
static byte[] AddNewFile(byte[] data)
{
byte[] res;
using (MemoryStream ms = new MemoryStream())
{
ms.Write(data);
ms.Position = 0;
using (ZipArchive zipArchive = new ZipArchive(ms, ZipArchiveMode.Update))
{
ZipArchiveEntry entry = zipArchive.CreateEntry("NewFile2.txt");
using (StreamWriter sw = new StreamWriter(entry.Open()))
{
sw.WriteLine("This sample file will no write to data");
}
}
res = new byte[ms.Length];
ms.Position = 0;
ms.Read(res, 0, res.Lenght);
return res;
}
}
static byte[] AddNewFile(byte[] data) { byte[] res; using (MemoryStream ms = new MemoryStream()) { ms.Write(data); ms.Position = 0; using (ZipArchive zipArchive = new ZipArchive(ms, ZipArchiveMode.Update)) { ZipArchiveEntry entry = zipArchive.CreateEntry("NewFile2.txt"); using (StreamWriter sw = new StreamWriter(entry.Open())) { sw.WriteLine("This sample file will no write to data"); } } res = new byte[ms.Length]; ms.Position = 0; ms.Read(res, 0, res.Lenght); return res; } }
static byte[] AddNewFile(byte[] data)
{
    byte[] res;
    using (MemoryStream ms = new MemoryStream())
    {
        ms.Write(data);
        ms.Position = 0;
        using (ZipArchive zipArchive = new ZipArchive(ms, ZipArchiveMode.Update))
        {
            ZipArchiveEntry entry = zipArchive.CreateEntry("NewFile2.txt");
            using (StreamWriter sw = new StreamWriter(entry.Open()))
            {
                sw.WriteLine("This sample file will no write to data");
            }
        }
 
        res = new byte[ms.Length];
        ms.Position = 0;
        ms.Read(res, 0, res.Lenght);
 
        return res;
    }
}

问题

运行一下,会提示MemoryStream被关闭了:

这是个很容易被忽略,但又非常重要的基础知识:.net中许多操作BaseStream的类在Dispose时都会关闭BaseStream,例如StreamReaderStreamWriter等等,当然ZipArchive也不例外。具体可以参考这篇文章:MSDN-CA2202:不要多次释放对象

不过呢,StreamReader、StreamWriter这些类都没有非托管资源,我们没有必要去释放他们。

再回到我们的问题,既然MemoryStream在ZipArchive被释放时关闭了,我们自然也就无法取出数据。那么我们能不能在ZipArchive释放之前取出数据呢。

修改后的代码:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
static byte[] AddNewFile(byte[] data)
{
byte[] res;
using (MemoryStream ms = new MemoryStream())
{
ms.Write(data);
ms.Position = 0;
using (ZipArchive zipArchive = new ZipArchive(ms, ZipArchiveMode.Update))
{
ZipArchiveEntry entry = zipArchive.CreateEntry("NewFile2.txt");
using (StreamWriter sw = new StreamWriter(entry.Open()))
{
sw.WriteLine("This sample file will no write to data");
}
int nowPos = (int)ms.Position;
res = new byte[ms.Length];
ms.Position = 0;
ms.Read(res, 0, res.Length);
ms.Position = nowPos;
}
}
return res;
}
static byte[] AddNewFile(byte[] data) { byte[] res; using (MemoryStream ms = new MemoryStream()) { ms.Write(data); ms.Position = 0; using (ZipArchive zipArchive = new ZipArchive(ms, ZipArchiveMode.Update)) { ZipArchiveEntry entry = zipArchive.CreateEntry("NewFile2.txt"); using (StreamWriter sw = new StreamWriter(entry.Open())) { sw.WriteLine("This sample file will no write to data"); } int nowPos = (int)ms.Position; res = new byte[ms.Length]; ms.Position = 0; ms.Read(res, 0, res.Length); ms.Position = nowPos; } } return res; }
static byte[] AddNewFile(byte[] data)
{
    byte[] res;
    using (MemoryStream ms = new MemoryStream())
    {
        ms.Write(data);
        ms.Position = 0;
        using (ZipArchive zipArchive = new ZipArchive(ms, ZipArchiveMode.Update))
        {
            ZipArchiveEntry entry = zipArchive.CreateEntry("NewFile2.txt");
            using (StreamWriter sw = new StreamWriter(entry.Open()))
            {
                sw.WriteLine("This sample file will no write to data");
            }
 
            int nowPos = (int)ms.Position;
 
            res = new byte[ms.Length];
            ms.Position = 0;
            ms.Read(res, 0, res.Length);
 
            ms.Position = nowPos;
        }
    }
 
    return res;
}

新的问题

这次我们尝试在ZipArchive释放之前取出数据,运行一下,将结果写到文件中,却发现没有新文件NewFile2.txt.

这让问题陷入了困境,既然能想到的方法都不能成功,不如查看一下ZipArchive的底层实现

官方源码:.Net-ZipArchive源码

有意思的地方来了,ZipArchive内部有一个名为WriteFile的私有方法(616行)。此方法会更新我们提供的BaseStream,也就是MemoryStream。而这个方法只在Dispose方法(199行)中被调用

下面是Dispose方法的代码

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
protected virtual void Dispose(bool disposing)
{
if (disposing && !_isDisposed)
{
try
{
switch (_mode)
{
case ZipArchiveMode.Read:
break;
case ZipArchiveMode.Create:
case ZipArchiveMode.Update:
default:
Debug.Assert(_mode == ZipArchiveMode.Update || _mode == ZipArchiveMode.Create);
WriteFile();
break;
}
}
finally
{
CloseStreams();
_isDisposed = true;
}
}
}
protected virtual void Dispose(bool disposing) { if (disposing && !_isDisposed) { try { switch (_mode) { case ZipArchiveMode.Read: break; case ZipArchiveMode.Create: case ZipArchiveMode.Update: default: Debug.Assert(_mode == ZipArchiveMode.Update || _mode == ZipArchiveMode.Create); WriteFile(); break; } } finally { CloseStreams(); _isDisposed = true; } } }
protected virtual void Dispose(bool disposing)
{
    if (disposing && !_isDisposed)
    {
        try
        {
            switch (_mode)
            {
                case ZipArchiveMode.Read:
                    break;
                case ZipArchiveMode.Create:
                case ZipArchiveMode.Update:
                default:
                    Debug.Assert(_mode == ZipArchiveMode.Update || _mode == ZipArchiveMode.Create);
                    WriteFile();
                    break;
            }
        }
        finally
        {
            CloseStreams();
            _isDisposed = true;
        }
    }
}

可以看到,在ZipArchive释放时,会先调用WriteFile来将改动更新基础流,然后立刻关闭基础流。

如果我们提供的是FileStream,这会是非常好的做法:当我们释放ZipArchive时,会立刻保存改动并关闭FileStream。但我们现在使用的是MemoryStream,这让问题变得非常棘手,因为我们没有任何方法让它更新Stream,除非Dispose它,但这又会关闭我们的MemoryStream,使得我们无法取出数据。

解决方法

至此,似乎只有一种解决方法:通过反射来调用私有的WriteFile来更新Stream,随后取出数据,再释放ZipArchive。

InvokeWriteFile方法用于调用ZipArchive的私有方法WriteFile。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
static void InvokeWriteFile(ZipArchive zipArchive)
{
foreach (MethodInfo method in zipArchive.GetType().GetRuntimeMethods())
{
if (method.Name == "WriteFile")
{
method.Invoke(zipArchive, new object[0]);
}
}
}
static byte[] AddNewFile(byte[] data)
{
byte[] res;
using (MemoryStream ms = new MemoryStream())
{
ms.Write(data);
ms.Position = 0;
using (ZipArchive zipArchive = new ZipArchive(ms, ZipArchiveMode.Update))
{
ZipArchiveEntry entry = zipArchive.CreateEntry("NewFile2.txt");
using (StreamWriter sw = new StreamWriter(entry.Open()))
{
sw.WriteLine("This sample file will write to data");
}
InvokeWriteFile(zipArchive);
int nowPos = (int)ms.Position;
res = new byte[ms.Length];
ms.Position = 0;
ms.Read(res, 0, res.Length);
ms.Position = nowPos;
}
}
return res;
}
static void InvokeWriteFile(ZipArchive zipArchive) { foreach (MethodInfo method in zipArchive.GetType().GetRuntimeMethods()) { if (method.Name == "WriteFile") { method.Invoke(zipArchive, new object[0]); } } } static byte[] AddNewFile(byte[] data) { byte[] res; using (MemoryStream ms = new MemoryStream()) { ms.Write(data); ms.Position = 0; using (ZipArchive zipArchive = new ZipArchive(ms, ZipArchiveMode.Update)) { ZipArchiveEntry entry = zipArchive.CreateEntry("NewFile2.txt"); using (StreamWriter sw = new StreamWriter(entry.Open())) { sw.WriteLine("This sample file will write to data"); } InvokeWriteFile(zipArchive); int nowPos = (int)ms.Position; res = new byte[ms.Length]; ms.Position = 0; ms.Read(res, 0, res.Length); ms.Position = nowPos; } } return res; }
static void InvokeWriteFile(ZipArchive zipArchive)
{
    foreach (MethodInfo method in zipArchive.GetType().GetRuntimeMethods())
    {
        if (method.Name == "WriteFile")
        {
            method.Invoke(zipArchive, new object[0]);
        }
    }
}
 
static byte[] AddNewFile(byte[] data)
{
    byte[] res;
    using (MemoryStream ms = new MemoryStream())
    {
        ms.Write(data);
        ms.Position = 0;
        using (ZipArchive zipArchive = new ZipArchive(ms, ZipArchiveMode.Update))
        {
            ZipArchiveEntry entry = zipArchive.CreateEntry("NewFile2.txt");
            using (StreamWriter sw = new StreamWriter(entry.Open()))
            {
                sw.WriteLine("This sample file will write to data");
            }
 
            InvokeWriteFile(zipArchive);
            int nowPos = (int)ms.Position;
 
            res = new byte[ms.Length];
            ms.Position = 0;
            ms.Read(res, 0, res.Length);
 
            ms.Position = nowPos;
        }
    }
 
    return res;
}

写入成功!

Azure99

底层码农,休闲音游玩家,偶尔写写代码

看看这些?

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注