View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   https://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.commons.compress.archivers.examples;
20  
21  import java.io.BufferedInputStream;
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.nio.channels.Channels;
27  import java.nio.channels.FileChannel;
28  import java.nio.channels.SeekableByteChannel;
29  import java.nio.file.Files;
30  import java.nio.file.Path;
31  import java.nio.file.StandardOpenOption;
32  import java.util.Enumeration;
33  import java.util.Iterator;
34  
35  import org.apache.commons.compress.archivers.ArchiveEntry;
36  import org.apache.commons.compress.archivers.ArchiveException;
37  import org.apache.commons.compress.archivers.ArchiveInputStream;
38  import org.apache.commons.compress.archivers.ArchiveStreamFactory;
39  import org.apache.commons.compress.archivers.sevenz.SevenZFile;
40  import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
41  import org.apache.commons.compress.archivers.tar.TarFile;
42  import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
43  import org.apache.commons.compress.archivers.zip.ZipFile;
44  import org.apache.commons.io.IOUtils;
45  import org.apache.commons.io.output.NullOutputStream;
46  
47  /**
48   * Provides a high level API for expanding archives.
49   *
50   * @since 1.17
51   */
52  public class Expander {
53  
54      @FunctionalInterface
55      private interface ArchiveEntryBiConsumer<T extends ArchiveEntry> {
56          void accept(T entry, OutputStream out) throws IOException;
57      }
58  
59      @FunctionalInterface
60      private interface ArchiveEntrySupplier<T extends ArchiveEntry> {
61          T get() throws IOException;
62      }
63  
64      /**
65       * @param targetDirectory May be null to simulate output to dev/null on Linux and NUL on Windows.
66       */
67      private <T extends ArchiveEntry> void expand(final ArchiveEntrySupplier<T> supplier, final ArchiveEntryBiConsumer<T> writer, final Path targetDirectory)
68              throws IOException {
69          final boolean nullTarget = targetDirectory == null;
70          final Path targetDirPath = nullTarget ? null : targetDirectory.normalize();
71          T nextEntry = supplier.get();
72          while (nextEntry != null) {
73              final Path targetPath = nullTarget ? null : nextEntry.resolveIn(targetDirPath);
74              if (nextEntry.isDirectory()) {
75                  if (!nullTarget && !Files.isDirectory(targetPath) && Files.createDirectories(targetPath) == null) {
76                      throw new IOException("Failed to create directory " + targetPath);
77                  }
78              } else {
79                  final Path parent = nullTarget ? null : targetPath.getParent();
80                  if (!nullTarget && !Files.isDirectory(parent) && Files.createDirectories(parent) == null) {
81                      throw new IOException("Failed to create directory " + parent);
82                  }
83                  if (nullTarget) {
84                      writer.accept(nextEntry, NullOutputStream.INSTANCE);
85                  } else {
86                      try (OutputStream outputStream = Files.newOutputStream(targetPath)) {
87                          writer.accept(nextEntry, outputStream);
88                      }
89                  }
90              }
91              nextEntry = supplier.get();
92          }
93      }
94  
95      /**
96       * Expands {@code archive} into {@code targetDirectory}.
97       *
98       * @param archive         the file to expand
99       * @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 }