web-dev-qa-db-fra.com

java.util.Zip - Recréation de la structure de répertoire

En essayant de compresser une archive en utilisant le Java.util.Zip, j'ai rencontré beaucoup de problèmes que j'ai résolus pour la plupart. Maintenant que j'ai enfin un résultat, j'ai du mal à obtenir le "bon" résultat. J'ai un fichier ODT extrait (le répertoire conviendrait mieux à une description) auquel j'ai apporté quelques modifications. Maintenant, je veux compresser ce répertoire pour recréer la structure de fichier ODT. Zipper le répertoire et le renommer pour qu'il se termine par .odt fonctionne bien, donc il ne devrait y avoir aucun problème.

Le problème principal est que je perds la structure interne du répertoire. Tout devient "plat" et je ne semble pas trouver le moyen de préserver la structure multicouche originale. J'apprécierais un peu d'aide car je n'arrive pas à trouver le problème.

Voici les extraits de code pertinents:

ZipOutputStream out = new ZipOutputStream(new FileOutputStream(
    FILEPATH.substring(0, FILEPATH.lastIndexOf(SEPARATOR) + 1).concat("test.Zip")));
    compressDirectory(TEMPARCH, out);

La SEPARATOR est le séparateur de fichiers système et la FILEPATH est le chemin de fichier de l'ODT d'origine que je vais remplacer mais que je n'ai pas encore fait ici pour des tests. J'écris simplement dans un fichier test.Zip situé dans le même répertoire.

private void compressDirectory(String directory, ZipOutputStream out) throws IOException
{
    File fileToCompress = new File(directory);
    // list contents.
    String[] contents = fileToCompress.list();
    // iterate through directory and compress files.
    for(int i = 0; i < contents.length; i++)
    {
        File f = new File(directory, contents[i]);
        // testing type. directories and files have to be treated separately.
        if(f.isDirectory())
        {
            // add empty directory
            out.putNextEntry(new ZipEntry(f.getName() + SEPARATOR));
            // initiate recursive call
            compressDirectory(f.getPath(), out);
            // continue the iteration
            continue;
        }else{
             // prepare stream to read file.
             FileInputStream in = new FileInputStream(f);
             // create ZipEntry and add to outputting stream.
             out.putNextEntry(new ZipEntry(f.getName()));
             // write the data.
             int len;
             while((len = in.read(data)) > 0)
             {
                 out.write(data, 0, len);
             }
             out.flush();
             out.closeEntry();
             in.close();
         }
     }
 }

Le répertoire qui contient les fichiers au format Zip se trouve quelque part dans l’espace utilisateur et non dans le même répertoire que le fichier résultant. Je suppose que cela pourrait être un problème, mais je ne vois pas vraiment comment. Aussi, j'ai pensé que le problème pourrait être d'utiliser le même flux pour la sortie, mais encore une fois, je ne vois pas comment. J'ai vu dans certains exemples et tutoriels qu'ils utilisaient getPath() à la place de getName(), mais changer me donne un fichier Zip vide.

38
Eric

La classe URI est utile pour utiliser des chemins relatifs.

File mydir = new File("C:\\mydir");
File myfile = new File("C:\\mydir\\path\\myfile.txt");
System.out.println(mydir.toURI().relativize(myfile.toURI()).getPath());

Le code ci-dessus émettra la chaîne path/myfile.txt.

Pour être complet, voici une méthode Zip pour archiver un répertoire:

  public static void Zip(File directory, File zipfile) throws IOException {
    URI base = directory.toURI();
    Deque<File> queue = new LinkedList<File>();
    queue.Push(directory);
    OutputStream out = new FileOutputStream(zipfile);
    Closeable res = out;
    try {
      ZipOutputStream zout = new ZipOutputStream(out);
      res = zout;
      while (!queue.isEmpty()) {
        directory = queue.pop();
        for (File kid : directory.listFiles()) {
          String name = base.relativize(kid.toURI()).getPath();
          if (kid.isDirectory()) {
            queue.Push(kid);
            name = name.endsWith("/") ? name : name + "/";
            zout.putNextEntry(new ZipEntry(name));
          } else {
            zout.putNextEntry(new ZipEntry(name));
            copy(kid, zout);
            zout.closeEntry();
          }
        }
      }
    } finally {
      res.close();
    }
  }

Ce code ne conserve pas les dates et je ne sais pas comment il réagirait à des choses comme des liens symboliques. Aucune tentative n'a été faite pour ajouter des entrées de répertoire, les répertoires vides ne seraient donc pas inclus.

La commande unzip correspondante:

  public static void unzip(File zipfile, File directory) throws IOException {
    ZipFile zfile = new ZipFile(zipfile);
    Enumeration<? extends ZipEntry> entries = zfile.entries();
    while (entries.hasMoreElements()) {
      ZipEntry entry = entries.nextElement();
      File file = new File(directory, entry.getName());
      if (entry.isDirectory()) {
        file.mkdirs();
      } else {
        file.getParentFile().mkdirs();
        InputStream in = zfile.getInputStream(entry);
        try {
          copy(in, file);
        } finally {
          in.close();
        }
      }
    }
  }

