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}