java.util.zip - воссоздание структуры каталогов

Пытаясь заархивировать архив с помощью java.util.zip, я столкнулся с множеством проблем, большинство из которых я решил. Теперь, когда я наконец получил какой-то результат, мне трудно получить «правильный» результат. У меня есть извлеченный файл ODT (каталог будет более подходящим для описания), в который я внес некоторые изменения. Теперь я хочу сжать этот каталог, чтобы воссоздать файловую структуру ODT. Заархивирование каталога и переименование его в конец с расширением .odt работает нормально, так что проблем быть не должно.

Основная проблема в том, что я теряю внутреннюю структуру каталога. Все становится «плоским», и я, кажется, не нахожу способа сохранить первоначальную многослойную структуру. Я был бы признателен за помощь в этом, так как я не могу найти проблему.

Вот соответствующие фрагменты кода:

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

SEPARATOR - это системный разделитель файлов, а FILEPATH - это путь к файлу исходного ODT, который я переопределю, но не сделал здесь для целей тестирования. Я просто записываю в файл test.zip в том же каталоге.

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

Каталог, содержащий файлы для архивирования, находится где-то в пользовательском пространстве, а не в том же каталоге, что и результирующий файл. Я предполагаю, что это может быть проблемой, но я не могу понять, как это сделать. Также я подумал, что проблема может заключаться в использовании того же потока для вывода, но опять же, я не понимаю, как это сделать. В некоторых примерах и учебных пособиях я видел, что они используют getPath() вместо getName(), но при изменении этого я получаю пустой zip-файл.


person Eric    schedule 09.09.2009    source источник


Ответы (8)


Класс URI полезен для работы с относительными пути.

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

Приведенный выше код выдаст строку path/myfile.txt.

Для полноты, вот метод zip архивации каталога:

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

Этот код не сохраняет даты, и я не уверен, как он отреагирует на такие вещи, как символические ссылки. Попытки добавить записи в каталог не предпринимаются, поэтому пустые каталоги не включаются.

Соответствующая команда unzip:

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

Полезные методы, на которые они полагаются:

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

Размер буфера совершенно произвольный.

person McDowell    schedule 09.09.2009
comment
Большое тебе спасибо! Увы, я не вижу, как исходный формат каталога сохраняется с вашим кодом сжатия. В ODT, который я использую, есть пустые каталоги. Насколько я понимаю ваш код, эти каталоги никогда не будут созданы. Возможно, я что-то упускаю? - person Eric; 09.09.2009
comment
Каталоги - это пустые записи с именем, заканчивающимся на /. Я изменил код. - person McDowell; 09.09.2009
comment
Я адаптировал структуру вашего кода и отказался от рекурсивных вызовов. Я думаю, это был неправильный взгляд на это. Код работает плавно, за одним исключением; он добавляет пустые папки в большинство дочерних каталогов, даже если они не пусты. Я обнаружил, что удаление следующей строки решает проблему: name = name.endsWith (/)? имя: имя + /; Я подозреваю, что при добавлении каталога путем добавления \ one также создается пустая папка внутри. Если просто позволить ZipEntries позаботиться о построении структуры, все будет в порядке. Спасибо за вашу помощь! - person Eric; 10.09.2009
comment
Возможный NPE: directory.listFiles () вернет null, если каталог пуст! - person Axel Fontaine; 28.04.2013
comment
@AxelFontaine Он вернет пустой каталог, если каталог пуст - doc - но существуют условия, при которых может быть возвращено значение null, поэтому в этом случае, вероятно, будет лучше выбросить исключение IOException. - person McDowell; 29.04.2013
comment
Большое спасибо за то, что сэкономили мне время! - person ojblass; 13.05.2015

Я вижу в вашем коде 2 проблемы,

  1. Вы не сохраняете путь к каталогу, поэтому нет возможности вернуть его.
  2. В Windows вам нужно использовать «/» в качестве разделителя пути. Какая-то программа-распаковщик не любит \.

Я включаю свою версию для вашей справки. Мы используем его, чтобы заархивировать фотографии для загрузки, поэтому он работает с различными программами для распаковки. Он сохраняет структуру каталогов и временные метки.

  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;
 }
}
person ZZ Coder    schedule 09.09.2009
comment
Спасибо за ваш вклад. Я благодарен за ваш пример кода, но я не уверен, как он поможет мне найти ошибку в моем коде: при первом вызове функции указывается полный путь к каталогу, который затем передается функции, а затем; моя константа SEPARATOR инициализируется с помощью System.getProperty (file.separator), который дает мне разделитель файлов по умолчанию в ОС. Я бы никогда не стал жестко кодировать разделитель, поскольку это предполагает, что ваш код будет развернут только в данной ОС. - person Eric; 10.09.2009
comment
Не используйте File.separator в ZIP. Разделитель должен быть / в соответствии со спецификацией. Если вы работаете в Windows, вы должны открыть файл как D: \ dir \ subdir \ file, но запись ZIP должна быть dir / subdir / file. - person ZZ Coder; 10.09.2009
comment
Я понимаю. Спасибо, что указали на это. Не знал, что ZIP такой разборчивый! :) - person Eric; 10.09.2009

Просто просмотрите исходный код java.util.zip.ZipEntry. Он рассматривает ZipEntry как каталог, если его имя заканчивается символами «/». Просто добавьте к имени каталога суффикс "/". Также вам нужно удалить префикс диска, чтобы сделать его относительным.

Проверьте этот пример для архивирования только пустых каталогов,

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

Пока вы можете создавать как пустые, так и непустые каталоги в ZIP-файле, ваша структура каталогов остается неизменной.

Удачи.

person user769087    schedule 25.05.2011

Вот еще один пример (рекурсивный), который также позволяет вам включать / исключать содержащую папку из 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();
    }
  }
}
person fcarriedo    schedule 13.02.2012

Если вы не хотите беспокоиться о байтовых входных потоках, размерах буферов и других деталях низкого уровня. Вы можете использовать библиотеки Ant Zip из своего java-кода (зависимости maven можно найти здесь). Вот теперь я делаю zip, состоящий из списка файлов и каталогов:

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");
}
person Choppy The Lumberjack    schedule 24.02.2012

Чтобы заархивировать содержимое папки и ее подпапок в Windows,

заменять,

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

с участием

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

Я хотел бы добавить сюда предложение / напоминание:

Если вы определяете выходной каталог, который совпадает с входным, вам нужно будет сравнить имя каждого файла с именем выходного файла .zip, чтобы избежать сжатия файла внутри себя, что может привести к нежелательному поведению. Надеюсь, это поможет.

person Luca Bezerra    schedule 22.11.2011

Этот фрагмент кода работает для меня. Никакой сторонней библиотеки не требуется.

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();
    }
}
person Alessandro Giusa    schedule 16.04.2017