Méthodes d'utilité sur lesquelles ils s'appuient:

  private static void copy(InputStream in, OutputStream out) throws IOException {
    byte[] buffer = new byte[1024];
    while (true) {
      int readCount = in.read(buffer);
      if (readCount < 0) {
        break;
      }
      out.write(buffer, 0, readCount);
    }
  }

  private static void copy(File file, OutputStream out) throws IOException {
    InputStream in = new FileInputStream(file);
    try {
      copy(in, out);
    } finally {
      in.close();
    }
  }

  private static void copy(InputStream in, File file) throws IOException {
    OutputStream out = new FileOutputStream(file);
    try {
      copy(in, out);
    } finally {
      out.close();
    }
  }

La taille de la mémoire tampon est entièrement arbitraire.

91
McDowell

Je vois 2 problèmes dans votre code,

  1. Vous n'enregistrez pas le chemin du répertoire, il n'y a donc aucun moyen de le récupérer.
  2. Sous Windows, vous devez utiliser "/" comme séparateur de chemin. Un programme unzip n'aime pas \.

J'inclus ma propre version pour votre référence. Nous utilisons celui-ci pour compresser des photos et les télécharger afin que cela fonctionne avec divers programmes de décompression. Il préserve la structure de répertoire et les horodatages. 

  public static void createZipFile(File srcDir, OutputStream out,
   boolean verbose) throws IOException {

  List<String> fileList = listDirectory(srcDir);
  ZipOutputStream zout = new ZipOutputStream(out);

  zout.setLevel(9);
  zout.setComment("Zipper v1.2");

  for (String fileName : fileList) {
   File file = new File(srcDir.getParent(), fileName);
   if (verbose)
    System.out.println("  adding: " + fileName);

   // Zip always use / as separator
   String zipName = fileName;
   if (File.separatorChar != '/')
    zipName = fileName.replace(File.separatorChar, '/');
   ZipEntry ze;
   if (file.isFile()) {
    ze = new ZipEntry(zipName);
    ze.setTime(file.lastModified());
    zout.putNextEntry(ze);
    FileInputStream fin = new FileInputStream(file);
    byte[] buffer = new byte[4096];
    for (int n; (n = fin.read(buffer)) > 0;)
     zout.write(buffer, 0, n);
    fin.close();
   } else {
    ze = new ZipEntry(zipName + '/');
    ze.setTime(file.lastModified());
    zout.putNextEntry(ze);
   }
  }
  zout.close();
 }

 public static List<String> listDirectory(File directory)
   throws IOException {

  Stack<String> stack = new Stack<String>();
  List<String> list = new ArrayList<String>();

  // If it's a file, just return itself
  if (directory.isFile()) {
   if (directory.canRead())
    list.add(directory.getName());
   return list;
  }

  // Traverse the directory in width-first manner, no-recursively
  String root = directory.getParent();
  stack.Push(directory.getName());
  while (!stack.empty()) {
   String current = (String) stack.pop();
   File curDir = new File(root, current);
   String[] fileList = curDir.list();
   if (fileList != null) {
    for (String entry : fileList) {
     File f = new File(curDir, entry);
     if (f.isFile()) {
      if (f.canRead()) {
       list.add(current + File.separator + entry);
      } else {
       System.err.println("File " + f.getPath()
         + " is unreadable");
       throw new IOException("Can't read file: "
         + f.getPath());
      }
     } else if (f.isDirectory()) {
      list.add(current + File.separator + entry);
      stack.Push(current + File.separator + f.getName());
     } else {
      throw new IOException("Unknown entry: " + f.getPath());
     }
    }
   }
  }
  return list;
 }
}
7
ZZ Coder

Il suffit de passer par la source de Java.util.Zip.ZipEntry. Il considère un répertoire ZipEntry comme un répertoire si son nom se termine par les caractères "/". Suffixez simplement le nom du répertoire avec "/".Vous devez également supprimer le préfixe du lecteur pour le rendre relatif.

Consultez cet exemple pour compresser uniquement les répertoires vides, 

http://bethecoder.com/applications/tutorials/showTutorials.action?tutorialId=Java_ZipUtilities_ZipEmptyDirectory

Tant que vous êtes capable de créer des répertoires vides et non vides dans un fichier Zip, votre structure de répertoires est intacte. 

Bonne chance.

4
user769087

Voici un autre exemple (récursif) qui vous permet également d'inclure/exclure le dossier contenant le fichier Zip:

import Java.io.File;
import Java.io.FileInputStream;
import Java.io.FileOutputStream;
import Java.io.IOException;
import Java.util.Zip.ZipEntry;
import Java.util.Zip.ZipOutputStream;

public class ZipUtil {

  private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;

  public static void main(String[] args) throws Exception {
    zipFile("C:/tmp/demo", "C:/tmp/demo.Zip", true);
  }

