2009-09-09 26 views
35

Al tratar de comprimir un archivo usando el java.util.zip tuve muchos problemas, la mayoría de los cuales resolví. Ahora que finalmente obtengo algo de salida, lucho por obtener la salida "correcta". Tengo un archivo ODT extraído (el directorio sería más adecuado para una descripción) al que hice algunas modificaciones. Ahora quiero comprimir ese directorio para recrear la estructura del archivo ODT. Comprimir el directorio y cambiarle el nombre para que termine con .odt funciona bien, así que no debería haber ningún problema.java.util.zip - Recreando la estructura de directorios

El problema principal es que pierdo la estructura interna del directorio. Todo se vuelve "plano" y no parece encontrar una manera de preservar la estructura original de varias capas. Agradecería algo de ayuda sobre esto ya que no puedo encontrar el problema.

Éstos son los fragmentos de código relevantes:

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

El SEPARATOR es el separador de archivos del sistema y el FILEPATH es la ruta de archivo de la ODT original que voy a reemplazar, pero no he hecho aquí con fines de prueba. Simplemente escribo en un archivo test.zip en el mismo directorio.

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

El directorio que contiene los archivos zip es un lugar en el espacio de usuario y no en el mismo directorio que el archivo resultante. Supongo que esto podría ser un problema, pero realmente no puedo ver cómo. También calculé que el problema podría estar en usar la misma corriente para la salida, pero de nuevo no puedo ver cómo. Vi en algunos ejemplos y tutoriales que usan getPath() en lugar de getName() pero cambiar eso me da un archivo zip vacío.

Respuesta

87

La clase URI es útil para trabajar con rutas relativas.

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

El código anterior emitirá la cadena path/myfile.txt.

Para completar, aquí es un método zip para el archivo de un directorio:

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

Este código hace que no conserva las fechas y no estoy seguro de cómo iba a reaccionar a cosas como enlaces simbólicos. No se intenta agregar entradas de directorio, por lo que los directorios vacíos no se incluirán.

El correspondiente unzip comando:

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étodos de utilidad de los que dependen:

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

El tamaño del búfer es totalmente arbitraria.

+0

Muchas gracias! Desgraciadamente, no veo cómo se conserva el formato de directorio original con tu código de compresión. En el ODT que uso hay directorios vacíos. Por lo que entiendo tu código, esos directorios nunca serán creados. ¿Me estoy perdiendo algo? – Eric

+2

Los directorios son entradas vacías con un nombre que termina con '/'. He alterado el código. – McDowell

+0

He adaptado la estructura de tu código y he abandonado las llamadas recursivas. Creo que fue la forma incorrecta de ver esto. El código funciona sin problemas con una excepción; Agrega carpetas vacías a la mayoría de los directorios secundarios, incluso si no están vacíos. Descubrí que eliminar la siguiente línea resuelve el problema: name = name.endsWith ("/")? nombre: nombre + "/"; Sospecho que al agregar un directorio al agregar la "\" también se crea una carpeta vacía dentro. Simplemente dejando que ZipEntries se ocupe de la construcción de la estructura, todo parece estar bien. ¡Gracias por toda tu ayuda! – Eric

7

veo 2 problemas en su código,

  1. no guarda la ruta del directorio de modo que no hay manera de recuperarlo.
  2. En Windows, debe usar "/" como separador de ruta. A algunos programas de descompresión no les gusta \.

Incluyo mi propia versión para su referencia. Usamos este para comprimir las fotos para descargar, así que funciona con varios programas de descompresión.Conserva la estructura del directorio y las marcas de tiempo.

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; 
} 
} 
+0

Gracias por su contribución. Agradezco su ejemplo de código pero no estoy seguro de cómo me ayudará a encontrar el error en mi código como; la llamada inicial a la función es con la ruta completa del directorio que luego se transmitirá en la función y en segundo lugar; mi constante SEPARATOR se inicializa con System.getProperty ("file.separator") que me dará el separador de archivos predeterminado del sistema operativo. Nunca codificaría un separador porque supone que su código solo se implementará en un sistema operativo determinado. – Eric

+4

No use File.separator en ZIP. El separador debe ser "/" según la especificación. Si está en Windows, debe abrir el archivo como "D: \ dir \ subdir \ file", pero la entrada ZIP debe ser "dir/subdir/file". –

+1

Ya veo. Gracias por señalar esto. ¡No sabía que ZIP era tan exigente! :) – Eric

4

Simplemente ingrese a la fuente de java.util.zip.ZipEntry. Trata un ZipEntry como directorio si su nombre termina con caracteres "/". Simplemente agregue el nombre del directorio con "/". También debe quitar el prefijo de la unidad para hacerlo relativo.

Comprobar este ejemplo para comprimir sólo los directorios vacíos,

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

Mientras que son capaces de crear tanto vacíos & directorios no vacíos en el archivo ZIP, la estructura de directorios está intacto.

Buena suerte.

1

Me gustaría añadir una sugerencia/recordatorio aquí:

Si se define el directorio de salida es el mismo que el de entrada, se le quiere comparar el nombre de cada archivo con el nombre de archivo del zip de salida , para evitar comprimir el archivo dentro de sí mismo, generando un comportamiento no deseado. Espero que esto sea de alguna ayuda.

1

Para comprimir contenido de una carpeta y sus subcarpetas de Windows,

sustituir,

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

con

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

Aquí es otro ejemplo (recursiva), que también le permite incluir/excluir la carpeta que contiene el 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(); 
    } 
    } 
} 
2

Si no desea preocuparse por las secuencias de entrada de bytes, tamaños de búfer y otros detalles de bajo nivel. Puede usar las librerías Zip de Ant desde su código java (las dependencias maven se pueden encontrar en here). Esto es ahora hago una cremallera que consiste en una lista de archivos de directorios: &

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"); 
} 
0

Este código cortó funciona para mí. No se necesita una biblioteca de terceros.

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(); 
    } 
} 
Cuestiones relacionadas