Inicio > .NET Framework, Apunte, C# > Creación de archivos ZIP en .NET

Creación de archivos ZIP en .NET


Hace algún tiempo publiqué un artículo sobre cómo manejar flujos de datos: la clase Stream y sus derivadas. Entre lo que expuse en aquella entrada está el uso de las clases GZipStream y DeflateStream como flujos para comprimir datos. Una de las limitantes, sin embargo, de estas clases, es que aunque generan un flujo que puede guardarse eventualmente en disco duro vía FileStream, no son compatibles con programas populares como WinZip, WinRAR o el mismo componente Zip que viene con el Windows Explorer.

O mejor dicho: GZipStream y DeflateStream comprimen un archivo, pero no crean un contenedor para archivos compresos, que al final del día es lo que es un archivo ZIP. En fin, que hasta hace poco, estas clases se podían usar para comprimir archivos de uso propio de la aplicación, pero no para que pudiesen distribuirse los archivos.

Con la salida de .NET 4.5, sin embargo, apareció unas clases en System.IO.Compression que no causaron mucho ruido o atención, pero que a mi entender son súper importantes, porque permiten precisamente comprimir archivos dentro de un contenedor en formato ZIP estándar. Las clases son las siguientes.

1.- ZipArchiveEntry. Representa un archivo compreso dentro de un archivo ZIP (ZipArchive).

2.- ZipArchive. Representa un archivo ZIP, que tiene archivos compresos (ZipArchiveEntry).

3.- ZipFile. Contiene métodos estáticos para trabajar con archivos ZIP y archivos compresos.

4.- ZipFileExtensions. Contiene métodos de extensión para ZipArchive y ZipArchiveEntry.


Veamos entonces algunos ejemplos sobre cómo utilizar estas clases.

Crear un archivo zip

 

using System.IO;
using System.IO.Compression;
…

// 1
FileInfo sourceFile = new FileInfo(@"C:\reporte1.xlsx");
FileStream sourceStream = sourceFile.OpenRead();
// 2
FileStream stream = new FileStream(@"C:\reportes.zip", FileMode.Open);
// 3 
ZipArchive archive = new ZipArchive(stream, ZipArchiveMode.Create);
// 4 
ZipArchiveEntry entry = archive.CreateEntry(sourceFile.Name);
// 5
Stream zipStream = entry.Open();
// 6
sourceStream.CopyTo(zipStream);
// 7
zipStream.Close();
sourceStream.Close();
archive.Dispose();
stream.Close();

 

Bueno, veamos que sucede. Asumimos que en C:\ existe un archivo, reporte1.xlsx, y que tenemos permisos de escritura en C:\. La verdad que escogí ese directorio para no hacer tan largo el ejemplo. En fin. Por tanto, primero abrimos el archivo que queremos añadir a un archivo zip. Para ello, usamos FileInfo y FileStream en modo lectura, como siempre.

El segundo paso es que necesitamos crear un flujo de archivo para nuestro archivo zip contenedor. Volvemos a usar un FileStream tradicional, hasta aquí no hay nada nuevo. Lo nuevo viene en el tercer paso: creamos un ZipArchive y le pasamos como primer parámetro el flujo que hemos creado para nuestro zip (así garantizamos que el ZipArchive escriba sobre éste), y como segundo parámetro pasamos la enumeración ZipArchiveMode.Create para indicarle que vamos a crear entradas. Y ahora sí, viene el cuarto paso: creamos una entrada invocando ZipArchive.CreateEntry, y le pasamos el nombre con el que queremos identificarlo. Para más fácil, le pasamos el nombre original mediante FileInfo.Name. Y ahora a tenemos un ZipArchiveEntry, aunque vacío. En el quinto paso abrimos el flujo de esa entrada y obtenemos un Stream donde podemos escribir. El sexto paso vacía el flujo del archivo que queremos comprimir a la entrada dentro del zip que hemos creado. Aquí es donde realmente ocurre la compresión. Y finalmente, en el paso 7 cerramos flujos y limpiamos memoria. Nota que puedes usar directivas "using" en lugar de invocar a Close y Dispose.

Resumiendo hasta el momento: 1) abrimos archivo fuente, 2 y 3) creamos archivo zip, 4) creamos una entrada vacía en el zip.

Extraer archivo de un zip

 

using System.IO; 
using System.IO.Compression; 
… 

// 1 
FileInfo zipFile = new FileInfo(@"C:\reportes.zip"); 
FileStream zipStream = zipFile.OpenRead(); 
// 2 
ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Read); 
// 3 
ZipArchiveEntry entry = archive.GetEntry("reporte1.xlsx"); 
// 4 
Stream stream = entry.Open(); 
// 5 
FileInfo destFile = new FileInfo(@"C:\reporte1.xlsx"); 
if (file.Exists) { 
    file.Delete(); 
    file.Refresh(); 
} 
FileStream destStream = destFile.OpenWrite(); 
// 6 
stream.CopyTo(destStream); 
// 7 
stream.Close(); 
destStream.Close(); 
archive.Dispose(); 
zipStream.Close();

 

