Comprimir una carpeta en un ZIP, leer un archivo de dentro sin extraer nada o descomprimir lo que te envían: .NET lo trae todo en System.IO.Compression, sin paquetes. Y con una protección contra los ZIP maliciosos que conviene conocer.
También te puede interesar
Comprimir una carpeta
Todo está en System.IO.Compression, que viene con .NET. La forma más corta de crear un ZIP es comprimir una carpeta entera:
using System.IO.Compression;
// Una carpeta de ejemplo con tres archivos
Directory.CreateDirectory("informe/datos");
File.WriteAllText("informe/resumen.txt", "Ventas de octubre: 1,240 unidades.\n");
File.WriteAllText("informe/datos/ventas.csv", string.Concat(Enumerable.Repeat("2026-10-01,Café,3,4.50\n", 200)));
File.WriteAllText("informe/datos/notas.txt", "Revisar el stock de té.\n");
File.Delete("informe.zip");
ZipFile.CreateFromDirectory("informe", "informe.zip");
using var zip = ZipFile.OpenRead("informe.zip");
foreach (var entrada in zip.Entries.OrderBy(e => e.FullName))
{
Console.WriteLine($"{entrada.FullName,-20} {entrada.Length,6} bytes -> {entrada.CompressedLength,4} comprimido");
}salidadatos/notas.txt 25 bytes -> 27 comprimido datos/ventas.csv 4800 bytes -> 76 comprimido resumen.txt 35 bytes -> 37 comprimido
CreateFromDirectory recorre la carpeta con sus subcarpetas, y cada archivo queda como una entrada con su ruta relativa. Para mirar lo que hay dentro, ZipFile.OpenRead da un ZipArchive con su lista Entries: el tamaño original y el comprimido. El CSV, que repite la misma línea 200 veces, se queda en casi nada; los textos cortos, en cambio, ocupan incluso un poco más que el original, porque la compresión añade su propia información. Las entradas usan siempre / como separador, también en Windows.
Leer una entrada sin extraer nada
using System.IO.Compression;
// Una carpeta de ejemplo con tres archivos
Directory.CreateDirectory("informe/datos");
File.WriteAllText("informe/resumen.txt", "Ventas de octubre: 1,240 unidades.\n");
File.WriteAllText("informe/datos/ventas.csv", string.Concat(Enumerable.Repeat("2026-10-01,Café,3,4.50\n", 200)));
File.WriteAllText("informe/datos/notas.txt", "Revisar el stock de té.\n");
File.Delete("informe.zip");
ZipFile.CreateFromDirectory("informe", "informe.zip");
using var zip = ZipFile.OpenRead("informe.zip");
var resumen = zip.GetEntry("resumen.txt")!;
using var lector = new StreamReader(resumen.Open());
Console.Write(lector.ReadToEnd());
Console.WriteLine(zip.GetEntry("no-existe.txt") is null);salidaVentas de octubre: 1,240 unidades. True
GetEntry busca una entrada por su ruta (o devuelve null), y Open() la abre como un Stream que se descomprime al leerlo, sin crear archivos en el disco.
Crear un ZIP entrada a entrada
using System.IO.Compression;
File.Delete("copia.zip");
using (var zip = ZipFile.Open("copia.zip", ZipArchiveMode.Create))
{
var nota = zip.CreateEntry("leeme.txt", CompressionLevel.SmallestSize);
using var escritor = new StreamWriter(nota.Open());
escritor.WriteLine("Copia generada por el programa.");
}
using (var zip = ZipFile.Open("copia.zip", ZipArchiveMode.Update))
{
File.WriteAllText("config.json", "{ \"version\": 2 }");
zip.CreateEntryFromFile("config.json", "ajustes/config.json");
}
using var final = ZipFile.OpenRead("copia.zip");
Console.WriteLine(string.Join(", ", final.Entries.Select(e => e.FullName)));salidaleeme.txt, ajustes/config.json
Con ZipArchiveMode.Create se escribe un ZIP nuevo; con Update, se añade a uno que ya existe. CreateEntry crea una entrada vacía para escribir en ella lo que quieras, y CreateEntryFromFile mete un archivo del disco con el nombre que le digas, carpetas incluidas.
Extraer
using System.IO.Compression;
// Una carpeta de ejemplo con tres archivos
Directory.CreateDirectory("informe/datos");
File.WriteAllText("informe/resumen.txt", "Ventas de octubre: 1,240 unidades.\n");
File.WriteAllText("informe/datos/ventas.csv", string.Concat(Enumerable.Repeat("2026-10-01,Café,3,4.50\n", 200)));
File.WriteAllText("informe/datos/notas.txt", "Revisar el stock de té.\n");
File.Delete("informe.zip");
ZipFile.CreateFromDirectory("informe", "informe.zip");
if (Directory.Exists("destino")) Directory.Delete("destino", recursive: true);
ZipFile.ExtractToDirectory("informe.zip", "destino");
Console.WriteLine(string.Join(", ", Directory.GetFiles("destino", "*", SearchOption.AllDirectories).Order()));
try
{
ZipFile.ExtractToDirectory("informe.zip", "destino");
}
catch (IOException)
{
Console.WriteLine("La segunda vez falla: los archivos ya existen");
}
ZipFile.ExtractToDirectory("informe.zip", "destino", overwriteFiles: true);
Console.WriteLine("Con overwriteFiles: true, los sustituye");salidadestino/datos/notas.txt, destino/datos/ventas.csv, destino/resumen.txt La segunda vez falla: los archivos ya existen Con overwriteFiles: true, los sustituye
Por omisión, ExtractToDirectory se niega a sobrescribir: si un archivo ya existe lanza IOException, y para entonces puede haber extraído una parte. Si quieres sustituirlos, dilo con overwriteFiles: true.
Los ZIP que intentan salirse de la carpeta
Un ZIP puede traer entradas con rutas como ../fuera.txt. Un programa que las extrajera sin mirar escribiría fuera de la carpeta de destino: un fallo de seguridad conocido como Zip Slip, con el que un archivo descargado puede sobrescribir lo que quiera. Este programa fabrica uno así a propósito:
using System.IO.Compression;
File.Delete("raro.zip");
using (var zip = ZipFile.Open("raro.zip", ZipArchiveMode.Create))
{
using var w = new StreamWriter(zip.CreateEntry("../fuera.txt").Open());
w.Write("esto no debería salir de la carpeta");
}
try
{
ZipFile.ExtractToDirectory("raro.zip", "seguro");
}
catch (IOException ex)
{
Console.WriteLine(ex.Message);
}
Console.WriteLine($"¿Existe ../fuera.txt? {File.Exists("../fuera.txt")}");salidaExtracting Zip entry would have resulted in a file outside the specified destination directory. ¿Existe ../fuera.txt? False
Path.Combine(destino, entrada.FullName), esa comprobación desaparece. En ese caso, calcula la ruta completa con Path.GetFullPath y verifica que empieza por la carpeta de destino antes de escribir.Todo el código de esta página se compiló con el .NET SDK 10.0.403 (C# 14) y se ejecutó con .NET 10.0.14 en un contenedor limpio antes de publicarla; las salidas y los mensajes del compilador están copiados de esa ejecución.