Creación de Archivos TAR en Go con Control de Inclusión/Exclusión

Este artículo describe cómo generar archivos TAR utilizando el paquete estándar archive/tar de Go. La implementación permite empaquetar múltiples archivos y directorios, ofreciendo flexibilidad mediante listas blancas y negras tanto para archivos como para directorios. El resultado puede ser un archivo TAR escrito en disco o un objeto io.Reader.

Limitación Importante: Al igual que el formato TAR nativo, esta solución no soporta nombres de archivo o rutas que contengan caracteres chinos (u otros caracteres no ASCII). Su inclusión resultará en datos corruptos (garbled data) o fallos al empaquetar. Se recomienda encarecidamente utilizar únicamente nombres de archivo y rutas que consistan en caracteres ASCII básicos.

Función de Empaquetado

La sigueinte función BuildTarArchive recorre recursivamente un directorio especificado y añade sus contenidos a un escritor TAR. Admite listas blancas y negras para filtrar qué archivos y directorios se incluyen en el archivo final.


package main

import (
	"archive/tar"
	"bytes"
	"errors"
	"io"
	"os"
	"path/filepath"
	"strings"
)

// BuildTarArchive recorre una ruta dada y la empaqueta en un archivo TAR.
// tarWriter: El destino donde se escribirá el archivo TAR.
// sourcePath: La ruta del directorio o archivo a empaquetar.
// relativePaths: Si es true, los nombres de archivo en el TAR serán relativos a sourcePath.
// fileWhitelist: Lista de nombres de archivo específicos a incluir. Si está vacía, se consideran todos.
// dirWhitelist: Lista de nombres de directorio específicos a incluir. Si está vacía, se consideran todos.
// fileBlacklist: Lista de nombres de archivo específicos a excluir.
// dirBlacklist: Lista de nombres de directorio específicos a excluir.
func BuildTarArchive(
	tarWriter *tar.Writer,
	sourcePath string,
	relativePaths bool,
	fileWhitelist,
	dirWhitelist,
	fileBlacklist,
	dirBlacklist []string,
) error {
	// Normalizar las listas para comparaciones consistentes
	normalizePaths := func(paths []string) []string {
		normalized := make([]string, len(paths))
		for i, p := range paths {
			normalized[i] = filepath.Clean(p)
		}
		return normalized
	}

	cleanFileWhitelist := normalizePaths(fileWhitelist)
	cleanDirWhitelist := normalizePaths(dirWhitelist)
	cleanFileBlacklist := normalizePaths(fileBlacklist)
	cleanDirBlacklist := normalizePaths(dirBlacklist)

	// Validar conflictos entre listas blancas y negras
	if conflicts := checkListConflicts(cleanFileWhitelist, cleanFileBlacklist, cleanDirWhitelist, cleanDirBlacklist); len(conflicts) > 0 {
		return errors.New("conflictos en listas blancas/negras: " + strings.Join(conflicts, ", "))
	}

	return filepath.Walk(sourcePath, func(currentPath string, info os.FileInfo, err error) error {
		if err != nil {
			return err // Propagar errores de acceso al sistema de archivos
		}

		// Ignorar el directorio raíz si no se quieren rutas absolutas
		if relativePaths && filepath.Clean(currentPath) == filepath.Clean(sourcePath) {
			return nil
		}

		// Determinar el nombre del archivo dentro del archivo TAR
		tarHeaderName := filepath.Base(currentPath)
		if relativePaths {
			relPath, err := filepath.Rel(sourcePath, currentPath)
			if err != nil {
				return err
			}
			tarHeaderName = filepath.ToSlash(relPath) // Usar barras inclinadas para compatibilidad
		}

		// Aplicar reglas de inclusión/exclusión
		if !shouldInclude(info, tarHeaderName, cleanFileWhitelist, cleanDirWhitelist, cleanFileBlacklist, cleanDirBlacklist) {
			if info.IsDir() {
				// Si es un directorio y debe ser excluido, evitar recorrer su contenido
				return filepath.SkipDir
			}
			return nil // Ignorar este archivo/directorio
		}

		// Preparar la cabecera del archivo TAR
		header, err := tar.FileInfoHeader(info, "")
		if err != nil {
			return err
		}

		header.Name = tarHeaderName
		if info.IsDir() {
			// Asegurarse de que los directorios terminen con una barra
			header.Name = filepath.ToSlash(header.Name) + "/"
		}
		header.Format = tar.FormatGNU // Usar formato GNU para mayor compatibilidad

		// Escribir la cabecera en el archivo TAR
		if err := tarWriter.WriteHeader(header); err != nil {
			return err
		}

		// Si es un archivo regular, copiar su contenido
		if !info.Mode().IsRegular() {
			return nil
		}

		file, err := os.Open(currentPath)
		if err != nil {
			return err
		}
		defer file.Close() // Asegurarse de cerrar el archivo

		_, err = io.Copy(tarWriter, file)
		return err
	})
}

// checkListConflicts verifica si hay superposiciones o contradicciones entre las listas.
func checkListConflicts(fw, fl, dw, dl []string) []string {
	var conflicts []string
	// Simplificado: Aquí se verificarían colisiones más complejas si fuera necesario.
	// Por ejemplo, un archivo en la lista blanca que también está en la negra.
	for _, fName := range fw {
		for _, fNameBlack := range fl {
			if fName == fNameBlack {
				conflicts = append(conflicts, "archivo blanco/negro conflictivo: "+fName)
			}
		}
	}
	for _, dName := range dw {
		for _, dNameBlack := range dl {
			if dName == dNameBlack {
				conflicts = append(conflicts, "directorio blanco/negro conflictivo: "+dName)
			}
		}
	}
	return conflicts
}

