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 }