.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");
}
}
Code language: JavaScript (javascript)或者在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");
}
}
}
Code language: JavaScript (javascript)另一个场景
假设我们需要从内存中获取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;
}
}
Code language: JavaScript (javascript)问题
运行一下,会提示MemoryStream被关闭了:

这是个很容易被忽略,但又非常重要的基础知识:.net中许多操作BaseStream的类在Dispose时都会关闭BaseStream,例如StreamReader、StreamWriter等等,当然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;
}
Code language: JavaScript (javascript)新的问题
这次我们尝试在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;
}
}
}
Code language: JavaScript (javascript)可以看到,在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;
}
Code language: JavaScript (javascript)写入成功!

