# Java - Zipper et dézipper des fichiers et des répertoires

Il existe deux APIs en java pour zipper/dézipper des fichiers:

  • l'API java.util.zip qui utilise des InputStream et des OutputStream,
  • l'API java.nio.file qui utilise l'abstraction FileSystem disponible depuis Java 7.

L'abstraction java.nio.file.FileSystem est globalement plus simple à utiliser puisqu'elle permet de réfléchir directement au niveau des fichiers sans se soucier de transférer leur contenu byte par byte.

# Zipper un fichier

Dans ce premier exemple, un fichier some-file-1.txt est zipper dans une archive some-file.zip, créée pour l'occasion. A noter que le nom du fichier dans l'archive est modifié pour some-file-1-in-zip.txt:

@Test
@Order(1)
public void zipFile() {
  Path zipPath = getTempPath("some-file.zip");
  Path inputFilePath = getResourcePath("some-file-1.txt");
  try (FileInputStream fis = new FileInputStream(inputFilePath.toFile());
      FileOutputStream fos = new FileOutputStream(zipPath.toFile());
      ZipOutputStream zos = new ZipOutputStream(fos)) {
    ZipEntry entry = new ZipEntry("some-file-1-in-zip.txt");
    zos.putNextEntry(entry);
    copy(fis, zos);
  } catch (IOException e) {
    LOG.error("The file can't be added to the zip", e);
  }
  assertThat(zipPath).exists();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

En pratique, il suffit d'ouvrir un InputStream sur le fichier à compresser et à ZipOutputStream sur le fichier zipper puis de copier le contenu de l'un à l'autre. Ici, la fonction copy utilisée entre les streams est la suivante:

private void copy(InputStream inputStream, OutputStream outputStream) throws IOException {
  byte[] buffer = new byte[1024];
  int length;
  while ((length = inputStream.read(buffer)) >= 0) {
    outputStream.write(buffer, 0, length);
  }
}
1
2
3
4
5
6

A noter que les répertoires et les fichiers contenu dans le zip sont représentées par des ZipEntry qui représente un chemin relatif dans le zip. Il faut donc toujours indiquer au ZipOutputStream dans quelle ZipEntry doivent se trouver les données transférées.

TIP

Les InputStream et OutputStream implémentent l'interface Closeable, il est donc conseillé de les ouvrir avec un try-with-resource

TIP

Dans l'exemple ci-dessus, un FileOutputStream a été directement utilisé, mais il est tout autant possible d'utiliser un BufferedOutputStream pour nourrir le ZipOutputStream.

# Zipper un répertoire

Pour zipper un répertoire, il suffit de répéter l'opération précédente pour l'ensemble des fichiers du répertoire. On peut utiliser Files.walk() pour parcourir récursivement les fichiers d'un répertoire:

@Test
@Order(2)
public void zipDirectory() {
  Path zipPath = getTempPath("some-directory.zip");
  Path inputDirectoryPath = getResourcePath("some-directory");
  try (FileOutputStream fos = new FileOutputStream(zipPath.toFile());
      ZipOutputStream zos = new ZipOutputStream(fos)) {
    Files.walk(inputDirectoryPath)
        .filter(someFileToZip -> !someFileToZip.equals(inputDirectoryPath))
        .forEach(
            someFileToZip -> {
              Path pathInZip = inputDirectoryPath.relativize(someFileToZip);
              if (Files.isDirectory(someFileToZip)) {
                addDirectory(zos, pathInZip);
              } else {
                addFile(zos, someFileToZip, pathInZip);
              }
            });
  } catch (IOException e) {
    LOG.error("The file can't be added to the zip", e);
  }
  assertThat(zipPath).exists();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Dans l'exemple ci-dessus, les fichiers sont ajoutés dans le zip en concervant leur nom et leur chemin relatif au répertoire compressé, d'où le inputDirectoryPath.relativize(path).

Les fichiers et les répertoires sont ajoutés différemment au zip. Pour les répertoires, il faut penser à simplement ajouter le ZipEntry correspondant en s'assurant que son nom se termine bien par un /:

private void addDirectory(ZipOutputStream zos, Path relativeFilePath) {
  try {
    ZipEntry entry = new ZipEntry(relativeFilePath.toString() + "/");
    zos.putNextEntry(entry);
    zos.closeEntry();
  } catch (IOException e) {
    LOG.error("Unable to add directory {} to zip", relativeFilePath, e);
  }
}
1
2
3
4
5
6
7
8

WARNING

Si le nom du ZipEntry ne se termine pas par /, il ne sera pas considérée comme un répertoire mais comme un fichier vide.

Pour les fichiers, le code utilisé dans la première section est repris sous forme de fonction:

private void addFile(ZipOutputStream zos, Path filePath, Path zipFilePath) {
  try (FileInputStream fis = new FileInputStream(filePath.toFile())) {
    ZipEntry entry = new ZipEntry(zipFilePath.toString());
    zos.putNextEntry(entry);
    copy(fis, zos);
  } catch (IOException e) {
    LOG.error("Unable to add file {} to zip", zipFilePath, e);
  }
}
1
2
3
4
5
6
7
8

# Dézipper

Dézipper les fichiers correspond à l'opération inverse, les données sont cette fois copiées d'un ZipInputStream vers un OutputStream.

@Test
@Order(3)
public void unzip() {
  Path zipPath = getTempPath("some-directory.zip");
  try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipPath.toFile()))) {
    ZipEntry entry = zis.getNextEntry();
    while (entry != null) {
      Path outputEntryPath = path.resolve(entry.getName());
      if (entry.isDirectory() && !Files.exists(outputEntryPath)) {
        Files.createDirectory(outputEntryPath);
      } else if (!entry.isDirectory()) {
        try (FileOutputStream fos = new FileOutputStream(outputEntryPath.toFile())) {
          copy(zis, fos);
        }
      }
      entry = zis.getNextEntry();
    }
    zis.closeEntry();
  } catch (IOException e) {
    LOG.error("Unable to unzip", zipPath, e);
  }
  assertThat(getTempPath("some-file-1.txt")).exists();
  assertThat(getTempPath("some-file-2.txt")).exists();
  assertThat(getTempPath("some-subdirectory/some-file-3.txt")).exists();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

