Comprimir y Descomprimir Archivos con ZIP, GZIP y TAR
Crea y extrae archivos ZIP, GZIP y TAR en Python, Node.js, Java y Bash con lectura en streaming y patrones de extracción segura.
Visión General
Una tarea nocturna tiene que empaquetar tres semanas de logs de la aplicación en un solo archivo para un auditor, y la exportación crece a 2 GB sin comprimir. Cada runtime ya trae una biblioteca de archivado, pero los comportamientos por defecto difieren en cosas que importan: ZIP comprime cada entrada por separado, así que puedes extraer un archivo sin desempaquetar el resto; TAR solo concatena y necesita un compresor externo; y GZIP maneja exactamente un stream. Esta receta cubre los tres formatos en Python, Node.js, Java y Bash, incluyendo el código de streaming y extracción segura que un snippet básico de ZipFile deja fuera.
Cuándo Usar
Usa este recurso cuando:
- Empaquetas paquetes de logs, exportaciones de reportes o artefactos de build para descarga
- Comprimes respuestas HTTP o payloads para reducir ancho de banda (ver compresión con Gzip y Brotli para el ajuste específico de HTTP)
- Extraes archivos subidos por usuarios en aplicaciones web
- Rotas y archivas logs en disco (ver rotación de logs en bash)
Omite la compresión programática cuando:
- El payload ya está comprimido (JPEG, MP4,
.zip,.parquet): gastas CPU sin ganar nada - Los archivos son diminutos y el endpoint es sensible a latencia; por debajo de ~1 KB, gzip puede producir una salida mayor que la entrada
- Necesitas acceso aleatorio frecuente al contenido del archivo; eso pide una base de datos o un almacén de objetos, no un zip más grande
Requisitos
- Python 3.9+ (
zipfile,gzip,tarfile,shutilen la biblioteca estándar;is_relative_torequiere 3.9,compresslevelentarfilerequiere 3.12) - Node.js 18+ con
npm install archiver extract-zippara los ejemplos de ZIP (zlibviene incluido) - JDK 11+ (
java.util.zipes biblioteca estándar) pigzopcional, para gzip paralelo en la sección de Bashpyminizip/Zip4jsolo si necesitas ZIPs cifrados (ver el FAQ)
Código companion: el repo companion de compress-decompress-files tiene estos ejemplos como archivos ejecutables, incluidos los helpers de extracción segura.
Solución
Python
import zipfile
import gzip
import tarfile
# Archivo ZIP
with zipfile.ZipFile('archive.zip', 'w', zipfile.ZIP_DEFLATED) as z:
z.write('file.txt')
# Archivo GZIP
with open('file.txt', 'rb') as f_in:
with gzip.open('file.txt.gz', 'wb') as f_out:
f_out.writelines(f_in)
# Archivo TAR
with tarfile.open('archive.tar.gz', 'w:gz') as tar:
tar.add('data/')
JavaScript
const fs = require('fs');
const zlib = require('zlib');
const archiver = require('archiver');
// Comprimir GZIP
const input = fs.createReadStream('file.txt');
const output = fs.createWriteStream('file.txt.gz');
input.pipe(zlib.createGzip()).pipe(output);
// Archivo ZIP
const archive = archiver('zip', { zlib: { level: 9 } });
archive.pipe(fs.createWriteStream('archive.zip'));
archive.file('file.txt', { name: 'file.txt' });
archive.finalize();
Java
import java.io.*;
import java.util.zip.*;
public class Compressor {
// Comprimir GZIP
public void gzip(String src, String dest) throws IOException {
try (FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dest);
GZIPOutputStream gzos = new GZIPOutputStream(fos)) {
fis.transferTo(gzos);
}
}
// Archivo ZIP
public void zip(String src, String dest) throws IOException {
try (FileOutputStream fos = new FileOutputStream(dest);
ZipOutputStream zos = new ZipOutputStream(fos);
FileInputStream fis = new FileInputStream(src)) {
zos.putNextEntry(new ZipEntry(new File(src).getName()));
fis.transferTo(zos);
zos.closeEntry();
}
}
}
Explicación
Los tres formatos resuelven problemas distintos. ZIP comprime cada entrada de forma independiente y guarda un directorio central al final del archivo; por eso puedes leer un miembro sin descomprimir el archivo entero — y también por eso un ZIP truncado puede perder el índice aunque los datos estén intactos. GZIP envuelve un único stream en compresión DEFLATE más un checksum CRC32; no sabe de nombres de archivo ni directorios. TAR concatena archivos con sus metadatos y no aplica compresión alguna, por lo que .tar siempre va emparejado con .gz, .bz2 o .xz en sistemas Unix.
Los tres usan DEFLATE por defecto (LZ77 + codificación Huffman), rápido y eficaz con texto (típicamente 60–80% de reducción en logs y código fuente). Sobre datos ya comprimidos casi no hace nada, y el algoritmo es anterior a los procesadores multinúcleo: para trabajo en masa, pigz paraleliza gzip manteniendo el formato, y Zstandard (zstd) da mejores ratios a velocidad comparable pero produce un formato distinto.
Elegir formato es sobre todo una cuestión de quién consume el archivo, no de qué lo comprime:
La extracción es la parte peligrosa
Leer archivos comprimidos es más arriesgado que escribirlos. Un ZIP malicioso puede llevar rutas como ../../etc/cron.d/evil que escapan del directorio de destino al extraer — es el ataque zip-slip. Además, un chequeo ingenuo de dest / entrada con prefijos de texto es explotable: un miembro que resuelve a /tmp/out-evil/x pasa un test de startswith("/tmp/out"). Compara siempre rutas resueltas componente a componente (Path.is_relative_to en Python, Path.startsWith sobre objetos Path en Java, path.relative en Node), y limita el tamaño total descomprimido para parar zip bombs. Para un recorrido más profundo, ver extracción segura de ZIP en Python.
Uso Avanzado
Python: compresión en streaming y extracción segura
import zipfile
import gzip
import tarfile
from pathlib import Path
def compress_directory_streaming(src_dir: str, dest_zip: str,
level: int = 6) -> int:
"""Comprime un directorio a ZIP entrada por entrada. Retorna el conteo."""
src_path = Path(src_dir)
file_count = 0
with zipfile.ZipFile(dest_zip, 'w', zipfile.ZIP_DEFLATED,
compresslevel=level) as zf:
for file_path in sorted(src_path.rglob('*')):
if file_path.is_file():
zf.write(file_path, file_path.relative_to(src_path))
file_count += 1
return file_count
def decompress_zip_safe(zip_path: str, dest_dir: str,
max_size: int = 1 << 30) -> int:
"""Extrae un ZIP con protección contra zip-slip y zip bombs."""
dest_path = Path(dest_dir).resolve()
dest_path.mkdir(parents=True, exist_ok=True)
total = 0
count = 0
with zipfile.ZipFile(zip_path, 'r') as zf:
for info in zf.infolist():
member = info.filename
member_path = (dest_path / member).resolve()
# is_relative_to compara componentes de ruta, no prefijos de
# texto, así que "/tmp/out-evil/x" no pasa un chequeo de "/tmp/out"
if not member_path.is_relative_to(dest_path):
raise ValueError(f"Ruta insegura detectada: {member}")
total += info.file_size
if total > max_size:
raise ValueError(f"El archivo excede {max_size} bytes")
zf.extract(member, dest_path)
count += 1
return count
def gzip_file_streaming(src: str, dest: str, level: int = 6) -> None:
"""Comprime un archivo con GZIP en bloques de 64 KB."""
with open(src, 'rb') as f_in, gzip.open(dest, 'wb',
compresslevel=level) as f_out:
while chunk := f_in.read(65536):
f_out.write(chunk)
def tar_directory(src_dir: str, dest: str, mode: str = 'w:gz') -> None:
"""Crea un archivo TAR.GZ de un directorio."""
with tarfile.open(dest, mode) as tar:
tar.add(src_dir, arcname=Path(src_dir).name, recursive=True)
def list_archive_contents(archive_path: str) -> list[str]:
"""Lista el contenido de un archivo ZIP o TAR."""
if archive_path.endswith('.zip'):
with zipfile.ZipFile(archive_path, 'r') as zf:
return zf.namelist()
elif archive_path.endswith(('.tar.gz', '.tgz', '.tar')):
with tarfile.open(archive_path, 'r:*') as tar:
return tar.getnames()
raise ValueError(f"Formato de archivo no soportado: {archive_path}")
# Uso
# count = compress_directory_streaming('logs/', 'logs.zip', level=6)
# print(f"Comprimidos {count} archivos")
# extracted = decompress_zip_safe('upload.zip', 'extracted/')
# print(f"Extraídos {extracted} archivos de forma segura")
Node.js: pipelines en streaming con zlib
const fs = require('fs');
const zlib = require('zlib');
const { pipeline } = require('stream');
const { promisify } = require('util');
const pipe = promisify(pipeline);
async function gzipFile(srcPath, destPath, level = 6) {
const src = fs.createReadStream(srcPath);
const gzip = zlib.createGzip({ level });
const dest = fs.createWriteStream(destPath);
await pipe(src, gzip, dest); // rechaza si cualquier etapa falla
}
async function gunzipFile(srcPath, destPath) {
const src = fs.createReadStream(srcPath);
const gunzip = zlib.createGunzip();
const dest = fs.createWriteStream(destPath);
await pipe(src, gunzip, dest);
}
async function gzipDirectory(srcDir, destZip) {
const archiver = require('archiver');
const output = fs.createWriteStream(destZip);
const archive = archiver('zip', { zlib: { level: 6 } });
const done = new Promise((resolve, reject) => {
output.on('close', () => resolve(archive.pointer()));
output.on('error', reject);
archive.on('error', reject);
});
archive.pipe(output);
archive.directory(srcDir, false);
archive.finalize();
return await done; // resuelve con los bytes escritos
}
async function extractZipSafe(zipPath, destDir) {
const extract = require('extract-zip');
const path = require('path');
const destRoot = path.resolve(destDir);
await extract(zipPath, {
dir: destRoot,
onEntry: (entry) => {
// path.relative devuelve "../..." para escapes; un chequeo de
// startsWith de texto dejaría pasar prefijos hermanos
const rel = path.relative(destRoot, path.resolve(destRoot, entry.fileName));
if (rel.startsWith('..') || path.isAbsolute(rel)) {
throw new Error(`Ruta insegura en archivo: ${entry.fileName}`);
}
},
});
}
// Uso
// gzipFile('large.log', 'large.log.gz', 9);
// const bytes = await gzipDirectory('logs/', 'logs.zip');
// console.log(`Tamaño del archivo: ${bytes} bytes`);
// await extractZipSafe('upload.zip', 'extracted/');
Java: compresión batch con try-with-resources
import java.io.*;
import java.nio.file.*;
import java.util.zip.*;
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Stream;
public class BatchCompressor {
// Comprime un archivo con GZIP
public static void gzipFile(Path src, Path dest, int bufferSize) throws IOException {
try (InputStream fis = Files.newInputStream(src);
OutputStream fos = Files.newOutputStream(dest);
GZIPOutputStream gzos = new GZIPOutputStream(fos, bufferSize)) {
fis.transferTo(gzos);
}
}
// Comprime varios archivos a ZIP con streaming
public static int zipFiles(List<Path> sources, Path destZip) throws IOException {
int count = 0;
try (OutputStream fos = Files.newOutputStream(destZip);
ZipOutputStream zos = new ZipOutputStream(fos)) {
for (Path src : sources) {
zos.putNextEntry(new ZipEntry(src.getFileName().toString()));
try (InputStream fis = Files.newInputStream(src)) {
fis.transferTo(zos);
}
zos.closeEntry();
count++;
}
}
return count;
}
// Extrae un ZIP con protección contra zip-slip
public static int extractZipSafe(Path zipPath, Path destDir) throws IOException {
Files.createDirectories(destDir);
int count = 0;
try (InputStream fis = Files.newInputStream(zipPath);
ZipInputStream zis = new ZipInputStream(fis)) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
// Path.startsWith compara componentes de nombre, así que un
// prefijo hermano como "out-evil" no pasa un chequeo de "out"
Path destFile = destDir.resolve(entry.getName()).normalize();
if (!destFile.startsWith(destDir)) {
throw new IOException("Entrada zip insegura: " + entry.getName());
}
if (entry.isDirectory()) {
Files.createDirectories(destFile);
} else {
Files.createDirectories(destFile.getParent());
Files.copy(zis, destFile, StandardCopyOption.REPLACE_EXISTING);
}
count++;
}
}
return count;
}
// Comprime todos los archivos de un directorio
public static int compressDirectory(Path srcDir, Path destZip) throws IOException {
List<Path> files = new ArrayList<>();
try (Stream<Path> stream = Files.walk(srcDir)) {
stream.filter(Files::isRegularFile).forEach(files::add);
}
return zipFiles(files, destZip);
}
}
// Uso
// BatchCompressor.gzipFile(Path.of("large.log"), Path.of("large.log.gz"), 8192);
// int count = BatchCompressor.compressDirectory(Path.of("logs/"), Path.of("logs.zip"));
// int extracted = BatchCompressor.extractZipSafe(Path.of("upload.zip"), Path.of("extracted/"));
Bash: tar/gzip con chequeos de seguridad y compresión paralela
#!/usr/bin/env bash
set -euo pipefail
# Comprime un directorio a tar.gz
compress_tarball() {
local src="$1"
local dest="$2"
local level="${3:-6}"
tar -c -C "$(dirname "$src")" "$(basename "$src")" \
| gzip -"$level" -c > "$dest"
echo "Creado $dest ($(du -h "$dest" | cut -f1))"
}
# Extrae un tarball de forma segura: rechaza rutas absolutas y escapes ../
extract_tarball_safe() {
local archive="$1"
local dest="$2"
mkdir -p "$dest"
if tar -tf "$archive" | grep -E '^/|\.\.'; then
echo "Error: rutas inseguras detectadas en $archive" >&2
return 1
fi
tar -xzf "$archive" -C "$dest"
echo "Extraído a $dest"
}
# gzip paralelo con pigz — más rápido en multinúcleo, mismo formato
compress_parallel() {
local src="$1"
local dest="$2"
if command -v pigz &>/dev/null; then
tar -c -C "$(dirname "$src")" "$(basename "$src")" | pigz -p 4 > "$dest"
else
tar -czf "$dest" -C "$(dirname "$src")" "$(basename "$src")"
fi
echo "Creado $dest"
}
# Comprime cada archivo de un directorio individualmente
batch_gzip() {
local dir="$1"
local count=0
for file in "$dir"/*; do
[[ -f "$file" ]] || continue
gzip -c "$file" > "${file}.gz"
((count++))
done
echo "Comprimidos $count archivos en $dir"
}
# Uso
# compress_tarball logs/ logs.tar.gz 6
# extract_tarball_safe archive.tar.gz extracted/
# compress_parallel data/ data.tar.gz
# batch_gzip /var/log/app/
Variantes
| Tecnología | Enfoque | Notas |
|---|---|---|
| Python | shutil.make_archive() | Crea ZIP/TAR en una sola llamada; sin streaming ni control por entrada |
| Node.js | adm-zip | Manipulación de ZIP en memoria; cómodo pero carga todo en el búfer |
| Java | Apache Commons Compress | Añade BZIP2, LZMA, 7Z y más formatos fuera de java.util.zip |
| Todos | zstd / 7z CLI | Mejores ratios que DEFLATE; son formatos distintos, no intercambiables |
Lo Que Funciona
- Procesa archivos en streaming en lugar de cargarlos en memoria:
transferTo,pipeliney los bloqueswith/trymantienen la memoria plana sin importar el tamaño de entrada - Usa el nivel de compresión 6 como valor predeterminado equilibrado; el nivel 9 cuesta bastante más CPU por ganancias de un dígito sobre texto
- Valida cada ruta extraída componente a componente y limita el tamaño total descomprimido; zip-slip y zip bombs son ataques del lado del archivo
- Prefiere GZIP para streams únicos y ZIP/TAR para paquetes; elige el formato según quién lo extrae, no según qué biblioteca tengas más cerca
- Lista el contenido de archivos subidos por usuarios antes de extraerlos (
tar -tf,zf.namelist())
Errores Comunes
- Cargar archivos enteros en memoria cuando todas las bibliotecas soportan streaming
- Comprobar rutas de extracción con
startswithde texto: un directorio hermano comodest-evillo elude - Olvidar
finalize()ocloseEntry(), produciendo archivos truncados o corruptos - Comprimir formatos ya comprimidos (JPEG, MP4, otros archivos): CPU desperdiciada y a veces una salida mayor
- Ignorar la codificación de nombres de archivo cuando los paquetes cruzan plataformas — ZIP es anterior a UTF-8, y los nombres con caracteres no ASCII se corrupten entre Windows y Linux
Lectura Adicional
- Documentación de
zipfileen Python:ZipFile, tipos de compresión y API de extracción - Documentación de
tarfileen Python: modos, filtros y el filtro de extraccióndata - Notas de OWASP sobre zip-slip y ataques de archivos: path traversal en extracción de archivos
Preguntas frecuentes
¿Qué formato debo usar?
GZIP para archivos únicos y payloads HTTP, ZIP para paquetes multi-archivo que se consumen en Windows, TAR.GZ para lo que sea en Unix. Ante la duda, fíjate en quién abre el archivo — ZIP tiene soporte nativo en el Explorador de Windows, mientras que .tar.gz no necesita herramientas extra en Linux.
¿Cómo manejo archivos muy grandes?
En streaming. Cada ejemplo anterior procesa una entrada cada vez, así que un archivo de 10 GB usa aproximadamente un búfer de memoria. Para trabajo masivo de escritura, pigz paraleliza gzip entre núcleos; para lectura intensiva, el directorio central de ZIP permite extraer un solo miembro directamente.
¿Es segura la compresión ZIP?
Compresión no es cifrado. ZipCrypto estándar es lo bastante débil para romperse en minutos; si necesitas confidencialidad, usa ZIPs cifrados con AES vía pyminizip (Python) o Zip4j (Java), o cifra externamente con GPG. Trata también la extracción como superficie de ataque: valida rutas y limita tamaños en todo lo que suba un usuario.
¿Qué nivel de compresión debo elegir?
El nivel 6 es el predeterminado correcto. Los niveles 1-3 sacrifican bastante ratio por velocidad; los niveles 7-9 gastan entre un 30 y un 50% más de CPU por unos pocos puntos extra de compresión sobre texto típico. Baja de 6 solo en rutas sensibles a latencia y sube a 9 solo para datos que se comprimen una vez y se descargan muchas.
¿Puedo parar una zip bomb?
Limita el tamaño total descomprimido durante la extracción, como hace decompress_zip_safe con max_size. Una zip bomb son unos pocos kilobytes de entrada que se expanden a gigabytes de salida; comprobar file_size por entrada antes de escribir la derrota.
Recursos Relacionados
Extraer Archivos Zip de Forma Segura con Python
Cómo extraer y validar archivos zip de forma segura usando zipfile y shutil en Python.
RecipeRotación y Compresión de Logs en Bash
Rota, comprime y purga logs de aplicaciones con configuraciones de logrotate y scripts bash personalizados —incluyendo zstd, copytruncate, chequeo de disco y envío a S3.
RecipeCómo Copiar y Mover Archivos con Python, JS, Java y Bash
Aprende a copiar y mover archivos multiplataforma con Python, JavaScript, Java y Bash. Incluye movimientos atómicos, checksums, symlinks y patrones batch.
RecipeValidación de Subida de Archivos
Cómo manejar subidas de archivos de forma segura con validación de tamaño, tipo y contenido.
RecipeSubida de Archivos Drag and Drop en JavaScript con HTML5
Implementa subida de archivos drag and drop nativa con HTML5 en JavaScript: zona de arrastre, validación, barra de progreso y vistas previas.
RecipeLeer Archivos Grandes
Cómo leer archivos grandes de forma eficiente sin agotar la memoria.