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

简介

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

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

using (FileStream fs = new FileStream("sample.zip", FileMode.Open))
{
    using (ZipArchive zipArchive = new ZipArchive(fs, ZipArchiveMode.Read))
    {
        zipArchive.ExtractToDirectory("extract");
    }
}

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

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,进行操作后保存,应当如何操作呢。

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

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释放之前取出数据呢。

修改后的代码:

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方法的代码

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

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

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

看看这些?

发表回复

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