41

我一直试图让 .NET 4.5 ( System.IO.Compression.ZipArchive) 中包含的“新”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
4

5 回答 5

54

注意:这已在 .Net Core 2.0 中修复。我不确定 .Net Framework 的修复状态如何。


Calbertoferreira 的回答有一些有用的信息,但结论大多是错误的。要创建档案,您不需要搜索,但您确实需要能够阅读Position.

根据文档Position,只有可搜索的流才应该支持读取,但ZipArchive似乎即使是不可搜索的流也需要这样做,这是一个错误

因此,要支持将 ZIP 文件直接写入到,您需要做的OutputStream就是将其包装在Stream支持获取Position. 就像是:

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 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("========================");
    }
}
于 2014-02-02T16:43:48.357 回答
6

对 2014 年 2 月 2 日 svick 的回答进行了改进。我发现有必要实现 Stream 抽象类的更多方法和属性,并将 pos 成员声明为 long。在那之后,它就像一个魅力。我没有对这个类进行广泛的测试,但它可以用于在 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;
    }
}
于 2016-03-18T10:19:36.087 回答
5

如果您将您的代码改编版本与MSDN 页面中提供的版本进行比较,您会发现从未使用过 ZipArchiveMode.Create,使用的是 ZipArchiveMode.Update。

尽管如此,主要问题是不支持更新模式下 ZipArchive 需要的 Read and Seek 的 OutputStream

当您将模式设置为更新时,底层文件或流必须支持读取、写入和查找。整个存档的内容保存在内存中,在处理存档之前,不会将数据写入底层文件或流。

资料来源:MSDN

创建模式没有任何异常,因为它只需要编写:

当您将模式设置为 Create 时,底层文件或流必须支持写入,但不必支持查找。存档中的每个条目只能打开一次以进行写入。如果您创建单个条目,则数据会在可用时立即写入基础流或文件。如果您创建多个条目(例如通过调用 CreateFromDirectory 方法),则在创建所有条目后将数据写入基础流或文件。

资料来源:MSDN

我相信您不能直接在 OutputStream 中创建 zip 文件,因为它是网络流并且不支持搜索:

流可以支持搜索。查找是指查询和修改流中的当前位置。查找能力取决于流所具有的后备存储类型。例如,网络流没有统一的当前位置概念,因此通常不支持查找。

另一种方法是写入内存流,然后使用 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 输出文件。

于 2013-05-22T10:30:42.407 回答
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);
}

(如果这看起来很明显,那对我来说不是!)

于 2014-10-03T14:11:48.407 回答
0

大概这不是一个 MVC 应用程序,您可以轻松地在其中使用FileStreamResult该类。

我目前正在使用它,并ZipArchive使用 a 创建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);
        }
    }
}

CodePlex 上的整个 FileStreamResult

这是我如何生成和返回ZipArchive. 用上面的方法
替换 FSR 应该没有问题,从下面的代码变成:WriteFileFileStreamresultStream

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;

最后,如果您要编写大型流(或在任何给定时间写入大量流),那么您可能需要考虑使用匿名管道在将数据写入到底层流之后立即将其写入输出流。压缩文件。因为您将在服务器的内存中保存所有文件内容。这个对类似问题的回答的结尾很好地解释了如何做到这一点。

于 2014-10-29T15:46:32.430 回答