    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 *   https://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 outputStream;
    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.outputStream = 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 = outputStream.createArchiveEntry(path, isFile || name.endsWith("/") ? name : name + "/", linkOptions);
    078                outputStream.putArchiveEntry(archiveEntry);
    079                if (isFile) {
    080                    // Refactor this as a BiConsumer on Java 8?
    081                    outputStream.write(path);
    082                }
    083                outputStream.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     * Constructs a new instance.
    101     */
    102    public Archiver() {
    103        // empty
    104    }
    105
    106    /**
    107     * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
    108     *
    109     * @param target    the stream to write the new archive to.
    110     * @param directory the directory that contains the files to archive.
    111     * @throws IOException if an I/O error occurs
    112     */
    113    public void create(final ArchiveOutputStream<?> target, final File directory) throws IOException {
    114        create(target, directory.toPath(), EMPTY_FileVisitOption);
    115    }
    116
    117    /**
    118     * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
    119     *
    120     * @param target    the stream to write the new archive to.
    121     * @param directory the directory that contains the files to archive.
    122     * @throws IOException if an I/O error occurs or the archive cannot be created for other reasons.
    123     * @since 1.21
    124     */
    125    public void create(final ArchiveOutputStream<?> target, final Path directory) throws IOException {
    126        create(target, directory, EMPTY_FileVisitOption);
    127    }
    128
    129    /**
    130     * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
    131     *
    132     * @param target           the stream to write the new archive to.
    133     * @param directory        the directory that contains the files to archive.
    134     * @param fileVisitOptions linkOptions to configure the traversal of the source {@code directory}.
    135     * @param linkOptions      indicating how symbolic links are handled.
    136     * @throws IOException if an I/O error occurs or the archive cannot be created for other reasons.
    137     * @since 1.21
    138     */
    139    public void create(final ArchiveOutputStream<?> target, final Path directory, final EnumSet<FileVisitOption> fileVisitOptions,
    140            final LinkOption... linkOptions) throws IOException {
    141        Files.walkFileTree(directory, fileVisitOptions, Integer.MAX_VALUE, new ArchiverFileVisitor<>(target, directory, linkOptions));
    142        target.finish();
    143    }
    144
    145    /**
    146     * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
    147     *
    148     * @param target    the file to write the new archive to.
    149     * @param directory the directory that contains the files to archive.
    150     * @throws IOException if an I/O error occurs
    151     */
    152    public void create(final SevenZOutputFile target, final File directory) throws IOException {
    153        create(target, directory.toPath());
    154    }
    155
    156    /**
    157     * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
    158     *
    159     * @param target    the file to write the new archive to.
    160     * @param directory the directory that contains the files to archive.
    161     * @throws IOException if an I/O error occurs
    162     * @since 1.21
    163     */
    164    public void create(final SevenZOutputFile target, final Path directory) throws IOException {
    165        // This custom SimpleFileVisitor goes away with Java 8's BiConsumer.
    166        Files.walkFileTree(directory, new ArchiverFileVisitor<ArchiveOutputStream<ArchiveEntry>, ArchiveEntry>(null, directory) {
    167
    168            @Override
    169            protected FileVisitResult visit(final Path path, final BasicFileAttributes attrs, final boolean isFile) throws IOException {
    170                Objects.requireNonNull(path);
    171                Objects.requireNonNull(attrs);
    172                final String name = directory.relativize(path).toString().replace('\\', '/');
    173                if (!name.isEmpty()) {
    174                    final SevenZArchiveEntry archiveEntry = target.createArchiveEntry(path, isFile || name.endsWith("/") ? name : name + "/");
    175                    target.putArchiveEntry(archiveEntry);
    176                    if (isFile) {
    177                        // Refactor this as a BiConsumer on Java 8
    178                        target.write(path);
    179                    }
    180                    target.closeArchiveEntry();
    181                }
    182                return FileVisitResult.CONTINUE;
    183            }
    184
    185        });
    186        target.finish();
    187    }
    188
    189    /**
    190     * Creates an archive {@code target} using the format {@code
    191     * format} by recursively including all files and directories in {@code directory}.
    192     *
    193     * @param format    the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    194     * @param target    the file to write the new archive to.
    195     * @param directory the directory that contains the files to archive.
    196     * @throws IOException      if an I/O error occurs
    197     * @throws ArchiveException if the archive cannot be created for other reasons
    198     */
    199    public void create(final String format, final File target, final File directory) throws IOException, ArchiveException {
    200        create(format, target.toPath(), directory.toPath());
    201    }
    202
    203    /**
    204     * Creates an archive {@code target} using the format {@code
    205     * format} by recursively including all files and directories in {@code directory}.
    206     *
    207     * <p>
    208     * This method creates a wrapper around the target stream which is never closed and thus leaks resources, please use
    209     * {@link #create(String,OutputStream,File,CloseableConsumer)} instead.
    210     * </p>
    211     *
    212     * @param format    the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    213     * @param target    the stream to write the new archive to.
    214     * @param directory the directory that contains the files to archive.
    215     * @throws IOException      if an I/O error occurs
    216     * @throws ArchiveException if the archive cannot be created for other reasons
    217     * @deprecated this method leaks resources
    218     */
    219    @Deprecated
    220    public void create(final String format, final OutputStream target, final File directory) throws IOException, ArchiveException {
    221        create(format, target, directory, CloseableConsumer.NULL_CONSUMER);
    222    }
    223
    224    /**
    225     * Creates an archive {@code target} using the format {@code
    226     * format} by recursively including all files and directories in {@code directory}.
    227     *
    228     * <p>
    229     * 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
    230     * closing the stream itself. The caller is informed about the wrapper object via the {@code
    231     * closeableConsumer} callback as soon as it is no longer needed by this class.
    232     * </p>
    233     *
    234     * @param format            the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    235     * @param target            the stream to write the new archive to.
    236     * @param directory         the directory that contains the files to archive.
    237     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
    238     * @throws IOException      if an I/O error occurs
    239     * @throws ArchiveException if the archive cannot be created for other reasons
    240     * @since 1.19
    241     */
    242    public void create(final String format, final OutputStream target, final File directory, final CloseableConsumer closeableConsumer)
    243            throws IOException, ArchiveException {
    244        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
    245            final ArchiveOutputStream<? extends ArchiveEntry> archiveOutputStream = ArchiveStreamFactory.DEFAULT.createArchiveOutputStream(format, target);
    246            create(c.track(archiveOutputStream), directory);
    247        }
    248    }
    249
    250    /**
    251     * Creates an archive {@code target} using the format {@code
    252     * format} by recursively including all files and directories in {@code directory}.
    253     *
    254     * @param format    the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    255     * @param target    the file to write the new archive to.
    256     * @param directory the directory that contains the files to archive.
    257     * @throws IOException      if an I/O error occurs
    258     * @throws ArchiveException if the archive cannot be created for other reasons
    259     * @since 1.21
    260     */
    261    public void create(final String format, final Path target, final Path directory) throws IOException, ArchiveException {
    262        if (prefersSeekableByteChannel(format)) {
    263            try (SeekableByteChannel channel = FileChannel.open(target, StandardOpenOption.WRITE, StandardOpenOption.CREATE,
    264                    StandardOpenOption.TRUNCATE_EXISTING)) {
    265                create(format, channel, directory);
    266                return;
    267            }
    268        }
    269        try (@SuppressWarnings("resource") // ArchiveOutputStream wraps newOutputStream result
    270        ArchiveOutputStream<?> outputStream = ArchiveStreamFactory.DEFAULT.createArchiveOutputStream(format, Files.newOutputStream(target))) {
    271            create(outputStream, directory, EMPTY_FileVisitOption);
    272        }
    273    }
    274
    275    /**
    276     * Creates an archive {@code target} using the format {@code
    277     * format} by recursively including all files and directories in {@code directory}.
    278     *
    279     * <p>
    280     * This method creates a wrapper around the target channel which is never closed and thus leaks resources, please use
    281     * {@link #create(String,SeekableByteChannel,File,CloseableConsumer)} instead.
    282     * </p>
    283     *
    284     * @param format    the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    285     * @param target    the channel to write the new archive to.
    286     * @param directory the directory that contains the files to archive.
    287     * @throws IOException      if an I/O error occurs
    288     * @throws ArchiveException if the archive cannot be created for other reasons
    289     * @deprecated this method leaks resources
    290     */
    291    @Deprecated
    292    public void create(final String format, final SeekableByteChannel target, final File directory) 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 it - probably at the same time as
    302     * closing the channel itself. The caller is informed about the wrapper object via the {@code
    303     * closeableConsumer} callback as soon as it is no longer needed by this class.
    304     * </p>
    305     *
    306     * @param format            the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    307     * @param target            the channel to write the new archive to.
    308     * @param directory         the directory that contains the files to archive.
    309     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
    310     * @throws IOException      if an I/O error occurs
    311     * @throws ArchiveException if the archive cannot be created for other reasons
    312     * @since 1.19
    313     */
    314    public void create(final String format, final SeekableByteChannel target, final File directory, final CloseableConsumer closeableConsumer)
    315            throws IOException, ArchiveException {
    316        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
    317            if (!prefersSeekableByteChannel(format)) {
    318                create(format, c.track(Channels.newOutputStream(target)), directory);
    319            } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
    320                create(c.track(new ZipArchiveOutputStream(target)), directory);
    321            } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
    322                create(c.track(new SevenZOutputFile(target)), directory);
    323            } else {
    324                // never reached as prefersSeekableByteChannel only returns true for ZIP and 7z
    325                throw new ArchiveException("Don't know how to handle format " + format);
    326            }
    327        }
    328    }
    329
    330    /**
    331     * Creates an archive {@code target} using the format {@code
    332     * format} by recursively including all files and directories in {@code directory}.
    333     *
    334     * @param format    the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    335     * @param target    the channel to write the new archive to.
    336     * @param directory the directory that contains the files to archive.
    337     * @throws IOException           if an I/O error occurs
    338     * @throws IllegalStateException if the format does not support {@code SeekableByteChannel}.
    339     */
    340    public void create(final String format, final SeekableByteChannel target, final Path directory) throws IOException {
    341        if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
    342            try (SevenZOutputFile sevenZFile = new SevenZOutputFile(target)) {
    343                create(sevenZFile, directory);
    344            }
    345        } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
    346            try (ZipArchiveOutputStream archiveOutputStream = new ZipArchiveOutputStream(target)) {
    347                create(archiveOutputStream, directory, EMPTY_FileVisitOption);
    348            }
    349        } else {
    350            throw new IllegalStateException(format);
    351        }
    352    }
    353
    354    private boolean prefersSeekableByteChannel(final String format) {
    355        return ArchiveStreamFactory.ZIP.equalsIgnoreCase(format) || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format);
    356    }
    357}