Quand les fichiers sont extraits, il faut s'assurer que leur sous-répertoire de destination existe bien avant de les écrire. Ci-dessus, cela est géré en créant le répertoire correspondant pour chaque ZipEntry de type répertoire.

# Zipper un répertoire avec FileSystem

L'API java.nio fournit une abstraction FileSystem avec une implémentation ZipFileSystem qui permet de manipuler un fichier zip comme un système de fichier normal. En utilisant cette API, la notion de ZipEntry disparait, et les fichiers sont directement copiés du système de fichier courant à celui du zip. Pour obtenir le Path d'un fichier dans le zip, on utilise simplement la fonction FileSystem.getPath.

@Test
@Order(4)
public void zipDirectoryWithFileSystem() throws IOException {
  Path directoryToZip = getResourcePath("some-directory");
  Map<String, String> env = new HashMap<>();
  env.put("create", "true");
  Path zipPath = getTempPath("some-directory-fs.zip");
  URI zipUri = URI.create("jar:file:" + zipPath.toString());
  try (FileSystem zipFs = FileSystems.newFileSystem(zipUri, env)) {
    Files.walk(directoryToZip)
        .forEach(
            someFileToZip -> {
              Path relativeFilePath = directoryToZip.relativize(someFileToZip);
              Path filePathInZip = zipFs.getPath("/", relativeFilePath.toString());
              try {
                if (Files.isDirectory(someFileToZip) && !Files.exists(filePathInZip)) {
                  Files.createDirectory(filePathInZip);
                } else if (!Files.isDirectory(someFileToZip)) {
                  Files.copy(someFileToZip, filePathInZip, StandardCopyOption.REPLACE_EXISTING);
                }
              } catch (IOException e) {
                LOG.error("Unable to add {} to zip", relativeFilePath, e);
              }
            });
  }
  assertThat(zipPath).exists();
}
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

Là encore, il faut s'assurer que les répertoires sont bien ajoutés dans le zip. Cette fois, il suffit de les créer directement dans le système de fichier du zip.

# Dézipper avec FileSystem

Dézipper via un FileSystem est l'opération inverse de la précédente: les fichiers sont copiés du système de fichier zip vers le système de fichier courant:

@Test
@Order(5)
public void unzipWithFileSystem() {
  Path zipPath = getTempPath("some-directory-fs.zip");
  URI uri = URI.create("jar:file:" + zipPath.toString());
  try (FileSystem zipFs = FileSystems.newFileSystem(uri, new HashMap<>())) {
    Files.walk(zipFs.getPath("/"))
        .forEach(
            someFileInZip -> {
              Path absoluteFilePath = getTempPath(someFileInZip.toString().substring(1));
              try {
                if (Files.isDirectory(absoluteFilePath) && !Files.exists(absoluteFilePath)) {
                  Files.createDirectory(absoluteFilePath);
                } else if (!Files.isDirectory(absoluteFilePath)) {
                  Files.copy(
                      someFileInZip, absoluteFilePath, StandardCopyOption.REPLACE_EXISTING);
                }
              } catch (IOException e) {
                LOG.error("Unable to unzip {} to {}", someFileInZip, absoluteFilePath, e);
              }
            });
  } catch (IOException e) {
    LOG.error("Unable to unzip {}", zipPath, e);
  }
  assertThat(getTempPath("some-file-1.txt")).exists();
  assertThat(getTempPath("some-file-2.txt")).exists();
  assertThat(getTempPath("some-subdirectory/some-file-3.txt")).exists();
}
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
Dernière mise à jour: 7/1/2019, 4:08:10 PM