  public static void zipFile(String fileToZip, String zipFile, boolean excludeContainingFolder)
    throws IOException {        
    ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile));    

    File srcFile = new File(fileToZip);
    if(excludeContainingFolder && srcFile.isDirectory()) {
      for(String fileName : srcFile.list()) {
        addToZip("", fileToZip + "/" + fileName, zipOut);
      }
    } else {
      addToZip("", fileToZip, zipOut);
    }

    zipOut.flush();
    zipOut.close();

    System.out.println("Successfully created " + zipFile);
  }

  private static void addToZip(String path, String srcFile, ZipOutputStream zipOut)
    throws IOException {        
    File file = new File(srcFile);
    String filePath = "".equals(path) ? file.getName() : path + "/" + file.getName();
    if (file.isDirectory()) {
      for (String fileName : file.list()) {             
        addToZip(filePath, srcFile + "/" + fileName, zipOut);
      }
    } else {
      zipOut.putNextEntry(new ZipEntry(filePath));
      FileInputStream in = new FileInputStream(srcFile);

      byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
      int len;
      while ((len = in.read(buffer)) != -1) {
        zipOut.write(buffer, 0, len);
      }

      in.close();
    }
  }
}
3
fcarriedo

Si vous ne voulez pas vous soucier des flux d'entrée d'octets, de la taille des tampons et d'autres détails de bas niveau. Vous pouvez utiliser les bibliothèques Zip de Ant à partir de votre code Java (vous pouvez trouver les dépendances maven ici ). Voici maintenant je fais un zip comprenant une liste de fichiers et de répertoires:

public static void createZip(File zipFile, List<String> fileList) {

    Project project = new Project();
    project.init();

    Zip zip = new Zip();
    Zip.setDestFile(zipFile);
    Zip.setProject(project);

    for(String relativePath : fileList) {

        //noramalize the path (using commons-io, might want to null-check)
        String normalizedPath = FilenameUtils.normalize(relativePath);

        //create the file that will be used
        File fileToZip = new File(normalizedPath);
        if(fileToZip.isDirectory()) {
            ZipFileSet fileSet = new ZipFileSet();
            fileSet.setDir(fileToZip);
            fileSet.setPrefix(fileToZip.getPath());
            Zip.addFileset(fileSet);
        } else {
            FileSet fileSet = new FileSet();
            fileSet.setDir(new File("."));
            fileSet.setIncludes(normalizedPath);
            Zip.addFileset(fileSet);
        }
    }

    Target target = new Target();
    target.setName("ziptarget");
    target.addTask(Zip);
    project.addTarget(target);
    project.executeTarget("ziptarget");
}
2
ChoppyTheLumberjack

Je voudrais ajouter une suggestion/rappel ici:

Si vous définissez le répertoire de sortie comme étant le même que le répertoire d'entrée, vous voudrez comparer le nom de chaque fichier avec le nom du fichier .Zip en sortie, afin d'éviter de compresser le fichier en lui-même et de générer un comportement indésirable. J'espère que cela vous aidera. 

1
Luca Bezerra

Pour compresser le contenu d’un dossier et de ses sous-dossiers dans Windows,

remplacer, 

out.putNextEntry(new ZipEntry(files[i])); 

avec

out.putNextEntry(new ZipEntry(files[i]).replace(inFolder+"\\,"")); 
1
Geo

Cet extrait de code fonctionne pour moi. Aucune bibliothèque tierce requise.

public static void zipDir(final Path dirToZip, final Path out) {
    final Stack<String> stackOfDirs = new Stack<>();
    final Function<Stack<String>, String> createPath = stack -> stack.stream().collect(Collectors.joining("/")) + "/";
    try(final ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(out.toFile()))) {
        Files.walkFileTree(dirToZip, new FileVisitor<Path>() {

            @Override
            public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) throws IOException {
                stackOfDirs.Push(dir.toFile().getName());
                final String path = createPath.apply(stackOfDirs);
                final ZipEntry zipEntry = new ZipEntry(path);
                zipOut.putNextEntry(zipEntry);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
                final String path = String.format("%s%s", createPath.apply(stackOfDirs), file.toFile().getName());
                final ZipEntry zipEntry = new ZipEntry(path);
                zipOut.putNextEntry(zipEntry);
                Files.copy(file, zipOut);
                zipOut.closeEntry();
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed(final Path file, final IOException exc) throws IOException {
                final StringWriter stringWriter = new StringWriter();
                try(final PrintWriter printWriter = new PrintWriter(stringWriter)) {
                    exc.printStackTrace(printWriter);
                    System.err.printf("Failed visiting %s because of:\n %s\n",
                            file.toFile().getAbsolutePath(), printWriter.toString());
                }
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) throws IOException {
                stackOfDirs.pop();
                return FileVisitResult.CONTINUE;
            }
        });
    } catch (IOException e) {
        e.printStackTrace();
    }
}
0
Alessandro Giusa