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