    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.BufferedInputStream;
    022import java.io.File;
    023import java.io.IOException;
    024import java.io.InputStream;
    025import java.io.OutputStream;
    026import java.nio.channels.Channels;
    027import java.nio.channels.FileChannel;
    028import java.nio.channels.SeekableByteChannel;
    029import java.nio.file.Files;
    030import java.nio.file.Path;
    031import java.nio.file.StandardOpenOption;
    032import java.util.Enumeration;
    033import java.util.Iterator;
    034
    035import org.apache.commons.compress.archivers.ArchiveEntry;
    036import org.apache.commons.compress.archivers.ArchiveException;
    037import org.apache.commons.compress.archivers.ArchiveInputStream;
    038import org.apache.commons.compress.archivers.ArchiveStreamFactory;
    039import org.apache.commons.compress.archivers.sevenz.SevenZFile;
    040import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
    041import org.apache.commons.compress.archivers.tar.TarFile;
    042import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
    043import org.apache.commons.compress.archivers.zip.ZipFile;
    044import org.apache.commons.io.IOUtils;
    045import org.apache.commons.io.output.NullOutputStream;
    046
    047/**
    048 * Provides a high level API for expanding archives.
    049 *
    050 * @since 1.17
    051 */
    052public class Expander {
    053
    054    @FunctionalInterface
    055    private interface ArchiveEntryBiConsumer<T extends ArchiveEntry> {
    056        void accept(T entry, OutputStream out) throws IOException;
    057    }
    058
    059    @FunctionalInterface
    060    private interface ArchiveEntrySupplier<T extends ArchiveEntry> {
    061        T get() throws IOException;
    062    }
    063
    064    /**
    065     * @param targetDirectory May be null to simulate output to dev/null on Linux and NUL on Windows.
    066     */
    067    private <T extends ArchiveEntry> void expand(final ArchiveEntrySupplier<T> supplier, final ArchiveEntryBiConsumer<T> writer, final Path targetDirectory)
    068            throws IOException {
    069        final boolean nullTarget = targetDirectory == null;
    070        final Path targetDirPath = nullTarget ? null : targetDirectory.normalize();
    071        T nextEntry = supplier.get();
    072        while (nextEntry != null) {
    073            final Path targetPath = nullTarget ? null : nextEntry.resolveIn(targetDirPath);
    074            if (nextEntry.isDirectory()) {
    075                if (!nullTarget && !Files.isDirectory(targetPath) && Files.createDirectories(targetPath) == null) {
    076                    throw new IOException("Failed to create directory " + targetPath);
    077                }
    078            } else {
    079                final Path parent = nullTarget ? null : targetPath.getParent();
    080                if (!nullTarget && !Files.isDirectory(parent) && Files.createDirectories(parent) == null) {
    081                    throw new IOException("Failed to create directory " + parent);
    082                }
    083                if (nullTarget) {
    084                    writer.accept(nextEntry, NullOutputStream.INSTANCE);
    085                } else {
    086                    try (OutputStream outputStream = Files.newOutputStream(targetPath)) {
    087                        writer.accept(nextEntry, outputStream);
    088                    }
    089                }
    090            }
    091            nextEntry = supplier.get();
    092        }
    093    }
    094
    095    /**
    096     * Expands {@code archive} into {@code targetDirectory}.
    097     *
    098     * @param archive         the file to expand
    099     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    100     * @throws IOException if an I/O error occurs
    101     */
    102    public void expand(final ArchiveInputStream<?> archive, final File targetDirectory) throws IOException {
    103        expand(archive, toPath(targetDirectory));
    104    }
    105
    106    /**
    107     * Expands {@code archive} into {@code targetDirectory}.
    108     *
    109     * @param archive         the file to expand
    110     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    111     * @throws IOException if an I/O error occurs
    112     * @since 1.22
    113     */
    114    public void expand(final ArchiveInputStream<?> archive, final Path targetDirectory) throws IOException {
    115        expand(() -> {
    116            ArchiveEntry next = archive.getNextEntry();
    117            while (next != null && !archive.canReadEntryData(next)) {
    118                next = archive.getNextEntry();
    119            }
    120            return next;
    121        }, (entry, out) -> IOUtils.copy(archive, out), targetDirectory);
    122    }
    123
    124    /**
    125     * Expands {@code archive} into {@code targetDirectory}.
    126     *
    127     * <p>
    128     * Tries to auto-detect the archive's format.
    129     * </p>
    130     *
    131     * @param archive         the file to expand
    132     * @param targetDirectory the target directory
    133     * @throws IOException      if an I/O error occurs
    134     * @throws ArchiveException if the archive cannot be read for other reasons
    135     */
    136    public void expand(final File archive, final File targetDirectory) throws IOException, ArchiveException {
    137        expand(archive.toPath(), toPath(targetDirectory));
    138    }
    139
    140    /**
    141     * Expands {@code archive} into {@code targetDirectory}.
    142     *
    143     * <p>
    144     * Tries to auto-detect the archive's format.
    145     * </p>
    146     *
    147     * <p>
    148     * This method creates a wrapper around the archive stream which is never closed and thus leaks resources, please use
    149     * {@link #expand(InputStream,File,CloseableConsumer)} instead.
    150     * </p>
    151     *
    152     * @param archive         the file to expand
    153     * @param targetDirectory the target directory
    154     * @throws IOException      if an I/O error occurs
    155     * @throws ArchiveException if the archive cannot be read for other reasons
    156     * @deprecated this method leaks resources
    157     */
    158    @Deprecated
    159    public void expand(final InputStream archive, final File targetDirectory) throws IOException, ArchiveException {
    160        expand(archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
    161    }
    162
    163    /**
    164     * Expands {@code archive} into {@code targetDirectory}.
    165     *
    166     * <p>
    167     * Tries to auto-detect the archive's format.
    168     * </p>
    169     *
    170     * <p>
    171     * 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
    172     * closing the stream itself. The caller is informed about the wrapper object via the {@code
    173     * closeableConsumer} callback as soon as it is no longer needed by this class.
    174     * </p>
    175     *
    176     * @param archive           the file to expand
    177     * @param targetDirectory   the target directory
    178     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
    179     * @throws IOException      if an I/O error occurs
    180     * @throws ArchiveException if the archive cannot be read for other reasons
    181     * @since 1.19
    182     */
    183    public void expand(final InputStream archive, final File targetDirectory, final CloseableConsumer closeableConsumer) throws IOException, ArchiveException {
    184        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
    185            expand(c.track(ArchiveStreamFactory.DEFAULT.createArchiveInputStream(archive)), targetDirectory);
    186        }
    187    }
    188
    189    /**
    190     * Expands {@code archive} into {@code targetDirectory}.
    191     *
    192     * <p>
    193     * Tries to auto-detect the archive's format.
    194     * </p>
    195     *
    196     * @param archive         the file to expand
    197     * @param targetDirectory the target directory
    198     * @throws IOException      if an I/O error occurs
    199     * @throws ArchiveException if the archive cannot be read for other reasons
    200     * @since 1.22
    201     */
    202    public void expand(final Path archive, final Path targetDirectory) throws IOException, ArchiveException {
    203        try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(archive))) {
    204            expand(ArchiveStreamFactory.detect(inputStream), archive, targetDirectory);
    205        }
    206    }
    207
    208    /**
    209     * Expands {@code archive} into {@code targetDirectory}.
    210     *
    211     * @param archive         the file to expand
    212     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    213     * @throws IOException if an I/O error occurs
    214     */
    215    public void expand(final SevenZFile archive, final File targetDirectory) throws IOException {
    216        expand(archive, toPath(targetDirectory));
    217    }
    218
    219    /**
    220     * Expands {@code archive} into {@code targetDirectory}.
    221     *
    222     * @param archive         the file to expand
    223     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    224     * @throws IOException if an I/O error occurs
    225     * @since 1.22
    226     */
    227    public void expand(final SevenZFile archive, final Path targetDirectory) throws IOException {
    228        expand(archive::getNextEntry, (entry, out) -> IOUtils.copyLarge(archive.getInputStream(entry), out), targetDirectory);
    229    }
    230
    231    /**
    232     * Expands {@code archive} into {@code targetDirectory}.
    233     *
    234     * @param archive         the file to expand
    235     * @param targetDirectory the target directory
    236     * @param format          the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    237     * @throws IOException      if an I/O error occurs
    238     * @throws ArchiveException if the archive cannot be read for other reasons
    239     */
    240    public void expand(final String format, final File archive, final File targetDirectory) throws IOException, ArchiveException {
    241        expand(format, archive.toPath(), toPath(targetDirectory));
    242    }
    243
    244    /**
    245     * Expands {@code archive} into {@code targetDirectory}.
    246     *
    247     * <p>
    248     * This method creates a wrapper around the archive stream which is never closed and thus leaks resources, please use
    249     * {@link #expand(String,InputStream,File,CloseableConsumer)} instead.
    250     * </p>
    251     *
    252     * @param archive         the file to expand
    253     * @param targetDirectory the target directory
    254     * @param format          the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    255     * @throws IOException      if an I/O error occurs
    256     * @throws ArchiveException if the archive cannot be read for other reasons
    257     * @deprecated this method leaks resources
    258     */
    259    @Deprecated
    260    public void expand(final String format, final InputStream archive, final File targetDirectory) throws IOException, ArchiveException {
    261        expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
    262    }
    263
    264    /**
    265     * Expands {@code archive} into {@code targetDirectory}.
    266     *
    267     * <p>
    268     * 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
    269     * closing the stream itself. The caller is informed about the wrapper object via the {@code
    270     * closeableConsumer} callback as soon as it is no longer needed by this class.
    271     * </p>
    272     *
    273     * @param archive           the file to expand
    274     * @param targetDirectory   the target directory
    275     * @param format            the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    276     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
    277     * @throws IOException      if an I/O error occurs
    278     * @throws ArchiveException if the archive cannot be read for other reasons
    279     * @since 1.19
    280     */
    281    public void expand(final String format, final InputStream archive, final File targetDirectory, final CloseableConsumer closeableConsumer)
    282            throws IOException, ArchiveException {
    283        expand(format, archive, toPath(targetDirectory), closeableConsumer);
    284    }
    285
    286    /**
    287     * Expands {@code archive} into {@code targetDirectory}.
    288     *
    289     * <p>
    290     * 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
    291     * closing the stream itself. The caller is informed about the wrapper object via the {@code
    292     * closeableConsumer} callback as soon as it is no longer needed by this class.
    293     * </p>
    294     *
    295     * @param archive           the file to expand
    296     * @param targetDirectory   the target directory
    297     * @param format            the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    298     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
    299     * @throws IOException      if an I/O error occurs
    300     * @throws ArchiveException if the archive cannot be read for other reasons
    301     * @since 1.22
    302     */
    303    public void expand(final String format, final InputStream archive, final Path targetDirectory, final CloseableConsumer closeableConsumer)
    304            throws IOException, ArchiveException {
    305        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
    306            final ArchiveInputStream<?> archiveInputStream = ArchiveStreamFactory.DEFAULT.createArchiveInputStream(format, archive);
    307            expand(c.track(archiveInputStream), targetDirectory);
    308        }
    309    }
    310
    311    /**
    312     * Expands {@code archive} into {@code targetDirectory}.
    313     *
    314     * @param archive         the file to expand
    315     * @param targetDirectory the target directory
    316     * @param format          the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    317     * @throws IOException      if an I/O error occurs
    318     * @throws ArchiveException if the archive cannot be read for other reasons
    319     * @since 1.22
    320     */
    321    public void expand(final String format, final Path archive, final Path targetDirectory) throws IOException, ArchiveException {
    322        if (prefersSeekableByteChannel(format)) {
    323            try (SeekableByteChannel channel = FileChannel.open(archive, StandardOpenOption.READ)) {
    324                expand(format, channel, targetDirectory, CloseableConsumer.CLOSING_CONSUMER);
    325            }
    326            return;
    327        }
    328        try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(archive))) {
    329            expand(format, inputStream, targetDirectory, CloseableConsumer.CLOSING_CONSUMER);
    330        }
    331    }
    332
    333    /**
    334     * Expands {@code archive} into {@code targetDirectory}.
    335     *
    336     * <p>
    337     * This method creates a wrapper around the archive channel which is never closed and thus leaks resources, please use
    338     * {@link #expand(String,SeekableByteChannel,File,CloseableConsumer)} instead.
    339     * </p>
    340     *
    341     * @param archive         the file to expand
    342     * @param targetDirectory the target directory
    343     * @param format          the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    344     * @throws IOException      if an I/O error occurs
    345     * @throws ArchiveException if the archive cannot be read for other reasons
    346     * @deprecated this method leaks resources
    347     */
    348    @Deprecated
    349    public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory) throws IOException, ArchiveException {
    350        expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
    351    }
    352
    353    /**
    354     * Expands {@code archive} into {@code targetDirectory}.
    355     *
    356     * <p>
    357     * 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
    358     * closing the channel itself. The caller is informed about the wrapper object via the {@code
    359     * closeableConsumer} callback as soon as it is no longer needed by this class.
    360     * </p>
    361     *
    362     * @param archive           the file to expand
    363     * @param targetDirectory   the target directory
    364     * @param format            the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    365     * @param closeableConsumer is informed about the stream wrapped around the passed in channel
    366     * @throws IOException      if an I/O error occurs
    367     * @throws ArchiveException if the archive cannot be read for other reasons
    368     * @since 1.19
    369     */
    370    public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory, final CloseableConsumer closeableConsumer)
    371            throws IOException, ArchiveException {
    372        expand(format, archive, toPath(targetDirectory), closeableConsumer);
    373    }
    374
    375    /**
    376     * Expands {@code archive} into {@code targetDirectory}.
    377     *
    378     * <p>
    379     * 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
    380     * closing the channel itself. The caller is informed about the wrapper object via the {@code
    381     * closeableConsumer} callback as soon as it is no longer needed by this class.
    382     * </p>
    383     *
    384     * @param archive           the file to expand
    385     * @param targetDirectory   the target directory
    386     * @param format            the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    387     * @param closeableConsumer is informed about the stream wrapped around the passed in channel
    388     * @throws IOException      if an I/O error occurs
    389     * @throws ArchiveException if the archive cannot be read for other reasons
    390     * @since 1.22
    391     */
    392    public void expand(final String format, final SeekableByteChannel archive, final Path targetDirectory, final CloseableConsumer closeableConsumer)
    393            throws IOException, ArchiveException {
    394        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
    395            if (!prefersSeekableByteChannel(format)) {
    396                expand(format, c.track(Channels.newInputStream(archive)), targetDirectory, CloseableConsumer.NULL_CONSUMER);
    397            } else if (ArchiveStreamFactory.TAR.equalsIgnoreCase(format)) {
    398                expand(c.track(new TarFile(archive)), targetDirectory);
    399            } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
    400                expand(c.track(ZipFile.builder().setSeekableByteChannel(archive).get()), targetDirectory);
    401            } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
    402                expand(c.track(SevenZFile.builder().setSeekableByteChannel(archive).get()), targetDirectory);
    403            } else {
    404                // never reached as prefersSeekableByteChannel only returns true for TAR, ZIP and 7z
    405                throw new ArchiveException("Don't know how to handle format " + format);
    406            }
    407        }
    408    }
    409
    410    /**
    411     * Expands {@code archive} into {@code targetDirectory}.
    412     *
    413     * @param archive         the file to expand
    414     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    415     * @throws IOException if an I/O error occurs
    416     * @since 1.21
    417     */
    418    public void expand(final TarFile archive, final File targetDirectory) throws IOException {
    419        expand(archive, toPath(targetDirectory));
    420    }
    421
    422    /**
    423     * Expands {@code archive} into {@code targetDirectory}.
    424     *
    425     * @param archive         the file to expand
    426     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    427     * @throws IOException if an I/O error occurs
    428     * @since 1.22
    429     */
    430    public void expand(final TarFile archive, final Path targetDirectory) throws IOException {
    431        final Iterator<TarArchiveEntry> entryIterator = archive.getEntries().iterator();
    432        expand(() -> entryIterator.hasNext() ? entryIterator.next() : null, (entry, out) -> {
    433            try (InputStream in = archive.getInputStream(entry)) {
    434                IOUtils.copy(in, out);
    435            }
    436        }, targetDirectory);
    437    }
    438
    439    /**
    440     * Expands {@code archive} into {@code targetDirectory}.
    441     *
    442     * @param archive         the file to expand
    443     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    444     * @throws IOException if an I/O error occurs
    445     */
    446    public void expand(final ZipFile archive, final File targetDirectory) throws IOException {
    447        expand(archive, toPath(targetDirectory));
    448    }
    449
    450    /**
    451     * Expands {@code archive} into {@code targetDirectory}.
    452     *
    453     * @param archive         the file to expand
    454     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    455     * @throws IOException if an I/O error occurs
    456     * @since 1.22
    457     */
    458    public void expand(final ZipFile archive, final Path targetDirectory) throws IOException {
    459        final Enumeration<ZipArchiveEntry> entries = archive.getEntries();
    460        expand(() -> {
    461            ZipArchiveEntry next = entries.hasMoreElements() ? entries.nextElement() : null;
    462            while (next != null && !archive.canReadEntryData(next)) {
    463                next = entries.hasMoreElements() ? entries.nextElement() : null;
    464            }
    465            return next;
    466        }, (entry, out) -> {
    467            try (InputStream in = archive.getInputStream(entry)) {
    468                IOUtils.copy(in, out);
    469            }
    470        }, targetDirectory);
    471    }
    472
    473    private boolean prefersSeekableByteChannel(final String format) {
    474        return ArchiveStreamFactory.TAR.equalsIgnoreCase(format) || ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)
    475                || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format);
    476    }
    477
    478    private Path toPath(final File targetDirectory) {
    479        return targetDirectory != null ? targetDirectory.toPath() : null;
    480    }
    481
    482}