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}