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