El paquete archive/zip viene con Go. Comprimir y extraer son dos bucles cortos, pero hay un detalle de seguridad al extraer que conviene no saltarse, porque es una vulnerabilidad con nombre propio.
También te puede interesar
Comprimir
func comprimir(destino string, rutas []string) error {
f, err := os.Create(destino)
if err != nil {
return err
}
defer f.Close()
z := zip.NewWriter(f)
defer z.Close()
for _, r := range rutas {
origen, err := os.Open(r)
if err != nil {
return fmt.Errorf("abriendo %s: %w", r, err)
}
w, err := z.Create(filepath.Base(r))
if err != nil {
origen.Close()
return err
}
if _, err := io.Copy(w, origen); err != nil {
origen.Close()
return err
}
origen.Close()
}
return nil
}
salidaoriginales: 1212 bytes el zip : 307 bytes
Close del escritor zip no es opcional. Es el que escribe el índice del archivo, al final. Sin él sale un zip corrupto aunque el contenido esté entero. Por eso va con defer nada más crearlo. Y fíjate en que los origen.Close() van a mano dentro del bucle, no con defer: con defer se acumularían todos abiertos hasta el final de la función.filepath.Base(r) guarda solo el nombre, sin la ruta. Si quieres conservar la estructura de carpetas dentro del zip, se pasa la ruta relativa en su lugar.
Mirar dentro sin extraer
r, err := zip.OpenReader("copia.zip")
if err != nil {
return err
}
defer r.Close()
for _, f := range r.File {
fmt.Printf("%-12s %5d -> %4d bytes\n", f.Name, f.UncompressedSize64, f.CompressedSize64)
}
salidanotas.txt 1200 -> 47 bytes datos.csv 12 -> 18 bytes
El índice está al final del archivo, así que leer la lista no cuesta descomprimir nada. Y fíjate en la segunda línea: el CSV de 12 bytes ocupa 18 comprimido. Con archivos diminutos, la cabecera de cada entrada pesa más que lo que se ahorra.
Extraer
for _, f := range z.File {
destino := filepath.Join(carpeta, f.Name)
// protección contra rutas que se salen de la carpeta
if !strings.HasPrefix(destino, filepath.Clean(carpeta)+string(os.PathSeparator)) {
return fmt.Errorf("ruta peligrosa en el zip: %s", f.Name)
}
if f.FileInfo().IsDir() {
os.MkdirAll(destino, 0755)
continue
}
if err := os.MkdirAll(filepath.Dir(destino), 0755); err != nil {
return err
}
dentro, err := f.Open()
if err != nil {
return err
}
fuera, err := os.Create(destino)
if err != nil {
dentro.Close()
return err
}
_, err = io.Copy(fuera, dentro)
dentro.Close()
fuera.Close()
if err != nil {
return err
}
}
salidadatos.csv 12 bytes notas.txt 1200 bytes
../../etc/cron.d/algo, y si extraes sin comprobar, escribes fuera de la carpeta de destino. Como el nombre viene del archivo y no de ti, hay que validarlo siempre, incluso con zips «de confianza». Es el error de seguridad más común al trabajar con archivos comprimidos.Lo demás del bucle es rutina: crear las carpetas intermedias que haga falta, y copiar. Las entradas de carpeta se reconocen con f.FileInfo().IsDir().
Lo que conviene recordar
| Al… | No olvides |
|---|---|
| Comprimir | defer z.Close(): escribe el índice |
| Comprimir en bucle | Cerrar cada origen dentro del bucle, no con defer |
| Extraer | Validar f.Name antes de escribir |
| Extraer | Crear las carpetas intermedias con MkdirAll |
| Listar | No hace falta descomprimir: el índice está al final |
Para .tar.gz, que es lo habitual en Linux, los paquetes son archive/tar y compress/gzip, encadenados: el tar escribe sobre el gzip y el gzip sobre el archivo.
Todo el código se compiló y ejecutó con Go 1.25 antes de publicarla; los tamaños que aparecen son los reales de esa ejecución. Documentación oficial: paquete archive/zip.