Escribir en ZipArchive usando HttpContext OutputStream

He estado tratando de incluir el “nuevo” ZipArchive en .NET 4.5 ( System.IO.Compression.ZipArchive ) para que funcione en un sitio ASP.NET. Pero parece que no le gusta escribir en la secuencia de HttpContext.Response.OutputStream .

Mi siguiente ejemplo de código arrojará

System.NotSupportedException: el método especificado no es compatible

tan pronto como se intente escribir en la transmisión.

La propiedad CanWrite en la secuencia devuelve verdadero.

Si cambio el OutputStream con un filestream, apuntando a un directorio local, funciona. ¿Lo que da?

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

Stacktrace:

 [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 

Nota: Esto se ha corregido en .Net Core 2.0. No estoy seguro de cuál es el estado de la solución para .Net Framework.


La respuesta de Calbertoferreira tiene información útil, pero la conclusión es mayormente incorrecta. Para crear un archivo, no necesita buscar, pero sí necesita poder leer la Position .

De acuerdo con la documentación , la Position lectura debe ser admitida solo para transmisiones buscables, pero ZipArchive parece requerir esto incluso de flujos no buscables, que es un error .

Por lo tanto, todo lo que necesita hacer para respaldar la escritura de archivos ZIP directamente en OutputStream es envolverlo en un Stream personalizado que admita obtener Position . Algo como:

 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 } 

Usando esto, el siguiente código escribirá un archivo ZIP en 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("========================"); } } 

Un refinamiento de la respuesta de svick del 2 de febrero de 2014. Descubrí que era necesario implementar más métodos y propiedades de la clase abstracta de Stream y declarar el miembro pos como el más largo. Después de eso funcionó como un encanto. No he probado ampliamente esta clase, pero funciona con el propósito de devolver un ZipArchive en HttpResponse. Supongo que he implementado Seek and Read correctamente, pero pueden necesitar algunos ajustes.

 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; } } 

Si compara su adaptación de código con la versión presentada en la página de MSDN verá que ZipArchiveMode.Create nunca se utiliza, lo que se utiliza es ZipArchiveMode.Update.

A pesar de eso, el principal problema es el OutputStream que no admite lectura y búsqueda, que ZipArchive necesita en el modo de actualización :

Cuando configura el modo en Actualizar, el archivo o secuencia subyacente debe ser compatible con lectura, escritura y búsqueda. El contenido del archivo completo se almacena en la memoria y no se graban datos en el archivo o la secuencia subyacente hasta que se elimine el archivo.

Fuente: MSDN

No obtuvo ninguna excepción con el modo de creación porque solo necesita escribir:

Cuando establece el modo en Crear, el archivo o secuencia subyacente debe admitir la escritura, pero no tiene que ser compatible con la búsqueda. Cada entrada en el archivo se puede abrir solo una vez para escribir. Si crea una sola entrada, los datos se escriben en la secuencia o archivo subyacente tan pronto como estén disponibles. Si crea varias entradas, como al llamar al método CreateFromDirectory, los datos se escriben en la secuencia o archivo subyacente después de que se crean todas las entradas.

Fuente: MSDN

Creo que no se puede crear un archivo zip directamente en OutputStream ya que es una transmisión en red y no se admite la búsqueda:

Las streams pueden apoyar la búsqueda. Buscar se refiere a consultar y modificar la posición actual dentro de una secuencia. La capacidad de búsqueda depende del tipo de tienda de respaldo que tenga una transmisión. Por ejemplo, las transmisiones de red no tienen un concepto unificado de una posición actual, y por lo tanto, típicamente no son compatibles con la búsqueda.

Una alternativa podría ser escribir en una secuencia de memoria, luego usar el método OutputStream.Write para enviar el archivo 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); 

EDITAR: con la retroalimentación de los comentarios y la lectura adicional, podría estar creando archivos Zip grandes, por lo que la secuencia de la memoria podría causarle problemas.

En este caso, le sugiero que cree el archivo zip en el servidor web y luego genere el archivo utilizando Response.WriteFile.

Una versión simplificada de la respuesta de svick para comprimir un archivo del lado del servidor y enviarlo a través del OutputStream:

 using (var outputStream = new PositionWrapperStream(Response.OutputStream)) using (var archive = new ZipArchive(outputStream, ZipArchiveMode.Create, false)) { var entry = archive.CreateEntryFromFile(fullPathOfFileOnDisk, fileNameAppearingInZipArchive); } 

(¡En caso de que esto parezca obvio, no fue para mí!)

Es de suponer que esta no es una aplicación de MVC, donde simplemente podría usar la clase FileStreamResult .

Estoy usando esto actualmente con ZipArchive creado usando un MemoryStream , así que sé que funciona.

Con esto en mente, eche un vistazo al método 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); } } } 

( FileStreamResult completo en CodePlex )

Así es como estoy generando y devolviendo ZipArchive .
No debería tener problemas para reemplazar el FSR con las agallas del método WriteFile de arriba, donde FileStream convierte en resultStream del siguiente código:

 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; 

Por último , si va a escribir grandes flujos (o un número mayor de ellos en un momento dado), puede considerar utilizar tubos anónimos para escribir los datos en la secuencia de salida inmediatamente después de escribirlos en la secuencia subyacente en el archivo zip. Porque mantendrá todo el contenido del archivo en la memoria en el servidor. El final de esta respuesta a una pregunta similar tiene una buena explicación de cómo hacer eso.