    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,
    133            final EnumSet<FileVisitOption> fileVisitOptions, 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)
    163                throws IOException {
    164                Objects.requireNonNull(path);
    165                Objects.requireNonNull(attrs);
    166                final String name = directory.relativize(path).toString().replace('\\', '/');
    167                if (!name.isEmpty()) {
    168                    final SevenZArchiveEntry archiveEntry = target.createArchiveEntry(path,
    169                        isFile || name.endsWith("/") ? name : name + "/");
    170                    target.putArchiveEntry(archiveEntry);
    171                    if (isFile) {
    172                        // Refactor this as a BiConsumer on Java 8
    173                        target.write(path);
    174                    }
    175                    target.closeArchiveEntry();
    176                }
    177                return FileVisitResult.CONTINUE;
    178            }
    179
    180        });
    181        target.finish();
    182    }
    183
    184    /**
    185     * Creates an archive {@code target} using the format {@code
    186     * format} by recursively including all files and directories in {@code directory}.
    187     *
    188     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    189     * @param target the file to write the new archive to.
    190     * @param directory the directory that contains the files to archive.
    191     * @throws IOException if an I/O error occurs
    192     * @throws ArchiveException if the archive cannot be created for other reasons
    193     */
    194    public void create(final String format, final File target, final File directory)
    195        throws IOException, ArchiveException {
    196        create(format, target.toPath(), directory.toPath());
    197    }
    198
    199    /**
    200     * Creates an archive {@code target} using the format {@code
    201     * format} by recursively including all files and directories in {@code directory}.
    202     *
    203     * <p>
    204     * This method creates a wrapper around the target stream which is never closed and thus leaks resources, please use
    205     * {@link #create(String,OutputStream,File,CloseableConsumer)} instead.
    206     * </p>
    207     *
    208     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    209     * @param target the stream to write the new archive to.
    210     * @param directory the directory that contains the files to archive.
    211     * @throws IOException if an I/O error occurs
    212     * @throws ArchiveException if the archive cannot be created for other reasons
    213     * @deprecated this method leaks resources
    214     */
    215    @Deprecated
    216    public void create(final String format, final OutputStream target, final File directory)
    217        throws IOException, ArchiveException {
    218        create(format, target, directory, CloseableConsumer.NULL_CONSUMER);
    219    }
    220
    221    /**
    222     * Creates an archive {@code target} using the format {@code
    223     * format} by recursively including all files and directories in {@code directory}.
    224     *
    225     * <p>
    226     * This method creates a wrapper around the archive stream and the caller of this method is responsible for closing
    227     * it - probably at the same time as closing the stream itself. The caller is informed about the wrapper object via
    228     * the {@code
    229     * closeableConsumer} callback as soon as it is no longer needed by this class.
    230     * </p>
    231     *
    232     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    233     * @param target the stream to write the new archive to.
    234     * @param directory the directory that contains the files to archive.
    235     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
    236     * @throws IOException if an I/O error occurs
    237     * @throws ArchiveException if the archive cannot be created for other reasons
    238     * @since 1.19
    239     */
    240    public void create(final String format, final OutputStream target, final File directory,
    241        final CloseableConsumer closeableConsumer) throws IOException, ArchiveException {
    242        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
    243            ArchiveOutputStream<? extends ArchiveEntry> archiveOutputStream = ArchiveStreamFactory.DEFAULT.createArchiveOutputStream(format, target);
    244            create(c.track(archiveOutputStream), directory);
    245        }
    246    }
    247
    248    /**
    249     * Creates an archive {@code target} using the format {@code
    250     * format} by recursively including all files and directories in {@code directory}.
    251     *
    252     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    253     * @param target the file to write the new archive to.
    254     * @param directory the directory that contains the files to archive.
    255     * @throws IOException if an I/O error occurs
    256     * @throws ArchiveException if the archive cannot be created for other reasons
    257     * @since 1.21
    258     */
    259    public void create(final String format, final Path target, final Path directory)
    260            throws IOException, ArchiveException {
    261        if (prefersSeekableByteChannel(format)) {
    262            try (SeekableByteChannel channel = FileChannel.open(target, StandardOpenOption.WRITE, StandardOpenOption.CREATE,
    263                    StandardOpenOption.TRUNCATE_EXISTING)) {
    264                create(format, channel, directory);
    265                return;
    266            }
    267        }
    268        try (@SuppressWarnings("resource") // ArchiveOutputStream wraps newOutputStream result
    269        ArchiveOutputStream<?> outputStream = ArchiveStreamFactory.DEFAULT.createArchiveOutputStream(format, Files.newOutputStream(target))) {
    270            create(outputStream, directory, EMPTY_FileVisitOption);
    271        }
    272    }
    273
    274    /**
    275     * Creates an archive {@code target} using the format {@code
    276     * format} by recursively including all files and directories in {@code directory}.
    277     *
    278     * <p>
    279     * This method creates a wrapper around the target channel which is never closed and thus leaks resources, please
    280     * use {@link #create(String,SeekableByteChannel,File,CloseableConsumer)} instead.
    281     * </p>
    282     *
    283     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    284     * @param target the channel to write the new archive to.
    285     * @param directory the directory that contains the files to archive.
    286     * @throws IOException if an I/O error occurs
    287     * @throws ArchiveException if the archive cannot be created for other reasons
    288     * @deprecated this method leaks resources
    289     */
    290    @Deprecated
    291    public void create(final String format, final SeekableByteChannel target, final File directory)
    292        throws IOException, ArchiveException {
    293        create(format, target, directory, CloseableConsumer.NULL_CONSUMER);
    294    }
    295
    296    /**
    297     * Creates an archive {@code target} using the format {@code
    298     * format} by recursively including all files and directories in {@code directory}.
    299     *
    300     * <p>
    301     * This method creates a wrapper around the archive channel and the caller of this method is responsible for closing
    302     * it - probably at the same time as closing the channel itself. The caller is informed about the wrapper object via
    303     * the {@code
    304     * closeableConsumer} callback as soon as it is no longer needed by this class.
    305     * </p>
    306     *
    307     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    308     * @param target the channel to write the new archive to.
    309     * @param directory the directory that contains the files to archive.
    310     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
    311     * @throws IOException if an I/O error occurs
    312     * @throws ArchiveException if the archive cannot be created for other reasons
    313     * @since 1.19
    314     */
    315    public void create(final String format, final SeekableByteChannel target, final File directory,
    316        final CloseableConsumer closeableConsumer) throws IOException, ArchiveException {
    317        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
    318            if (!prefersSeekableByteChannel(format)) {
    319                create(format, c.track(Channels.newOutputStream(target)), directory);
    320            } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
    321                create(c.track(new ZipArchiveOutputStream(target)), directory);
    322            } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
    323                create(c.track(new SevenZOutputFile(target)), directory);
    324            } else {
    325                // never reached as prefersSeekableByteChannel only returns true for ZIP and 7z
    326                throw new ArchiveException("Don't know how to handle format " + format);
    327            }
    328        }
    329    }
    330
    331    /**
    332     * Creates an archive {@code target} using the format {@code
    333     * format} by recursively including all files and directories in {@code directory}.
    334     *
    335     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    336     * @param target the channel to write the new archive to.
    337     * @param directory the directory that contains the files to archive.
    338     * @throws IOException if an I/O error occurs
    339     * @throws IllegalStateException if the format does not support {@code SeekableByteChannel}.
    340     */
    341    public void create(final String format, final SeekableByteChannel target, final Path directory) throws IOException {
    342        if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
    343            try (SevenZOutputFile sevenZFile = new SevenZOutputFile(target)) {
    344                create(sevenZFile, directory);
    345            }
    346        } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
    347            try (ZipArchiveOutputStream archiveOutputStream = new ZipArchiveOutputStream(target)) {
    348                create(archiveOutputStream, directory, EMPTY_FileVisitOption);
    349            }
    350        } else {
    351            throw new IllegalStateException(format);
    352        }
    353    }
    354
    355    private boolean prefersSeekableByteChannel(final String format) {
    356        return ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)
    357            || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format);
    358    }
    359}