Archiver.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one
  3.  * or more contributor license agreements.  See the NOTICE file
  4.  * distributed with this work for additional information
  5.  * regarding copyright ownership.  The ASF licenses this file
  6.  * to you under the Apache License, Version 2.0 (the
  7.  * "License"); you may not use this file except in compliance
  8.  * with the License.  You may obtain a copy of the License at
  9.  *
  10.  * http://www.apache.org/licenses/LICENSE-2.0
  11.  *
  12.  * Unless required by applicable law or agreed to in writing,
  13.  * software distributed under the License is distributed on an
  14.  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15.  * KIND, either express or implied.  See the License for the
  16.  * specific language governing permissions and limitations
  17.  * under the License.
  18.  */
  19. package org.apache.commons.compress.archivers.examples;

  20. import java.io.File;
  21. import java.io.IOException;
  22. import java.io.OutputStream;
  23. import java.nio.channels.Channels;
  24. import java.nio.channels.FileChannel;
  25. import java.nio.channels.SeekableByteChannel;
  26. import java.nio.file.FileVisitOption;
  27. import java.nio.file.FileVisitResult;
  28. import java.nio.file.Files;
  29. import java.nio.file.LinkOption;
  30. import java.nio.file.Path;
  31. import java.nio.file.SimpleFileVisitor;
  32. import java.nio.file.StandardOpenOption;
  33. import java.nio.file.attribute.BasicFileAttributes;
  34. import java.util.EnumSet;
  35. import java.util.Objects;

  36. import org.apache.commons.compress.archivers.ArchiveEntry;
  37. import org.apache.commons.compress.archivers.ArchiveException;
  38. import org.apache.commons.compress.archivers.ArchiveOutputStream;
  39. import org.apache.commons.compress.archivers.ArchiveStreamFactory;
  40. import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry;
  41. import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile;
  42. import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
  43. import org.apache.commons.compress.utils.IOUtils;

  44. /**
  45.  * Provides a high level API for creating archives.
  46.  *
  47.  * @since 1.17
  48.  * @since 1.21 Supports {@link Path}.
  49.  */
  50. public class Archiver {

  51.     private static class ArchiverFileVisitor<O extends ArchiveOutputStream<E>, E extends ArchiveEntry> extends SimpleFileVisitor<Path> {

  52.         private final O target;
  53.         private final Path directory;
  54.         private final LinkOption[] linkOptions;

  55.         private ArchiverFileVisitor(final O target, final Path directory, final LinkOption... linkOptions) {
  56.             this.target = target;
  57.             this.directory = directory;
  58.             this.linkOptions = linkOptions == null ? IOUtils.EMPTY_LINK_OPTIONS : linkOptions.clone();
  59.         }

  60.         @Override
  61.         public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) throws IOException {
  62.             return visit(dir, attrs, false);
  63.         }

  64.         protected FileVisitResult visit(final Path path, final BasicFileAttributes attrs, final boolean isFile) throws IOException {
  65.             Objects.requireNonNull(path);
  66.             Objects.requireNonNull(attrs);
  67.             final String name = directory.relativize(path).toString().replace('\\', '/');
  68.             if (!name.isEmpty()) {
  69.                 final E archiveEntry = target.createArchiveEntry(path, isFile || name.endsWith("/") ? name : name + "/", linkOptions);
  70.                 target.putArchiveEntry(archiveEntry);
  71.                 if (isFile) {
  72.                     // Refactor this as a BiConsumer on Java 8
  73.                     Files.copy(path, target);
  74.                 }
  75.                 target.closeArchiveEntry();
  76.             }
  77.             return FileVisitResult.CONTINUE;
  78.         }

  79.         @Override
  80.         public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
  81.             return visit(file, attrs, true);
  82.         }
  83.     }

  84.     /**
  85.      * No {@link FileVisitOption}.
  86.      */
  87.     public static final EnumSet<FileVisitOption> EMPTY_FileVisitOption = EnumSet.noneOf(FileVisitOption.class);

  88.     /**
  89.      * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
  90.      *
  91.      * @param target    the stream to write the new archive to.
  92.      * @param directory the directory that contains the files to archive.
  93.      * @throws IOException if an I/O error occurs
  94.      */
  95.     public void create(final ArchiveOutputStream<?> target, final File directory) throws IOException {
  96.         create(target, directory.toPath(), EMPTY_FileVisitOption);
  97.     }

  98.     /**
  99.      * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
  100.      *
  101.      * @param target    the stream to write the new archive to.
  102.      * @param directory the directory that contains the files to archive.
  103.      * @throws IOException if an I/O error occurs or the archive cannot be created for other reasons.
  104.      * @since 1.21
  105.      */
  106.     public void create(final ArchiveOutputStream<?> target, final Path directory) throws IOException {
  107.         create(target, directory, EMPTY_FileVisitOption);
  108.     }

  109.     /**
  110.      * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
  111.      *
  112.      * @param target           the stream to write the new archive to.
  113.      * @param directory        the directory that contains the files to archive.
  114.      * @param fileVisitOptions linkOptions to configure the traversal of the source {@code directory}.
  115.      * @param linkOptions      indicating how symbolic links are handled.
  116.      * @throws IOException if an I/O error occurs or the archive cannot be created for other reasons.
  117.      * @since 1.21
  118.      */
  119.     public void create(final ArchiveOutputStream<?> target, final Path directory, final EnumSet<FileVisitOption> fileVisitOptions,
  120.             final LinkOption... linkOptions) throws IOException {
  121.         Files.walkFileTree(directory, fileVisitOptions, Integer.MAX_VALUE, new ArchiverFileVisitor<>(target, directory, linkOptions));
  122.         target.finish();
  123.     }

  124.     /**
  125.      * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
  126.      *
  127.      * @param target    the file to write the new archive to.
  128.      * @param directory the directory that contains the files to archive.
  129.      * @throws IOException if an I/O error occurs
  130.      */
  131.     public void create(final SevenZOutputFile target, final File directory) throws IOException {
  132.         create(target, directory.toPath());
  133.     }

  134.     /**
  135.      * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
  136.      *
  137.      * @param target    the file to write the new archive to.
  138.      * @param directory the directory that contains the files to archive.
  139.      * @throws IOException if an I/O error occurs
  140.      * @since 1.21
  141.      */
  142.     public void create(final SevenZOutputFile target, final Path directory) throws IOException {
  143.         // This custom SimpleFileVisitor goes away with Java 8's BiConsumer.
  144.         Files.walkFileTree(directory, new ArchiverFileVisitor<ArchiveOutputStream<ArchiveEntry>, ArchiveEntry>(null, directory) {

  145.             @Override
  146.             protected FileVisitResult visit(final Path path, final BasicFileAttributes attrs, final boolean isFile) throws IOException {
  147.                 Objects.requireNonNull(path);
  148.                 Objects.requireNonNull(attrs);
  149.                 final String name = directory.relativize(path).toString().replace('\\', '/');
  150.                 if (!name.isEmpty()) {
  151.                     final SevenZArchiveEntry archiveEntry = target.createArchiveEntry(path, isFile || name.endsWith("/") ? name : name + "/");
  152.                     target.putArchiveEntry(archiveEntry);
  153.                     if (isFile) {
  154.                         // Refactor this as a BiConsumer on Java 8
  155.                         target.write(path);
  156.                     }
  157.                     target.closeArchiveEntry();
  158.                 }
  159.                 return FileVisitResult.CONTINUE;
  160.             }

  161.         });
  162.         target.finish();
  163.     }

  164.     /**
  165.      * Creates an archive {@code target} using the format {@code
  166.      * format} by recursively including all files and directories in {@code directory}.
  167.      *
  168.      * @param format    the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
  169.      * @param target    the file to write the new archive to.
  170.      * @param directory the directory that contains the files to archive.
  171.      * @throws IOException      if an I/O error occurs
  172.      * @throws ArchiveException if the archive cannot be created for other reasons
  173.      */
  174.     public void create(final String format, final File target, final File directory) throws IOException, ArchiveException {
  175.         create(format, target.toPath(), directory.toPath());
  176.     }

  177.     /**
  178.      * Creates an archive {@code target} using the format {@code
  179.      * format} by recursively including all files and directories in {@code directory}.
  180.      *
  181.      * <p>
  182.      * This method creates a wrapper around the target stream which is never closed and thus leaks resources, please use
  183.      * {@link #create(String,OutputStream,File,CloseableConsumer)} instead.
  184.      * </p>
  185.      *
  186.      * @param format    the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
  187.      * @param target    the stream to write the new archive to.
  188.      * @param directory the directory that contains the files to archive.
  189.      * @throws IOException      if an I/O error occurs
  190.      * @throws ArchiveException if the archive cannot be created for other reasons
  191.      * @deprecated this method leaks resources
  192.      */
  193.     @Deprecated
  194.     public void create(final String format, final OutputStream target, final File directory) throws IOException, ArchiveException {
  195.         create(format, target, directory, CloseableConsumer.NULL_CONSUMER);
  196.     }

  197.     /**
  198.      * Creates an archive {@code target} using the format {@code
  199.      * format} by recursively including all files and directories in {@code directory}.
  200.      *
  201.      * <p>
  202.      * This method creates a wrapper around the archive stream and the caller of this method is responsible for closing it - probably at the same time as
  203.      * closing the stream itself. The caller is informed about the wrapper object via the {@code
  204.      * closeableConsumer} callback as soon as it is no longer needed by this class.
  205.      * </p>
  206.      *
  207.      * @param format            the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
  208.      * @param target            the stream to write the new archive to.
  209.      * @param directory         the directory that contains the files to archive.
  210.      * @param closeableConsumer is informed about the stream wrapped around the passed in stream
  211.      * @throws IOException      if an I/O error occurs
  212.      * @throws ArchiveException if the archive cannot be created for other reasons
  213.      * @since 1.19
  214.      */
  215.     public void create(final String format, final OutputStream target, final File directory, final CloseableConsumer closeableConsumer)
  216.             throws IOException, ArchiveException {
  217.         try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
  218.             final ArchiveOutputStream<? extends ArchiveEntry> archiveOutputStream = ArchiveStreamFactory.DEFAULT.createArchiveOutputStream(format, target);
  219.             create(c.track(archiveOutputStream), directory);
  220.         }
  221.     }

  222.     /**
  223.      * Creates an archive {@code target} using the format {@code
  224.      * format} by recursively including all files and directories in {@code directory}.
  225.      *
  226.      * @param format    the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
  227.      * @param target    the file to write the new archive to.
  228.      * @param directory the directory that contains the files to archive.
  229.      * @throws IOException      if an I/O error occurs
  230.      * @throws ArchiveException if the archive cannot be created for other reasons
  231.      * @since 1.21
  232.      */
  233.     public void create(final String format, final Path target, final Path directory) throws IOException, ArchiveException {
  234.         if (prefersSeekableByteChannel(format)) {
  235.             try (SeekableByteChannel channel = FileChannel.open(target, StandardOpenOption.WRITE, StandardOpenOption.CREATE,
  236.                     StandardOpenOption.TRUNCATE_EXISTING)) {
  237.                 create(format, channel, directory);
  238.                 return;
  239.             }
  240.         }
  241.         try (@SuppressWarnings("resource") // ArchiveOutputStream wraps newOutputStream result
  242.         ArchiveOutputStream<?> outputStream = ArchiveStreamFactory.DEFAULT.createArchiveOutputStream(format, Files.newOutputStream(target))) {
  243.             create(outputStream, directory, EMPTY_FileVisitOption);
  244.         }
  245.     }

  246.     /**
  247.      * Creates an archive {@code target} using the format {@code
  248.      * format} by recursively including all files and directories in {@code directory}.
  249.      *
  250.      * <p>
  251.      * This method creates a wrapper around the target channel which is never closed and thus leaks resources, please use
  252.      * {@link #create(String,SeekableByteChannel,File,CloseableConsumer)} instead.
  253.      * </p>
  254.      *
  255.      * @param format    the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
  256.      * @param target    the channel to write the new archive to.
  257.      * @param directory the directory that contains the files to archive.
  258.      * @throws IOException      if an I/O error occurs
  259.      * @throws ArchiveException if the archive cannot be created for other reasons
  260.      * @deprecated this method leaks resources
  261.      */
  262.     @Deprecated
  263.     public void create(final String format, final SeekableByteChannel target, final File directory) throws IOException, ArchiveException {
  264.         create(format, target, directory, CloseableConsumer.NULL_CONSUMER);
  265.     }

  266.     /**
  267.      * Creates an archive {@code target} using the format {@code
  268.      * format} by recursively including all files and directories in {@code directory}.
  269.      *
  270.      * <p>
  271.      * This method creates a wrapper around the archive channel and the caller of this method is responsible for closing it - probably at the same time as
  272.      * closing the channel itself. The caller is informed about the wrapper object via the {@code
  273.      * closeableConsumer} callback as soon as it is no longer needed by this class.
  274.      * </p>
  275.      *
  276.      * @param format            the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
  277.      * @param target            the channel to write the new archive to.
  278.      * @param directory         the directory that contains the files to archive.
  279.      * @param closeableConsumer is informed about the stream wrapped around the passed in stream
  280.      * @throws IOException      if an I/O error occurs
  281.      * @throws ArchiveException if the archive cannot be created for other reasons
  282.      * @since 1.19
  283.      */
  284.     public void create(final String format, final SeekableByteChannel target, final File directory, final CloseableConsumer closeableConsumer)
  285.             throws IOException, ArchiveException {
  286.         try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
  287.             if (!prefersSeekableByteChannel(format)) {
  288.                 create(format, c.track(Channels.newOutputStream(target)), directory);
  289.             } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
  290.                 create(c.track(new ZipArchiveOutputStream(target)), directory);
  291.             } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
  292.                 create(c.track(new SevenZOutputFile(target)), directory);
  293.             } else {
  294.                 // never reached as prefersSeekableByteChannel only returns true for ZIP and 7z
  295.                 throw new ArchiveException("Don't know how to handle format " + format);
  296.             }
  297.         }
  298.     }

  299.     /**
  300.      * Creates an archive {@code target} using the format {@code
  301.      * format} by recursively including all files and directories in {@code directory}.
  302.      *
  303.      * @param format    the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
  304.      * @param target    the channel to write the new archive to.
  305.      * @param directory the directory that contains the files to archive.
  306.      * @throws IOException           if an I/O error occurs
  307.      * @throws IllegalStateException if the format does not support {@code SeekableByteChannel}.
  308.      */
  309.     public void create(final String format, final SeekableByteChannel target, final Path directory) throws IOException {
  310.         if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
  311.             try (SevenZOutputFile sevenZFile = new SevenZOutputFile(target)) {
  312.                 create(sevenZFile, directory);
  313.             }
  314.         } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
  315.             try (ZipArchiveOutputStream archiveOutputStream = new ZipArchiveOutputStream(target)) {
  316.                 create(archiveOutputStream, directory, EMPTY_FileVisitOption);
  317.             }
  318.         } else {
  319.             throw new IllegalStateException(format);
  320.         }
  321.     }

  322.     private boolean prefersSeekableByteChannel(final String format) {
  323.         return ArchiveStreamFactory.ZIP.equalsIgnoreCase(format) || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format);
  324.     }
  325. }