    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.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) -> {
    229            final byte[] buffer = new byte[8192];
    230            int n;
    231            while (-1 != (n = archive.read(buffer))) {
    232                if (out != null) {
    233                    out.write(buffer, 0, n);
    234                }
    235            }
    236        }, targetDirectory);
    237    }
    238
    239    /**
    240     * Expands {@code archive} into {@code targetDirectory}.
    241     *
    242     * @param archive         the file to expand
    243     * @param targetDirectory the target directory
    244     * @param format          the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    245     * @throws IOException      if an I/O error occurs
    246     * @throws ArchiveException if the archive cannot be read for other reasons
    247     */
    248    public void expand(final String format, final File archive, final File targetDirectory) throws IOException, ArchiveException {
    249        expand(format, archive.toPath(), toPath(targetDirectory));
    250    }
    251
    252    /**
    253     * Expands {@code archive} into {@code targetDirectory}.
    254     *
    255     * <p>
    256     * This method creates a wrapper around the archive stream which is never closed and thus leaks resources, please use
    257     * {@link #expand(String,InputStream,File,CloseableConsumer)} instead.
    258     * </p>
    259     *
    260     * @param archive         the file to expand
    261     * @param targetDirectory the target directory
    262     * @param format          the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    263     * @throws IOException      if an I/O error occurs
    264     * @throws ArchiveException if the archive cannot be read for other reasons
    265     * @deprecated this method leaks resources
    266     */
    267    @Deprecated
    268    public void expand(final String format, final InputStream archive, final File targetDirectory) throws IOException, ArchiveException {
    269        expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
    270    }
    271
    272    /**
    273     * Expands {@code archive} into {@code targetDirectory}.
    274     *
    275     * <p>
    276     * 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
    277     * closing the stream itself. The caller is informed about the wrapper object via the {@code
    278     * closeableConsumer} callback as soon as it is no longer needed by this class.
    279     * </p>
    280     *
    281     * @param archive           the file to expand
    282     * @param targetDirectory   the target directory
    283     * @param format            the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    284     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
    285     * @throws IOException      if an I/O error occurs
    286     * @throws ArchiveException if the archive cannot be read for other reasons
    287     * @since 1.19
    288     */
    289    public void expand(final String format, final InputStream archive, final File targetDirectory, final CloseableConsumer closeableConsumer)
    290            throws IOException, ArchiveException {
    291        expand(format, archive, toPath(targetDirectory), closeableConsumer);
    292    }
    293
    294    /**
    295     * Expands {@code archive} into {@code targetDirectory}.
    296     *
    297     * <p>
    298     * 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
    299     * closing the stream itself. The caller is informed about the wrapper object via the {@code
    300     * closeableConsumer} callback as soon as it is no longer needed by this class.
    301     * </p>
    302     *
    303     * @param archive           the file to expand
    304     * @param targetDirectory   the target directory
    305     * @param format            the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    306     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
    307     * @throws IOException      if an I/O error occurs
    308     * @throws ArchiveException if the archive cannot be read for other reasons
    309     * @since 1.22
    310     */
    311    public void expand(final String format, final InputStream archive, final Path targetDirectory, final CloseableConsumer closeableConsumer)
    312            throws IOException, ArchiveException {
    313        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
    314            final ArchiveInputStream<?> archiveInputStream = ArchiveStreamFactory.DEFAULT.createArchiveInputStream(format, archive);
    315            expand(c.track(archiveInputStream), targetDirectory);
    316        }
    317    }
    318
    319    /**
    320     * Expands {@code archive} into {@code targetDirectory}.
    321     *
    322     * @param archive         the file to expand
    323     * @param targetDirectory the target directory
    324     * @param format          the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    325     * @throws IOException      if an I/O error occurs
    326     * @throws ArchiveException if the archive cannot be read for other reasons
    327     * @since 1.22
    328     */
    329    public void expand(final String format, final Path archive, final Path targetDirectory) throws IOException, ArchiveException {
    330        if (prefersSeekableByteChannel(format)) {
    331            try (SeekableByteChannel channel = FileChannel.open(archive, StandardOpenOption.READ)) {
    332                expand(format, channel, targetDirectory, CloseableConsumer.CLOSING_CONSUMER);
    333            }
    334            return;
    335        }
    336        try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(archive))) {
    337            expand(format, inputStream, targetDirectory, CloseableConsumer.CLOSING_CONSUMER);
    338        }
    339    }
    340
    341    /**
    342     * Expands {@code archive} into {@code targetDirectory}.
    343     *
    344     * <p>
    345     * This method creates a wrapper around the archive channel which is never closed and thus leaks resources, please use
    346     * {@link #expand(String,SeekableByteChannel,File,CloseableConsumer)} instead.
    347     * </p>
    348     *
    349     * @param archive         the file to expand
    350     * @param targetDirectory the target directory
    351     * @param format          the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    352     * @throws IOException      if an I/O error occurs
    353     * @throws ArchiveException if the archive cannot be read for other reasons
    354     * @deprecated this method leaks resources
    355     */
    356    @Deprecated
    357    public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory) throws IOException, ArchiveException {
    358        expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
    359    }
    360
    361    /**
    362     * Expands {@code archive} into {@code targetDirectory}.
    363     *
    364     * <p>
    365     * 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
    366     * closing the channel itself. The caller is informed about the wrapper object via the {@code
    367     * closeableConsumer} callback as soon as it is no longer needed by this class.
    368     * </p>
    369     *
    370     * @param archive           the file to expand
    371     * @param targetDirectory   the target directory
    372     * @param format            the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    373     * @param closeableConsumer is informed about the stream wrapped around the passed in channel
    374     * @throws IOException      if an I/O error occurs
    375     * @throws ArchiveException if the archive cannot be read for other reasons
    376     * @since 1.19
    377     */
    378    public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory, final CloseableConsumer closeableConsumer)
    379            throws IOException, ArchiveException {
    380        expand(format, archive, toPath(targetDirectory), closeableConsumer);
    381    }
    382
    383    /**
    384     * Expands {@code archive} into {@code targetDirectory}.
    385     *
    386     * <p>
    387     * 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
    388     * closing the channel itself. The caller is informed about the wrapper object via the {@code
    389     * closeableConsumer} callback as soon as it is no longer needed by this class.
    390     * </p>
    391     *
    392     * @param archive           the file to expand
    393     * @param targetDirectory   the target directory
    394     * @param format            the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
    395     * @param closeableConsumer is informed about the stream wrapped around the passed in channel
    396     * @throws IOException      if an I/O error occurs
    397     * @throws ArchiveException if the archive cannot be read for other reasons
    398     * @since 1.22
    399     */
    400    public void expand(final String format, final SeekableByteChannel archive, final Path targetDirectory, final CloseableConsumer closeableConsumer)
    401            throws IOException, ArchiveException {
    402        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
    403            if (!prefersSeekableByteChannel(format)) {
    404                expand(format, c.track(Channels.newInputStream(archive)), targetDirectory, CloseableConsumer.NULL_CONSUMER);
    405            } else if (ArchiveStreamFactory.TAR.equalsIgnoreCase(format)) {
    406                expand(c.track(new TarFile(archive)), targetDirectory);
    407            } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
    408                expand(c.track(ZipFile.builder().setSeekableByteChannel(archive).get()), targetDirectory);
    409            } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
    410                expand(c.track(SevenZFile.builder().setSeekableByteChannel(archive).get()), targetDirectory);
    411            } else {
    412                // never reached as prefersSeekableByteChannel only returns true for TAR, ZIP and 7z
    413                throw new ArchiveException("Don't know how to handle format " + format);
    414            }
    415        }
    416    }
    417
    418    /**
    419     * Expands {@code archive} into {@code targetDirectory}.
    420     *
    421     * @param archive         the file to expand
    422     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    423     * @throws IOException if an I/O error occurs
    424     * @since 1.21
    425     */
    426    public void expand(final TarFile archive, final File targetDirectory) throws IOException {
    427        expand(archive, toPath(targetDirectory));
    428    }
    429
    430    /**
    431     * Expands {@code archive} into {@code targetDirectory}.
    432     *
    433     * @param archive         the file to expand
    434     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    435     * @throws IOException if an I/O error occurs
    436     * @since 1.22
    437     */
    438    public void expand(final TarFile archive, final Path targetDirectory) throws IOException {
    439        final Iterator<TarArchiveEntry> entryIterator = archive.getEntries().iterator();
    440        expand(() -> entryIterator.hasNext() ? entryIterator.next() : null, (entry, out) -> {
    441            try (InputStream in = archive.getInputStream(entry)) {
    442                IOUtils.copy(in, out);
    443            }
    444        }, targetDirectory);
    445    }
    446
    447    /**
    448     * Expands {@code archive} into {@code targetDirectory}.
    449     *
    450     * @param archive         the file to expand
    451     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    452     * @throws IOException if an I/O error occurs
    453     */
    454    public void expand(final ZipFile archive, final File targetDirectory) throws IOException {
    455        expand(archive, toPath(targetDirectory));
    456    }
    457
    458    /**
    459     * Expands {@code archive} into {@code targetDirectory}.
    460     *
    461     * @param archive         the file to expand
    462     * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
    463     * @throws IOException if an I/O error occurs
    464     * @since 1.22
    465     */
    466    public void expand(final ZipFile archive, final Path targetDirectory) throws IOException {
    467        final Enumeration<ZipArchiveEntry> entries = archive.getEntries();
    468        expand(() -> {
    469            ZipArchiveEntry next = entries.hasMoreElements() ? entries.nextElement() : null;
    470            while (next != null && !archive.canReadEntryData(next)) {
    471                next = entries.hasMoreElements() ? entries.nextElement() : null;
    472            }
    473            return next;
    474        }, (entry, out) -> {
    475            try (InputStream in = archive.getInputStream(entry)) {
    476                IOUtils.copy(in, out);
    477            }
    478        }, targetDirectory);
    479    }
    480
    481    private boolean prefersSeekableByteChannel(final String format) {
    482        return ArchiveStreamFactory.TAR.equalsIgnoreCase(format) || ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)
    483                || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format);
    484    }
    485
    486    private Path toPath(final File targetDirectory) {
    487        return targetDirectory != null ? targetDirectory.toPath() : null;
    488    }
    489
    490}