    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one
    003 * or more contributor license agreements.  See the NOTICE file
    004 * distributed with this work for additional information
    005 * regarding copyright ownership.  The ASF licenses this file
    006 * to you under the Apache License, Version 2.0 (the
    007 * "License"); you may not use this file except in compliance
    008 * with the License.  You may obtain a copy of the License at
    009 *
    010 * http://www.apache.org/licenses/LICENSE-2.0
    011 *
    012 * Unless required by applicable law or agreed to in writing,
    013 * software distributed under the License is distributed on an
    014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    015 * KIND, either express or implied.  See the License for the
    016 * specific language governing permissions and limitations
    017 * under the License.
    018 */
    019package org.apache.commons.compress.archivers.examples;
    020
    021import java.io.File;
    022import java.io.IOException;
    023import java.io.OutputStream;
    024import java.nio.channels.Channels;
    025import java.nio.channels.FileChannel;
    026import java.nio.channels.SeekableByteChannel;
    027import java.nio.file.FileVisitOption;
    028import java.nio.file.FileVisitResult;
    029import java.nio.file.Files;
    030import java.nio.file.LinkOption;
    031import java.nio.file.Path;
    032import java.nio.file.SimpleFileVisitor;
    033import java.nio.file.StandardOpenOption;
    034import java.nio.file.attribute.BasicFileAttributes;
    035import java.util.EnumSet;
    036import java.util.Objects;
    037
    038import org.apache.commons.compress.archivers.ArchiveEntry;
    039import org.apache.commons.compress.archivers.ArchiveException;
    040import org.apache.commons.compress.archivers.ArchiveOutputStream;
    041import org.apache.commons.compress.archivers.ArchiveStreamFactory;
    042import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry;
    043import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile;
    044import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
    045import org.apache.commons.compress.utils.IOUtils;
    046
    047/**
    048 * Provides a high level API for creating archives.
    049 *
    050 * @since 1.17
    051 * @since 1.21 Supports {@link Path}.
    052 */
    053public class Archiver {
    054
    055    private static class ArchiverFileVisitor<O extends ArchiveOutputStream<E>, E extends ArchiveEntry> extends SimpleFileVisitor<Path> {
    056
    057        private final O target;
    058        private final Path directory;
    059        private final LinkOption[] linkOptions;
    060
    061        private ArchiverFileVisitor(final O target, final Path directory, final LinkOption... linkOptions) {
    062            this.target = target;
    063            this.directory = directory;
    064            this.linkOptions = linkOptions == null ? IOUtils.EMPTY_LINK_OPTIONS : linkOptions.clone();
    065        }
    066
    067        @Override
    068        public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) throws IOException {
    069            return visit(dir, attrs, false);
    070        }
    071
    072        protected FileVisitResult visit(final Path path, final BasicFileAttributes attrs, final boolean isFile) throws IOException {
    073            Objects.requireNonNull(path);
    074            Objects.requireNonNull(attrs);
    075            final String name = directory.relativize(path).toString().replace('\\', '/');
    076            if (!name.isEmpty()) {
    077                final E archiveEntry = target.createArchiveEntry(path, isFile || name.endsWith("/") ? name : name + "/", linkOptions);
    078                target.putArchiveEntry(archiveEntry);
    079                if (isFile) {
    080                    // Refactor this as a BiConsumer on Java 8
    081                    Files.copy(path, target);
    082                }
    083                target.closeArchiveEntry();
    084            }
    085            return FileVisitResult.CONTINUE;
    086        }
    087
    088        @Override
    089        public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
    090            return visit(file, attrs, true);
    091        }
    092    }
    093
    094    /**
    095     * No {@link FileVisitOption}.
    096     */
    097    public static final EnumSet<FileVisitOption> EMPTY_FileVisitOption = EnumSet.noneOf(FileVisitOption.class);
    098
    099    /**
    100     * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
    101     *
    102     * @param target    the stream to write the new archive to.
    103     * @param directory the directory that contains the files to archive.
    104     * @throws IOException if an I/O error occurs
    105     */
    106    public void create(final ArchiveOutputStream<?> target, final File directory) throws IOException {
    107        create(target, directory.toPath(), EMPTY_FileVisitOption);
    108    }
    109
    110    /**
    111     * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
    112     *
    113     * @param target    the stream to write the new archive to.
    114     * @param directory the directory that contains the files to archive.
    115     * @throws IOException if an I/O error occurs or the archive cannot be created for other reasons.
    116     * @since 1.21
    117     */
    118    public void create(final ArchiveOutputStream<?> target, final Path directory) throws IOException {
    119        create(target, directory, EMPTY_FileVisitOption);
    120    }
    121
    122    /**
    123     * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
    124     *
    125     * @param target           the stream to write the new archive to.
    126     * @param directory        the directory that contains the files to archive.
    127     * @param fileVisitOptions linkOptions to configure the traversal of the source {@code directory}.
    128     * @param linkOptions      indicating how symbolic links are handled.
    129     * @throws IOException if an I/O error occurs or the archive cannot be created for other reasons.
    130     * @since 1.21
    131     */
    132    public void create(final ArchiveOutputStream<?> target, final Path directory, final EnumSet<FileVisitOption> fileVisitOptions,
    133            final LinkOption... linkOptions) throws IOException {
    134        Files.walkFileTree(directory, fileVisitOptions, Integer.MAX_VALUE, new ArchiverFileVisitor<>(target, directory, linkOptions));
    135        target.finish();
    136    }
    137
    138    /**
    139     * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
    140     *
    141     * @param target    the file to write the new archive to.
    142     * @param directory the directory that contains the files to archive.
    143     * @throws IOException if an I/O error occurs
    144     */
    145    public void create(final SevenZOutputFile target, final File directory) throws IOException {
    146        create(target, directory.toPath());
    147    }
    148
    149    /**
    150     * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
    151     *
    152     * @param target    the file to write the new archive to.
    153     * @param directory the directory that contains the files to archive.
    154     * @throws IOException if an I/O error occurs
    155     * @since 1.21
    156     */
    157    public void create(final SevenZOutputFile target, final Path directory) throws IOException {
    158        // This custom SimpleFileVisitor goes away with Java 8's BiConsumer.
    159        Files.walkFileTree(directory, new ArchiverFileVisitor(null, directory) {
    160
    161            @Override
    162            protected FileVisitResult visit(final Path path, final BasicFileAttributes attrs, final boolean isFile) throws IOException {
    163                Objects.requireNonNull(path);
    164                Objects.requireNonNull(attrs);
    165                final String name = directory.relativize(path).toString().replace('\\', '/');
    166                if (!name.isEmpty()) {
    167                    final SevenZArchiveEntry archiveEntry = target.createArchiveEntry(path, isFile || name.endsWith("/") ? name : name + "/");
    168                    target.putArchiveEntry(archiveEntry);
    169                    if (isFile) {
    170                        // Refactor this as a BiConsumer on Java 8
    171                        target.write(path);
    172                    }
    173                    target.closeArchiveEntry();
    174                }
    175                return FileVisitResult.CONTINUE;
    176            }
    177
    178        });
    179        target.finish();
    180    }
    181
    182    /**
    183     * Creates an archive {@code target} using the format {@code
    184     * format} by recursively including all files and directories in {@code directory}.
    185     *
    186     * @param format    the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    187     * @param target    the file 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     */
    192    public void create(final String format, final File target, final File directory) throws IOException, ArchiveException {
    193        create(format, target.toPath(), directory.toPath());
    194    }
    195
    196    /**
    197     * Creates an archive {@code target} using the format {@code
    198     * format} by recursively including all files and directories in {@code directory}.
    199     *
    200     * <p>
    201     * This method creates a wrapper around the target stream which is never closed and thus leaks resources, please use
    202     * {@link #create(String,OutputStream,File,CloseableConsumer)} instead.
    203     * </p>
    204     *
    205     * @param format    the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    206     * @param target    the stream to write the new archive to.
    207     * @param directory the directory that contains the files to archive.
    208     * @throws IOException      if an I/O error occurs
    209     * @throws ArchiveException if the archive cannot be created for other reasons
    210     * @deprecated this method leaks resources
    211     */
    212    @Deprecated
    213    public void create(final String format, final OutputStream target, final File directory) throws IOException, ArchiveException {
    214        create(format, target, directory, CloseableConsumer.NULL_CONSUMER);
    215    }
    216
    217    /**
    218     * Creates an archive {@code target} using the format {@code
    219     * format} by recursively including all files and directories in {@code directory}.
    220     *
    221     * <p>
    222     * 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
    223     * closing the stream itself. The caller is informed about the wrapper object via the {@code
    224     * closeableConsumer} callback as soon as it is no longer needed by this class.
    225     * </p>
    226     *
    227     * @param format            the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    228     * @param target            the stream to write the new archive to.
    229     * @param directory         the directory that contains the files to archive.
    230     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
    231     * @throws IOException      if an I/O error occurs
    232     * @throws ArchiveException if the archive cannot be created for other reasons
    233     * @since 1.19
    234     */
    235    public void create(final String format, final OutputStream target, final File directory, final CloseableConsumer closeableConsumer)
    236            throws IOException, ArchiveException {
    237        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
    238            final ArchiveOutputStream<? extends ArchiveEntry> archiveOutputStream = ArchiveStreamFactory.DEFAULT.createArchiveOutputStream(format, target);
    239            create(c.track(archiveOutputStream), directory);
    240        }
    241    }
    242
    243    /**
    244     * Creates an archive {@code target} using the format {@code
    245     * format} by recursively including all files and directories in {@code directory}.
    246     *
    247     * @param format    the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    248     * @param target    the file to write the new archive to.
    249     * @param directory the directory that contains the files to archive.
    250     * @throws IOException      if an I/O error occurs
    251     * @throws ArchiveException if the archive cannot be created for other reasons
    252     * @since 1.21
    253     */
    254    public void create(final String format, final Path target, final Path directory) throws IOException, ArchiveException {
    255        if (prefersSeekableByteChannel(format)) {
    256            try (SeekableByteChannel channel = FileChannel.open(target, StandardOpenOption.WRITE, StandardOpenOption.CREATE,
    257                    StandardOpenOption.TRUNCATE_EXISTING)) {
    258                create(format, channel, directory);
    259                return;
    260            }
    261        }
    262        try (@SuppressWarnings("resource") // ArchiveOutputStream wraps newOutputStream result
    263        ArchiveOutputStream<?> outputStream = ArchiveStreamFactory.DEFAULT.createArchiveOutputStream(format, Files.newOutputStream(target))) {
    264            create(outputStream, directory, EMPTY_FileVisitOption);
    265        }
    266    }
    267
    268    /**
    269     * Creates an archive {@code target} using the format {@code
    270     * format} by recursively including all files and directories in {@code directory}.
    271     *
    272     * <p>
    273     * This method creates a wrapper around the target channel which is never closed and thus leaks resources, please use
    274     * {@link #create(String,SeekableByteChannel,File,CloseableConsumer)} instead.
    275     * </p>
    276     *
    277     * @param format    the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    278     * @param target    the channel to write the new archive to.
    279     * @param directory the directory that contains the files to archive.
    280     * @throws IOException      if an I/O error occurs
    281     * @throws ArchiveException if the archive cannot be created for other reasons
    282     * @deprecated this method leaks resources
    283     */
    284    @Deprecated
    285    public void create(final String format, final SeekableByteChannel target, final File directory) throws IOException, ArchiveException {
    286        create(format, target, directory, CloseableConsumer.NULL_CONSUMER);
    287    }
    288
    289    /**
    290     * Creates an archive {@code target} using the format {@code
    291     * format} by recursively including all files and directories in {@code directory}.
    292     *
    293     * <p>
    294     * 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
    295     * closing the channel itself. The caller is informed about the wrapper object via the {@code
    296     * closeableConsumer} callback as soon as it is no longer needed by this class.
    297     * </p>
    298     *
    299     * @param format            the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    300     * @param target            the channel to write the new archive to.
    301     * @param directory         the directory that contains the files to archive.
    302     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
    303     * @throws IOException      if an I/O error occurs
    304     * @throws ArchiveException if the archive cannot be created for other reasons
    305     * @since 1.19
    306     */
    307    public void create(final String format, final SeekableByteChannel target, final File directory, final CloseableConsumer closeableConsumer)
    308            throws IOException, ArchiveException {
    309        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
    310            if (!prefersSeekableByteChannel(format)) {
    311                create(format, c.track(Channels.newOutputStream(target)), directory);
    312            } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
    313                create(c.track(new ZipArchiveOutputStream(target)), directory);
    314            } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
    315                create(c.track(new SevenZOutputFile(target)), directory);
    316            } else {
    317                // never reached as prefersSeekableByteChannel only returns true for ZIP and 7z
    318                throw new ArchiveException("Don't know how to handle format " + format);
    319            }
    320        }
    321    }
    322
    323    /**
    324     * Creates an archive {@code target} using the format {@code
    325     * format} by recursively including all files and directories in {@code directory}.
    326     *
    327     * @param format    the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    328     * @param target    the channel to write the new archive to.
    329     * @param directory the directory that contains the files to archive.
    330     * @throws IOException           if an I/O error occurs
    331     * @throws IllegalStateException if the format does not support {@code SeekableByteChannel}.
    332     */
    333    public void create(final String format, final SeekableByteChannel target, final Path directory) throws IOException {
    334        if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
    335            try (SevenZOutputFile sevenZFile = new SevenZOutputFile(target)) {
    336                create(sevenZFile, directory);
    337            }
    338        } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
    339            try (ZipArchiveOutputStream archiveOutputStream = new ZipArchiveOutputStream(target)) {
    340                create(archiveOutputStream, directory, EMPTY_FileVisitOption);
    341            }
    342        } else {
    343            throw new IllegalStateException(format);
    344        }
    345    }
    346
    347    private boolean prefersSeekableByteChannel(final String format) {
    348        return ArchiveStreamFactory.ZIP.equalsIgnoreCase(format) || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format);
    349    }
    350}