h1

Como usar el Java API para generar archivos tar.gz

09/10/2007

Wizdoc [Icon By Buuf]  Tips & Tricks.

Un poco de contexto: comúnmente al procesar información que implique generación de archivos éstos tienen que ser almacenados en algún servidor. Lo más probable, para cualquier aplicación de este tipo es que:

1. Los archivos se almacenen a manera de respaldo en algún directorio.
2. Sea necesario comprimir estos archivos para conservar espacio en disco o consolidar la información de los mismos.
3. El servidor donde se almacenan estos archivos es un Unix/Linux.

Por lo tanto, no es del todo usable generar archivos .zip mediante el API de Java (paquete java.util.zip) pues al estar almacenados en un servidor Unix/Linux se generan problemas de logística, conversión y a veces incluso de corrupción de datos.

Así entonces, es necesario generar archivos tar.gz para compresión y archivado, pero la especificación GZIP, implementada en la clase java.util.zip.GZipOutputStream, sólo comprime (es decir, sólo genera un archivo de salida por cada archivo de entrada) mientras que la especificación TAR sólo archiva (o dicho de otra manera, sólo puede generar un archivo de salida a partir de varios de entrada, pero no comprime).

Java Tar

Por ahí de 1990, Tim Endres fundó la empresa ICE Engineering, y entre otras librerías que creó en parte por hobby y en parte para facilitar su trabajo, incluye el paquete com.ice.tar que proporciona la implementación de tar.gz que necesitamos:

com.ice.tar package

Paquete com.ice.tar visto desde el Eclipse.

En general la librería es buena, aunque tiene sus detalles:

• Es necesario asignarle un tamaño al TarEntry para que el búfer interno pueda ser configurado.

