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)
}
}