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}