我一直在尝试将.NET 4.5中包含的“新”ZipArchive(System.IO.Compression.ZipArchive
)用于ASP.NET站点。但似乎它不喜欢写入HttpContext.Response.OutputStream
的流。
我的以下代码示例将抛出
System.NotSupportedException:不支持指定的方法
只要在流上尝试写入。
流上的CanWrite
属性返回true。
如果我将OutputStream与文件流交换,指向本地目录,它可以工作。是什么给了什么?
ZipArchive archive = new ZipArchive(HttpContext.Response.OutputStream, ZipArchiveMode.Create, false);
ZipArchiveEntry entry = archive.CreateEntry("filename");
using (StreamWriter writer = new StreamWriter(entry.Open()))
{
writer.WriteLine("Information about this package.");
writer.WriteLine("========================");
}
堆栈跟踪:
[NotSupportedException: Specified method is not supported.]
System.Web.HttpResponseStream.get_Position() +29
System.IO.Compression.ZipArchiveEntry.WriteLocalFileHeader(Boolean isEmptyFile) +389
System.IO.Compression.DirectToArchiveWriterStream.Write(Byte[] buffer, Int32 offset, Int32 count) +94
System.IO.Compression.WrappedStream.Write(Byte[] buffer, Int32 offset, Int32 count) +41
答案 0 :(得分:43)
注意:这已在.Net Core 2.0中修复。我不确定.Net Framework修复程序的状态是什么。
Calbertoferreira的回答有一些有用的信息,但结论大多是错误的。要创建存档,您不需要搜索,但您需要能够阅读Position
。
根据the documentation,只有可搜索流才支持阅读Position
,但ZipArchive
似乎甚至需要来自不可搜索的流,a bug。
因此,支持将ZIP文件直接写入OutputStream
所需要做的就是将其包装在支持获取Stream
的自定义Position
中。类似的东西:
class PositionWrapperStream : Stream
{
private readonly Stream wrapped;
private int pos = 0;
public PositionWrapperStream(Stream wrapped)
{
this.wrapped = wrapped;
}
public override bool CanSeek { get { return false; } }
public override bool CanWrite { get { return true; } }
public override long Position
{
get { return pos; }
set { throw new NotSupportedException(); }
}
public override void Write(byte[] buffer, int offset, int count)
{
pos += count;
wrapped.Write(buffer, offset, count);
}
public override void Flush()
{
wrapped.Flush();
}
protected override void Dispose(bool disposing)
{
wrapped.Dispose();
base.Dispose(disposing);
}
// all the other required methods can throw NotSupportedException
}
使用此代码,以下代码会将ZIP存档写入OutputStream
:
using (var outputStream = new PositionWrapperStream(Response.OutputStream))
using (var archive = new ZipArchive(outputStream, ZipArchiveMode.Create, false))
{
var entry = archive.CreateEntry("filename");
using (var writer = new StreamWriter(entry.Open()))
{
writer.WriteLine("Information about this package.");
writer.WriteLine("========================");
}
}
答案 1 :(得分:6)
对svick 2014年2月2日答案的改进。我发现有必要实现Stream抽象类的更多方法和属性,并声明pos成员的长度。之后,它就像一个魅力。我没有对这个类进行过广泛的测试,但它的工作原理是在HttpResponse中返回一个ZipArchive。我假设我已经正确地实现了Seek和Read,但是他们可能需要一些调整。
class PositionWrapperStream : Stream
{
private readonly Stream wrapped;
private long pos = 0;
public PositionWrapperStream(Stream wrapped)
{
this.wrapped = wrapped;
}
public override bool CanSeek
{
get { return false; }
}
public override bool CanWrite
{
get { return true; }
}
public override long Position
{
get { return pos; }
set { throw new NotSupportedException(); }
}
public override bool CanRead
{
get { return wrapped.CanRead; }
}
public override long Length
{
get { return wrapped.Length; }
}
public override void Write(byte[] buffer, int offset, int count)
{
pos += count;
wrapped.Write(buffer, offset, count);
}
public override void Flush()
{
wrapped.Flush();
}
protected override void Dispose(bool disposing)
{
wrapped.Dispose();
base.Dispose(disposing);
}
public override long Seek(long offset, SeekOrigin origin)
{
switch (origin)
{
case SeekOrigin.Begin:
pos = 0;
break;
case SeekOrigin.End:
pos = Length - 1;
break;
}
pos += offset;
return wrapped.Seek(offset, origin);
}
public override void SetLength(long value)
{
wrapped.SetLength(value);
}
public override int Read(byte[] buffer, int offset, int count)
{
pos += offset;
int result = wrapped.Read(buffer, offset, count);
pos += count;
return result;
}
}
答案 2 :(得分:3)
如果您将代码修改与MSDN page中提供的版本进行比较,您将看到ZipArchiveMode.Create从未使用过,使用的是ZipArchiveMode.Update。
尽管如此,主要问题是OutputStream不支持ZipArchive在更新模式下需要的Read和Seek :
将模式设置为“更新”时,基础文件或流必须 支持阅读,写作和寻求。整个内容 存档保存在内存中,并且没有数据写入底层 文件或流,直到存档被处理。
您没有在创建模式下获得任何异常,因为它只需要写:
将模式设置为“创建”时,基础文件或流必须支持写入,但不必支持搜索。存档中的每个条目只能打开一次以进行写入。如果创建单个条目,则数据一旦可用就会立即写入基础流或文件。如果创建多个条目(例如通过调用CreateFromDirectory方法),则会在创建所有条目后将数据写入基础流或文件。
我相信你不能直接在OutputStream中创建一个zip文件,因为它是一个网络流并且不支持搜索:
Streams可以支持搜索。寻求是指查询和修改流中的当前位置。搜索功能取决于流所具有的后备存储类型。例如,网络流没有统一的当前位置概念,因此通常不支持搜索。
另一种方法是写入内存流,然后使用OutputStream.Write方法发送zip文件。
MemoryStream ZipInMemory = new MemoryStream();
using (ZipArchive UpdateArchive = new ZipArchive(ZipInMemory, ZipArchiveMode.Update))
{
ZipArchiveEntry Zipentry = UpdateArchive.CreateEntry("filename.txt");
foreach (ZipArchiveEntry entry in UpdateArchive.Entries)
{
using (StreamWriter writer = new StreamWriter(entry.Open()))
{
writer.WriteLine("Information about this package.");
writer.WriteLine("========================");
}
}
}
byte[] buffer = ZipInMemory.GetBuffer();
Response.AppendHeader("content-disposition", "attachment; filename=Zip_" + DateTime.Now.ToString() + ".zip");
Response.AppendHeader("content-length", buffer.Length.ToString());
Response.ContentType = "application/x-compressed";
Response.OutputStream.Write(buffer, 0, buffer.Length);
编辑:根据评论和进一步阅读的反馈,您可能会创建大型Zip文件,因此内存流可能会导致问题。
在这种情况下,我建议您在Web服务器上创建zip文件,然后使用Response.WriteFile输出文件。
答案 3 :(得分:0)
svick 的简化版本,用于压缩服务器端文件并通过OutputStream发送它:
using (var outputStream = new PositionWrapperStream(Response.OutputStream))
using (var archive = new ZipArchive(outputStream, ZipArchiveMode.Create, false))
{
var entry = archive.CreateEntryFromFile(fullPathOfFileOnDisk, fileNameAppearingInZipArchive);
}
(如果这看起来很明显,那对我来说就不行了!)
答案 4 :(得分:0)
据推测,这不是一个MVC应用程序,您可以轻松地使用FileStreamResult
类。
我目前正在使用ZipArchive
使用MemoryStream
创建,所以我知道它有效。
考虑到这一点,请查看FileStreamResult.WriteFile()
方法:
protected override void WriteFile(HttpResponseBase response)
{
// grab chunks of data and write to the output stream
Stream outputStream = response.OutputStream;
using (FileStream)
{
byte[] buffer = newbyte[_bufferSize];
while (true)
{
int bytesRead = FileStream.Read(buffer, 0, _bufferSize);
if (bytesRead == 0)
{
// no more data
break;
}
outputStream.Write(buffer, 0, bytesRead);
}
}
}
(Entire FileStreamResult on CodePlex)
以下是我如何生成并返回ZipArchive
您应该没有问题替换FSR与上面的WriteFile
方法的内容,其中FileStream
从下面的代码变为resultStream
:
var resultStream = new MemoryStream();
using (var zipArchive = new ZipArchive(resultStream, ZipArchiveMode.Create, true))
{
foreach (var doc in req)
{
var fileName = string.Format("Install.Rollback.{0}.v{1}.docx", doc.AppName, doc.Version);
var xmlData = doc.GetXDocument();
var fileStream = WriteWord.BuildFile(templatePath, xmlData);
var docZipEntry = zipArchive.CreateEntry(fileName, CompressionLevel.Optimal);
using (var entryStream = docZipEntry.Open())
{
fileStream.CopyTo(entryStream);
}
}
}
resultStream.Position = 0;
// add the Response Header for downloading the file
var cd = new ContentDisposition
{
FileName = string.Format(
"{0}.{1}.{2}.{3}.Install.Rollback.Documents.zip",
DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, (long)DateTime.Now.TimeOfDay.TotalSeconds),
// always prompt the user for downloading, set to true if you want
// the browser to try to show the file inline
Inline = false,
};
Response.AppendHeader("Content-Disposition", cd.ToString());
// stuff the zip package into a FileStreamResult
var fsr = new FileStreamResult(resultStream, MediaTypeNames.Application.Zip);
return fsr;
最后,如果您要编写大型流(或在任何给定时间更多数量的流),那么您可能需要考虑使用匿名管道将数据立即写入输出流将其写入zip文件中的基础流后。因为您将把所有文件内容保存在服务器的内存中。对The end of this answer类似的问题有一个很好的解释,说明如何做到这一点。