【问题标题】:Writing to ZipArchive using the HttpContext OutputStream使用 HttpContext OutputStream 写入 ZipArchive
【发布时间】:2013-05-11 05:33:17
【问题描述】:

我一直在尝试让 .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

【问题讨论】:

  • 你是在开发环境还是服务器上试用
  • 我目前在本地运行,所以是开发环境。我从 IHttpHandler 获取 HttpContext。
  • 好的,MVC 还是 Web 表单?
  • 网络表单。遗憾的是,我在项目中使用 MVC 受到限制。
  • 你可以尝试线性编码,你可能缺少 response.contenttype 等;与此同时,我将尝试重新创建错误并回复您。快乐编码

标签: c# .net-4.5 outputstream httpcontext asp.net-4.5


【解决方案1】:

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

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

当您将模式设置为更新时,基础文件或流必须 支持阅读、写作和寻找。整个内容 存档保存在内存中,没有数据写入底层 文件或流,直到档案被处置。

Source: MSDN

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

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

Source: 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);

编辑:根据 cmets 的反馈和进一步阅读,您可能正在创建大型 Zip 文件,因此内存流可能会给您带来问题。

在这种情况下,我建议您在 Web 服务器上创建 zip 文件,然后使用 Response.WriteFile 输出文件。

【讨论】:

  • 所以说直接写到outputStream是不可能的吧?就像我之前说的,memoryStream 不是一个选项,因为文件大小会变化并且可能会变得很大。
  • 我正在尝试使用 response.filter 来查看是否可以找到解决方法,但根据 MSDN,您不能:Streams 可以支持搜索。查找是指查询和修改流中的当前位置。查找能力取决于流所具有的后备存储类型。例如,网络流没有统一的当前位置概念,因此通常不支持搜索。
  • 我很困惑,你的引号不是意味着它应该与ZipArchiveMode.Create一起使用吗?
  • 你是对的。问题出在更新 zip 存档时,要更新流必须支持查找的存档。
  • ZipArchiveMode.Create 也会出现问题。
【解决方案2】:

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


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

根据the documentation,阅读Position 应该只支持可搜索的流,但ZipArchive 似乎要求即使来自不可搜索的流,即a bug

因此,支持将 ZIP 文件直接写入 OutputStream 所需要做的就是将其包装在支持获取 Position 的自定义 Stream 中。比如:

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("========================");
    }
}

【讨论】:

  • 很好,我确实想知道是否只是获得了问题所在的位置,并假设还会有其他问题。我没试过,你这样做了,谢谢你的发现!我同意,ZipArchive 代码可以轻松跟踪它已经写入的字节数,并且不需要像这样的包装器。
  • 确实不错,我使用了第 3 方 zip 库,因为当时我找不到解决方案。但是当我有时间的时候,我会回去尝试使用你的想法。如果有效,我会返回并标记它已回答! :)
  • 在这个问题上花了一天多的时间后,您帮助我解决了一个问题,使用 ZipArchive 和我实现的自定义不可搜索流。谢谢!
  • @svick 看起来不错,唯一的一点 - 最好将“pos”变量类型更改为 long,因为 Position 属性具有 long 类型。
【解决方案3】:

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】:

    大概这不是一个 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 的方式。
    用上面的 WriteFile 方法的胆量替换 FSR 应该没有问题,其中 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 对类似的问题有一个很好的解释如何做到这一点。

    【讨论】:

      【解决方案5】:

      对 svick 2014 年 2 月 2 日的回答进行了改进。我发现有必要实现 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;
          }
      }
      

      【讨论】:

      • 看起来很奇怪 - CanSeek 返回 false,但同时实现了 Seek(...) 方法。另外我想不需要阅读 Response.OutputStream。所以我更喜欢为 CanRead 属性返回 false 并且不实现 Read(...) 方法。
      • 一旦我让它在我的实现中工作,我就停下来了。但你可能是对的。
      猜你喜欢
      • 2012-04-26
      • 1970-01-01
      • 2019-09-13
      • 2022-09-30
      • 1970-01-01
      • 2020-10-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多