Es un proceso similar, de hecho, pero a la inversa. Veamos. En 1, abrimos un flujo proveniente del archivo ZIP del cual queremos extraer el archivo. Luego, en 2 creamos el ZipArchive, pasándole el flujo que acabamos de abrir, y la bandera ZipArchiveMode.Read, para indicarle que vamos a leer. Esto es de suma importancia, porque las banderas de lectura, escritura y búsqueda de los objetos Stream dependen de esta bandera. Si pones un Read e intentas escribir, seguramente tendrás un InvalidOperationException entre manos.

Bueno bueno, ya. En 3 usamos el método GetEntry, pasándole el nombre del archivo dentro del zip, es decir, de la entrada, y obtenemos un objeto de tipo ZipAchiveEntry. Alternativamente, puedes iterar sobre ZipArchive.Entries para ver todas las entradas existentes. Ya con el ZipArchiveEntry podemos abrir el stream mediante ZipArchiveEntry.Open, como se muestra en 4. Ya tenemos el flujo, ahora sólo debemos guardarlo en disco duro. El paso 5 abre el archivo destino donde guardaremos el archivo descompreso. Antes revisamos que si el archivo existe, lo eliminemos. Y abrimos el flujo de dicho archivo en modo escritura.

Y ahora sí, en 6 vemos cómo copiamos los bytes del flujo del archivo compreso al flujo del archivo en disco duro. Perfecto, ya estamos del otro lado. Lo último que resta es cerrar los objetos invocando a Close y Dispose, y listo.

Comprimir un directorio entero

Muy similar a comprimir un archivo, sólo que nos basamos en DirectoryInfo… Veamos.

 

using System.IO; 
using System.IO.Compression; 
… 

