.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,例如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; }
新的问题
这次我们尝试在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; }
写入成功!