// shouldInclude determina si un archivo o directorio debe ser incluido basado en las listas.
func shouldInclude(info os.FileInfo, name string, fw, dw, fl, dl []string) bool {
	isDir := info.IsDir()
	nameClean := filepath.Clean(name)

	// Verificar listas negras primero
	if isDir {
		for _, excludedDir := range dl {
			if strings.HasPrefix(nameClean, excludedDir) {
				return false // Excluido por lista negra de directorios
			}
		}
	} else {
		for _, excludedFile := range fl {
			if nameClean == excludedFile {
				return false // Excluido por lista negra de archivos
			}
		}
	}

	// Si hay listas blancas, aplicar reglas
	if len(fw) > 0 || len(dw) > 0 {
		included := false
		if isDir {
			// Directorio: debe estar en la lista blanca de directorios o ser subdirectorio de uno
			for _, allowedDir := range dw {
				if nameClean == allowedDir || strings.HasPrefix(nameClean, allowedDir+"/") {
					included = true
					break
				}
			}
		} else {
			// Archivo: debe estar en la lista blanca de archivos o en un directorio permitido
			for _, allowedFile := range fw {
				if nameClean == allowedFile {
					included = true
					break
				}
			}
			if !included && len(dw) > 0 {
				for _, allowedDir := range dw {
					if strings.HasPrefix(nameClean, allowedDir+"/") {
						included = true
						break
					}
				}
			}
		}
		if !included {
			return false // No incluido por lista blanca
		}
	}

	// Si no hay listas blancas o el elemento pasó las verificaciones, incluirlo
	return true
}


Ejemplo de Uso: Empaquetar en un Archivo

Este ejemplo muestra cómo crear un archivo TAR llamado output.tar en el disco, incluyendo solo archivos específicos y directorios de la ruta actual (.).


package main

import (
	"archive/tar"
	"io/ioutil" // ioutil se usa aquí para simplificar la creación de archivos, aunque os.Create es preferible para control
	"os"
	"testing"
)

func TestCreateTarFile(t *testing.T) {
	tarFileName := "output.tar"

	// Crear el archivo de salida
	outputFile, err := os.Create(tarFileName)
	if err != nil {
		t.Fatalf("Error al crear el archivo de salida %s: %v", tarFileName, err)
	}
	defer outputFile.Close()

	// Crear un nuevo escritor TAR
	tarWriter := tar.NewWriter(outputFile)
	defer tarWriter.Close() // Asegurarse de cerrar el escritor TAR

	// Definir listas de inclusión/exclusión
	fileWhitelist := []string{
		"main.go",
		"config.yaml",
	}
	dirWhitelist := []string{
		"static",
		"templates",
	}

	// Empaquetar el contenido del directorio actual
	err = BuildTarArchive(
		tarWriter,
		".",
		true, // Usar rutas relativas
		fileWhitelist,
		dirWhitelist,
		nil, // Sin lista negra de archivos
		nil, // Sin lista negra de directorios
	)
	if err != nil {
		t.Fatalf("Error al empaquetar el archivo: %v", err)
	}

	t.Logf("Archivo TAR '%s' creado exitosamente.", tarFileName)

	// Opcional: verificar el contenido del archivo creado (requiere lógica adicional)
}


Ejemplo de Uso: Empaquetar en un Buffer (io.Reader)

En lugar de escribir directamente a un archivo, los datos del TAR pueden generarse en un bytes.Buffer, que implementa io.Reader. Esto es útil para enviar los datos a través de una red o procesarlos en memoria.


package main

import (
	"archive/tar"
	"bytes"
	"os"
	"testing"
)

func TestCreateTarInMemory(t *testing.T) {
	// Usar un buffer de bytes como destino para el archivo TAR
	var buffer bytes.Buffer
	tarWriter := tar.NewWriter(&buffer)

	// Definir listas de inclusión/exclusión
	fileWhitelist := []string{
		"main.go",
		"README.md",
	}
	dirWhitelist := []string{
		"assets",
	}

	// Empaquetar el contenido del directorio actual en el buffer
	err := BuildTarArchive(
		tarWriter,
		".",
		false, // Usar rutas absolutas o relativas según la configuración interna
		fileWhitelist,
		dirWhitelist,
		nil,
		nil,
	)
	if err != nil {
		t.Fatalf("Error al empaquetar en memoria: %v", err)
	}
	// Asegurarse de cerrar el escritor TAR para finalizar la escritura
	if err := tarWriter.Close(); err != nil {
		t.Fatalf("Error al cerrar el escritor TAR: %v", err)
	}

	// Ahora 'buffer.Bytes()' contiene los datos del archivo TAR
	// y 'bytes.NewReader(&buffer)' es un io.Reader
	tarReader := bytes.NewReader(buffer.Bytes())

	t.Logf("Archivo TAR generado en memoria. Tamaño: %d bytes.", buffer.Len())

	// Aquí podrías leer 'tarReader' para procesar el archivo TAR, enviarlo, etc.
	// Ejemplo: leer el primer encabezado
	tr := tar.NewReader(tarReader)
	header, err := tr.Next()
	if err != nil {
		t.Errorf("Error al leer del TAR en memoria: %v", err)
	} else {
		t.Logf("Primer archivo en el TAR en memoria: %s", header.Name)
	}
}


Etiquetas: Go archive/tar empaquetado io.Reader lista blanca

Publicado el 9-17 05:56