// 1 
DirectoryInfo dir = new DirectoryInfo(@"C:\reportes\"); 
// 2 
FileStream stream = new FileStream(@"C:\reportes.zip", FileMode.Open); 
// 3 
ZipArchive archive = new ZipArchive(stream, ZipArchiveMode.Create); 
// 4 
FileInfo[] sourceFiles = dir.GetFiles(); 
foreach (FileInfo sourceFile in sourceFiles) 
{ 
    // 5 
    Stream sourceStream = sourceFile.OpenRead(); 
    // 6 
    ZipArchiveEntry entry = archive.CreateEntry(sourceFile.Name); 
    // 7 
    Stream zipStream = entry.Open(); 
    // 8 
    sourceStream.CopyTo(zipStream); 
    // 9 
    zipStream.Close(); 
    sourceStream.Close(); 
} 
// 10 
archive.Dispose(); 
stream.Close();

  

Como puedes apreciar, es un código muy similar, sólo que usamos DirectoryInfo.GetFiles para obtener los archivos, e iteramos sobre estos. Veamos. En 1 abrimos el directorio que queremos comprimir. En 2, creamos el archivo donde guardaremos el zip y en 3 asociamos el flujo de éste al archivo zip. En 4 obtenemos los archivos del directorio y comenzamos a iterar para cada uno de ellos. En 5 abrimos el flujo de cada archivo, en 6 creamos la entrada dentro del zip, en 7 abrimos el flujo de la entrada dentro del zip, y en 8 vaciamos los bytes del archivo a la entrada dentro del zip. En 9 limpiamos los flujos locales. Finalmente, en 10 hacemos limpieza del archivo zip y del flujo. ¡Easy Peasy!

Bueno, si no quieres hacer tanto, huevas, puedes usar ZipFile.CreateFromDirectory:

ZipFile.CreateFromDirectory(@"C:\reportes\", @"C:\reportes.zip");

y listo. Pero bueno, lo interesante del código anterior era mostrar cómo hacerlo manual. Ahora que sabes cómo, usa la versión corta. Nota: la versión corta está disponible en .NET 4.5, pero no en .NET para Windows Store Apps. Así que si estabas pensando hacer alguna app para Windows 8 / RT, te la pelas: tendrás que emplear la versión larga.

Extraer los archivos de un zip

 

Vamos ahora a extraer todos los archivos de un zip a un directorio. Sea pues.

 

using System.IO; 
using System.IO.Compression; 
… 

// 1 
FileInfo zipFile = new FileInfo(@"C:\reportes.zip"); 
FileStream zipStream = zipFile.OpenRead(); 
// 2 
ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Read); 
// 3 
foreach (ZipArchiveEntry entry in archive.Entries) 
{ 
    // 4 
    Stream stream = entry.Open(); 
    // 5 
    FileInfo destFile = new FileInfo(@"C:\reporte\" + entry.Name); 
    if (file.Exists) { 
        file.Delete(); 
        file.Update(); 
    } 
    FileStream destStream = destFile.OpenWrite(); 
    // 6 
    stream.CopyTo(destStream); 
    // 7 
    stream.Close(); 
    destStream.Close(); 
} 
// 8 
archive.Dispose(); 
zipStream.Close(); 

Pues easy peasy también, ¿no? Ya nos la sabemos: abrimos archivo zip (1), asociamos el archivo con un zip (2), iteramos por cada entrada del zip (3), abrimos el flujo de una entrada (4), revisamos que el archivo no exista en el directorio destino (5) y abrimos el flujo del archivo destino, luego copiamos el flujo de la entrada del zip hacia el flujo del archivo abierto (6), para por último, cerrar flujos locales (7) y globales (8).

Para los huevas, pueden hacerlo más fácil, si no programan para .NET en Windows Store Apps.

ZipFile.ExtractToFile(@"C:\reportes.zip", @"C:\reportes\");

¡Jo! Bueno, siempre es mejor saber cómo se hacen las cosas por detrás. Sin albur.

Agregar y eliminar archivos a un zip

Y ya como último ejemplo, y creo que cubrimos todos los escenarios más importantes, está éste, el cual muestra cómo añadir un archivo a algún zip existente a la vez que eliminamos una entrada al mismo zip. Aquí la clave está en abrir el ZipArchive en modo ZipArchiveMode.Update en lugar de Read o Create. Lo demás es igual a lo que ya hemos visto.

 

using System.IO; 
using System.IO.Compression; 
… 

// 1 
FileInfo zipFile = new FileInfo(@"C:\reportes.zip"); 
FileStream zipStream = zipFile.Open(FileMode.Open); 
ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Update); 
// 2 
ZipArchiveEntry oldEntry = archive.GetEntry("reporte1.xlsx"); 
oldEntry.Delete(); 
// 3 
FileInfo sourceFile = new FileInfo(@"C:\reporte2.xlsx"); 
FileStream sourceStream = sourceFile.OpenRead(); 
ZipArchiveEntry newEntry = archive.CreateEntry(sourceFile.Name); 
Stream stream = newEntry.Open(); 
sourceStream.CopyTo(stream); 
stream.Close(); 
sourceStream.Close(); 
// 4 
archive.Dispose(); 
zipStream.Close();

  

 

Mucho más compacto porque ya no necesitamos re-explicar miles de cosas, vamos por pasos. En 1 abrimos el archivo y el flujo del mismo. Nota que ahora no usamos OpenRead u OpenWrite, pues necesitamos que el flujo sea tanto de lectura como de escritura, así que usamos Open con FileMode.Open. Luego asociamos el flujo a un zip, pero ojo, aquí pasamos como parámetro ZipArchiveMode.Update como segundo parámetro.

Luego, en 2 utilizamos GetEntry para obtener el archivo reporte1.xlsx, que debe encontrarse en el zip. Esto nos regresa un ZipArchiveEntry. Para eliminarlo, sólo tenemos que invocar su método Delete. Easy peasy esta parte.

Luego continuamos con 3, lo cual es lo mismo que hemos visto: abrir el archivo que queremos añadir al zip (en este caso, el ingeniosamente llamado reporte2.xlsx), abrir el flujo, crear la entrada en el zip, copiar bytes de la fuente a la entrada, y cerrar. Esto no varía en nada, todo el trabajo lo hace ya ZipArchive cuando le dijimos que se abriera en modo Update.

Por último, cerramos con 4 cerrando los objetos que hemos creado, haciendo Close y Dispose. And be done with it.

Conclusiones

Es bueno que Microsoft haya añadido estas clases a .NET. Me parece que hace mucho tiempo que eran necesarias. Hasta ahora uno tenía que utilizar librerías de terceros. Y no es que sean malas. SharpZipLib, de iC# Code, es muy buena, por ejemplo. Sin embargo tienen el estigma "no Microsoft", y eso hace que algunas empresas le pongan peros al momento de aceptar desarrollos que la utilicen. No todas, pero sí las hay. Además, de esta forma se estandariza la forma de hacerlo.

Quizás lo que me llama la atención es que no haya mucha publicidad sobre este tema importante. Es decir, el Task Parallel Library recibió mucha atención cuando salió .NET 4, y la integración de C# con el TPL (i.e. las palabras reservadas async y await) ahora que salió el .NET 4.5. Y estas clases se me hacen muy importantes también. Ojalá sólo sea falta de información de mi parte.

Pero bueno, que ahora ya las conoces y no hay pretexto para comenzar a utilizar estas clases y comenzar a migrar tu código legado… ¿Comentarios, preguntas, dudas?

Categorías:.NET Framework, Apunte, C# Etiquetas: , ,
  1. Claudio
    enero 29, 2014 a las 5:30 am

    Muy bueno tu artículo. Estoy necesitando comprimir archivos con framework 1.1, sabes si hay alguna forma?? Muchas gracias. Saludos Claudio

  2. Antonio
    noviembre 25, 2014 a las 5:46 pm

    Fantástico. La mejor información con diferencia que he encontrado en la red: en cinco minutos he asimilado lo que llevaba días intentando comprender. Gracias. Una pregunta: ¿hay posibilidad de añadir una clave al fichero zip generado?

    Gracias de nuevo. Un saludo. Antonio.

  1. noviembre 27, 2012 a las 7:16 pm

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s