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}