• Es necesario cerrar explícitamente el TarEntry para que el búfer genere un flush() y escriba en el OutputStream. De lo contrario, al abrir el archivo (mediante #gunzip -f archivo.tar.gz) nos marcará un error de “Final inesperado de archivo; pasando a siguiente cabecera“.


• En estricta teoría se supone que el TarGzOutputStream ya hace uso de un BufferedOutputStream de manera interna (de ahí los dos puntos anteriores), pero tengo mis sospechas de que esto no es cierto por lo que si se requiere desempeño, tal vez sea necesario agregar los filtros del paquete java.nio. Por el momento dejémoslo así.

Tomando en cuenta estos detalles, podemos hacer un código que lea varios archivos y los archive y comprima en un tar.gz:

  public void tarGz(final List filePaths, final String zipPath) {

    FileInputStream inputStream = null;
    BufferedInputStream bufferedInputStream = null;

    FileOutputStream outputStream = null;
    TarGzOutputStream tarOutputStream = null;

    try {
      outputStream = new FileOutputStream(zipPath.toString());
      tarOutputStream = new TarGzOutputStream(outputStream);

      byte data[] = new byte[1024];
      int dataSize = 0;

      for (Iterator t = filePaths.iterator(); t.hasNext();) {
        String cdrPath = “” + t.next();
        String cdrName = CDRUtils.getFileNameFromPath(cdrPath);

        TarEntry entry = new TarEntry(cdrName);
        entry.setSize(new File(cdrPath).length()); //Esto es a wilson
        entry.setName(cdrName);
        tarOutputStream.putNextEntry(entry);

        inputStream = new FileInputStream(cdrPath);
        bufferedInputStream = new BufferedInputStream(inputStream, data.length);

        while ((dataSize = bufferedInputStream.read(data, 0, data.length)) != -1) {
          tarOutputStream.write(data, 0, dataSize);
        }

        tarOutputStream.closeEntry(); //Esto es a wilson

        if (bufferedInputStream != null) {
          bufferedInputStream.close();
        }

        if (inputStream != null) {
          inputStream.close();
        }
      }
    } catch (final Exception e) {
      logger.error(“Error IO”, e);
    } finally {
      try {
        if (bufferedInputStream != null) {
          bufferedInputStream.close();
        }

        if (inputStream != null) {
          inputStream.close();
        }

        if (tarOutputStream != null) {
          tarOutputStream.close();
        }

        if (outputStream != null) {
          outputStream.close();
        }
      } catch (final IOException ioe) {
        logger.error(“Resource release error”, ioe);
      }
    }
  }

Actualización: 23/06/2011

Si lo que buscamos es la operación inversa (extraer los archivos de un tar.gz) es bastante más sencillo programarlo en Java, sin embargo es necesario considerar un pequeño detalle: un archivo tar.gz consiste de varios documentos o carpetas archivados dentro de un .tar que posteriormente es sometido a una operación de compresión mediante gzip. Es decir, primero hay que descomprimir y posteriormente “desarchivar”, como se hace normalmente con los comandos de Unix/Linux: gunzip archivo.tar.gz \n tar -xvf archivo.tar


public static void unTarGz(final String zipPath, final String unZipPath) throws Exception {

  //java.util.zip.GZIPInputStream forma parte del API estandar de Java
  GZIPInputStream gzipInputStream = new GZIPInputStream(new FileInputStream(zipPath));

  //Path y nombre del archivo temporal
  String tempDir = unZipPath.substring(0, unZipPath.lastIndexOf(‘/’));
  String tempFile = “” + new Date().getTime() + “.tmp”;
  String tempPath = tempDir + “/” + tempFile;

  //Primero se crea un temporal que contiene el .tar “gunzipeado”…
  OutputStream out = new FileOutputStream(tempPath);

  byte[] data = new byte[1024];
  int len;
  while ((len = gzipInputStream.read(data)) > 0) {
    out.write(data, 0, len);
  }

  gzipInputStream.close();
  out.close();

  //…Y luego mediante el paquete com.ice.tar extraemos la informacion del .tar temporal
  TarArchive tarArchive = new TarArchive(new FileInputStream(tempPath));
  tarArchive.extractContents(new File(unZipPath));
  tarArchive.closeArchive();

  //Borrado del archivo remporal (.tar)
  new File(tempPath).delete();
}

4 comentarios

  1. hola. lo primero d todo felicidads x la explicación de cómo se debe realizar un tar.gz. m ha sido muy util porque tengo que hacer algo parecido. pero aunque sé que escribiste ste post hace tiempo, no sé si me podrías solucionar una duda: por lo que veo, va leyendo los ficheros que le metes en la lista de paths y va generando nuevas entries en el tar.gz que contienen una copia del original pero… si lo que se quiere introducir en el tar.gz es una jerarquia de directorios, cada uno con sus ficheros, cómo se implementaría?? xq al intentar leer el directorio como se lee el fichero: bufferedInputStream.read(data, 0, data.length)) da error. habría que establecer algún tipo de control de niveles dentro de la jerarquia??gracias y un saludo.


  2. De hecho se utilizaria prácticamente el mismo código, con un par de modificaciones. Primero, es necesario cambiar la función CDRUtils.getFileNameFromPath(cdrPath) a la siguiente implementación:

    public static String getFileNameFromPath(String cdrPath) {
    String result = "./";if(cdrPath != null && cdrPath.length() > 0) {
    result = cdrPath.substring(cdrPath.indexOf("/") + 1);}return result;}

    Éste método parsearía los paths de los archivos que quieres agregar al .tar.gz incluyendo los directorios en los que se encuentran. Con esta pequeña modificación, y usando el siguiente input:

    List fileList = new ArrayList();
    fileList.add("C:/archivo1.txt");
    fileList.add("C:/Temp/archivo2.txt");
    fileList.add("C:/Temp/SubTemp/archivo3.txt");
    TarGz.tarGz(fileList, "C:/myZip.tar.gz")

    Obtendrás un .tar.gz con tres archivos comprimidos que conservan su estructura de directorios. Lo que NO puedes hacer con éste código, es crear directorios vacíos; sin embargo se pueden tener "directorios vacíos" en el .tar.gz siempre y cuando contengan algún subdirectorio o sub-subdirectorio con un archivo (algo así como /dir/subdir/subsubdir/arch.txt donde dir y subdir no contienen archivos).

    Ahora, para poder agregar directorios vacíos, se agregan tres líneas únicamente:…

    TarEntry entry = new TarEntry(cdrName);
    File file = new File(cdrPath); //MODIFICACION 1
    entry.setSize(file.length()); //Esto es a wilson
    entry.setName(cdrName);
    tarOutputStream.putNextEntry(entry);
    if(!file.isDirectory()) { //MODIFICACION 2
    inputStream = new FileInputStream(cdrPath);
    bufferedInputStream = new BufferedInputStream(inputStream, data.length);
    while ((dataSize = bufferedInputStream.read(data, 0, data.length)) != -1) {
    tarOutputStream.write(data, 0, dataSize);}}//MODIFICACION 3…

    Con esto puedes crear directorios vacíos en tu tar.gz sin problemas. Con el siguiente input:

    fileList.add("C:/Temp/SubTemp/");
    fileList.add("C:/Temp/archivo2.txt");

    Obtendrás un .tar.gz con un archivo contenido en su directorio y un subdirectorio llamada SubTemp que está vacío.


  3. Te agradezco mucho el aporte. Entretenido tu blog, anduve paseando por otros post tuyos como el de “la evolución del programador”.. genial.. es universal! jajaja.
    Respecto de este post, te cuento a modo comentario que una alternativa al metodo CDRUtils.getFileNameFromPath, es utilizarlo de esta otra forma:

    String cdrPath = “” + t.next();
    File fileTemp = new File(cdrPath); //nueva linea
    //String cdrName = CDRUtils.getFileNameFromPath(cdrPath);
    String cdrName = fileTemp.getName(); //otra forma

    donde lo tienes mas directo.
    Y una alternativa a la consulta de Alvaro es cambiar lo siguiente:

    String cdrPath = “” + t.next();
    File fileTemp = new File(cdrPath);
    //String cdrName = CDRUtils.getFileNameFromPath(cdrPath);
    //String cdrName = fileTemp.getName();
    String cdrName = cdrPath;

    TarEntry entry = new TarEntry(cdrName);
    //entry.setSize(new File(cdrPath).length()); //Esto es a wilson
    entry.setSize(fileTemp.length());

    Ahora una consulta, ¿cuál es la forma de extraer estos archivos a traves de java?, análogamente al proceso de compresión pero necesito la descompresión.

    Agradezco tu tiempo y tus aportes.

    Saludos desde Chile.


    • Listo; he actualizado el post para incluir el código necesario para la extracción de los archivos desde un .tar.gz



Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: