Trabajar con archivos zip en Java

En este artículo, cubro los aspectos básicos de la creación, la interacción, la inspección y la extracción de archivos zip con Java (OpenJDK 11 para ser específicos). El código...

Introducción

En este artículo, cubro los aspectos básicos de la creación, la interacción, la inspección y la extracción de archivos zip con Java (OpenJDK 11 para ser específicos). El ejemplo de código utilizado en este artículo tiene la forma de un proyecto Gradle y está alojado en este repositorio de GitHub para que lo ejecute y experimente. . Tenga cuidado al cambiar el código que elimina archivos.

Como ya se mencionó, los ejemplos de código aquí están escritos en Java 11 y utilizan la palabra clave var que se introdujo en Java 10 y los paradigmas de programación funcional en Java 8, por lo que se requiere una versión mínima de Java 10 para ejecutarlos tal cual.

Contenido

Clases clave de Java para trabajar con archivos comprimidos

Siento que es una buena idea comenzar identificando algunas de las clases prominentes que se usan comúnmente cuando se trata de archivos zip en Java. Estas clases viven en los paquetes java.util.zip o java.nio.file.

  • java.util.zip.ZipFile se usa para leer e interactuar con elementos (instancias de ZipEntry) en un archivo zip
  • java.util.zip.ZipEntry es una abstracción que representa un elemento como un archivo o directorio en un archivo zip (es decir, instancia ZipFile)
  • java.util.zip.ZipOutputStream es una implementación del resumen Salida de corriente y se utiliza para escribir elementos en un archivo Zip
  • java.nio.file.Archivos es una clase de utilidades muy útil para transmitir y copiar datos de archivos en instancias ZipOutputStream o fuera de instancias ZipFile
  • java.nio.file.Path otra clase de utilidades útiles para trabajar eficazmente con rutas de archivo

Rutas de archivos comunes para los ejemplos de código

Para el código de ejemplo, uso dos directorios comunes para escribir y leer datos hacia/desde, ambos relativos a la raíz del proyecto Gradle. Eche un vistazo al Repo vinculado en la introducción, o mejor aún, ejecute las muestras. Solo tenga en cuenta estas dos variables de ruta, ya que se usan a menudo como directorio de inicio para entradas y salidas.

1
2
3
4
5
6
7
public class App {

    static final Path zippedDir = Path.of("ZippedData");
    static final Path inputDataDir = Path.of("InputData");
    
    // ... other stuff   
}

Inspección del contenido de un archivo zip

Puede crear una instancia de una clase ZipFile y pasarle la ruta a un archivo zip existente, que esencialmente lo abre como cualquier otro archivo, luego inspeccionar el contenido consultando la enumeración ZipEntry que contiene. Tenga en cuenta que ZipFile implementa la interfaz Cierre automático, lo que lo convierte en un gran candidato para la prueba- con-recursos Construcción de programación Java que se muestra a continuación ya lo largo de los ejemplos aquí.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
static void showZipContents() {
    try (var zf = new ZipFile("ZipToInspect.zip")) {
    
        System.out.println(String.format("Inspecting contents of: %s\n", zf.getName()));
        
        Enumeration<? extends ZipEntry> zipEntries = zf.entries();
        zipEntries.asIterator().forEachRemaining(entry -> {
            System.out.println(String.format(
                "Item: %s \nType: %s \nSize: %d\n",
                entry.getName(),
                entry.isDirectory() ? "directory" : "file",
                entry.getSize()
            ));
        });
    } catch (IOException e) {
      e.printStackTrace();
    }
}

Ejecutando el proyecto Gradle usando lo siguiente:

1
$ ./gradlew run

Esto produce una salida para el método App.showZipContents de:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
> Task :run
Inspecting contents of: ZipToInspect.zip

Item: ZipToInspect/ 
Type: directory 
Size: 0

Item: ZipToInspect/greetings.txt 
Type: file 
Size: 160

Item: ZipToInspect/InnerFolder/ 
Type: directory 
Size: 0

Item: ZipToInspect/InnerFolder/About.txt 
Type: file 
Size: 39

Aquí puede ver que esto imprime todos los archivos y directorios en el archivo zip, incluso los archivos dentro de los directorios.

Extracción de un archivo zip

Extraer el contenido de un archivo zip en el disco requiere nada más que replicar la misma estructura de directorios que está dentro del ZipFile, que se puede determinar a través de ZipEntry.isDirectory y luego copiar los archivos representados en las instancias de ZipEntry en disco.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static void unzipAZip() {
    var outputPath = Path.of("UnzippedContents");

    try (var zf = new ZipFile("ZipToInspect.zip")) {
    
        // Delete if exists, then create a fresh empty directory to put the zip archive contents
        initialize(outputPath);

        Enumeration<? extends ZipEntry> zipEntries = zf.entries();
        zipEntries.asIterator().forEachRemaining(entry -> {
            try {
                if (entry.isDirectory()) {
                    var dirToCreate = outputPath.resolve(entry.getName());
                    Files.createDirectories(dirToCreate);
                } else {
                    var fileToCreate = outputPath.resolve(entry.getName());
                    Files.copy(zf.getInputStream(entry), fileToCreate);
                }
            } catch(IOException ei) {
                ei.printStackTrace();
            }
         });
    } catch(IOException e) {
        e.printStackTrace();
    }
}

Escribir archivos directamente en un nuevo archivo zip {#escribir archivos directamente en un nuevo archivo zip}

Dado que escribir un archivo zip no es más que escribir un flujo de datos en algún destino (un archivo Zip en este caso), escribir datos, como datos de cadena, en un archivo zip solo es diferente en que necesita hacer coincidir los datos que se están almacenando. escrito en instancias de ZipEntry agregadas a ZipOutputStream.

Una vez más, ZipOutputStream implementa la interfaz AutoCloseable, por lo que es mejor usarla con una declaración de prueba con recursos. El único problema real es recordar cerrar su ZipEntry cuando haya terminado con cada uno para dejar en claro cuándo ya no debería recibir datos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
static void zipSomeStrings() {
    Map<String, String> stringsToZip = Map.ofEntries(
        entry("file1", "This is the first file"),
        entry("file2", "This is the second file"),
        entry("file3", "This is the third file")
    );
    var zipPath = zippedDir.resolve("ZipOfStringData.zip");
    try (var zos = new ZipOutputStream(
                            new BufferedOutputStream(Files.newOutputStream(zipPath)))) {
        for (var entry : stringsToZip.entrySet()) {
            zos.putNextEntry(new ZipEntry(entry.getKey()));
            zos.write(entry.getValue().getBytes());
            zos.closeEntry();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Comprimir un archivo existente en un nuevo archivo zip {#comprimir un archivo existente en un nuevo archivo zip}

Si ha copiado un archivo en Java antes de eso, esencialmente ya es un PRO en la creación de un archivo zip a partir de un archivo existente (o directorio para el caso ). Una vez más, la única diferencia real es que debe tener un poco más de precaución para asegurarse de que los archivos coincidan con las instancias apropiadas de ZipEntry.

En este ejemplo, creo un archivo de entrada "FileToZip.txt" y le escribo algunos datos "¡Hola amigos de Java!" y luego uso [Archivos.copiar (ruta, flujo de salida)](https:/ /docs.oracle.com/javase/7/docs/api/java/nio/file/Files.html#copy(java.nio.file.Path,%20java.io.OutputStream)) para asociar ZipEntry con el archivo FileToZip.txt dentro del archivo comprimido ZippedFile.zip que estoy creando con una instancia ZipOutoutStream.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
static void zipAFile() {
    var inputPath = inputDataDir.resolve("FileToZip.txt");
    var zipPath = zippedDir.resolve("ZippedFile.zip");
    
    try (var zos = new ZipOutputStream(
                            new BufferedOutputStream(Files.newOutputStream(zipPath)))) {
                            
        Files.writeString(inputPath, "Howdy There Java Friends!\n");

        zos.putNextEntry(new ZipEntry(inputPath.toString()));
        Files.copy(inputPath, zos);
        zos.closeEntry();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Comprimir una carpeta en un nuevo archivo zip

Comprimir un directorio que no está vacío se vuelve un poco más complicado, especialmente si desea mantener directorios vacíos dentro del directorio principal. Para mantener la presencia de un directorio vacío dentro de un archivo zip, debe asegurarse de crear una entrada que tenga el sufijo del separador de directorio del sistema de archivos al crearlo como ZipEntry y luego cerrarlo inmediatamente.

En este ejemplo, creo un directorio llamado "foldertozip" que contiene la estructura que se muestra a continuación, luego lo comprimo en un archivo zip.

1
2
3
4
5
6
tree .
.
└── foldertozip
    ├── emptydir
    ├── file1.txt
    └── file2.txt

En el siguiente código, observe que utilizo el método Files.walk(Path) para recorrer el árbol de directorios de "foldertozip" y buscar directorios vacíos ("emptydir" en este ejemplo) y si / cuando los encuentre concatene el separador de directorio con el nombre dentro de ZipEntry. Después de esto, lo cierro tan pronto como lo agrego a la instancia ZipOutputStream.

También uso un enfoque ligeramente diferente para inyectar los archivos que no son de directorio en ZipOutputStream en comparación con el último ejemplo, pero solo estoy usando este enfoque diferente por el bien de la variedad en los ejemplos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static void zipADirectoryWithFiles() {
    var foldertozip = inputDataDir.resolve("foldertozip"); 
    var dirFile1 = foldertozip.resolve("file1.txt");
    var dirFile2 = foldertozip.resolve("file2.txt"); 

    var zipPath = zippedDir.resolve("ZippedDirectory.zip");
    try (var zos = new ZipOutputStream(
                            new BufferedOutputStream(Files.newOutputStream(zipPath)))) {
                            
        Files.createDirectory(foldertozip);
        Files.createDirectory(foldertozip.resolve("emptydir"));
        Files.writeString(dirFile1, "Does this Java get you rev'd up or what?");
        Files.writeString(dirFile2, "Java Java Java ... Buz Buz Buz!");

        Files.walk(foldertozip).forEach(path -> {
            try {
                var reliativePath = inputDataDir.relativize(path);
                var file = path.toFile();
                if (file.isDirectory()) {
                    var files = file.listFiles();
                    if (files == null || files.length == 0) {
                        zos.putNextEntry(new ZipEntry(
                                reliativePath.toString() + File.separator));
                        zos.closeEntry();
                    }
                } else {
                    zos.putNextEntry(new ZipEntry(reliativePath.toString()));
                    zos.write(Files.readAllBytes(path));
                    zos.closeEntry();
                }
            } catch(IOException e) {
                e.printStackTrace();
            }
        });
    } catch(IOException e) {
        e.printStackTrace();
    }
}

Conclusión

En este artículo, he discutido y demostrado un enfoque moderno para trabajar con archivos zip en Java utilizando Java puro y sin bibliotecas de terceros. También puede notar que uso algunas características más modernas del lenguaje Java, como paradigmas de programación funcional y la palabra clave var para escribir variables inferidas, así que asegúrese de estar usando al menos Java 10 cuando ejecute estos ejemplos.

Como siempre, gracias por leer y no se avergüence de comentar o criticar a continuación. ntinuación.

Licensed under CC BY-NC-SA 4.0