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.compress.utils.IOUtils; 045 046/** 047 * Provides a high level API for expanding archives. 048 * @since 1.17 049 */ 050public class Expander { 051 052 @FunctionalInterface 053 private interface ArchiveEntrySupplier<T extends ArchiveEntry> { 054 T get() throws IOException; 055 } 056 057 @FunctionalInterface 058 private interface ArchiveEntryBiConsumer<T extends ArchiveEntry> { 059 void accept(T entry, OutputStream out) throws IOException; 060 } 061 062 /** 063 * @param targetDirectory May be null to simulate output to dev/null on Linux and NUL on Windows. 064 */ 065 private <T extends ArchiveEntry> void expand(final ArchiveEntrySupplier<T> supplier, final ArchiveEntryBiConsumer<T> writer, final Path targetDirectory) 066 throws IOException { 067 final boolean nullTarget = targetDirectory == null; 068 final Path targetDirPath = nullTarget ? null : targetDirectory.normalize(); 069 T nextEntry = supplier.get(); 070 while (nextEntry != null) { 071 final Path targetPath = nullTarget ? null : targetDirectory.resolve(nextEntry.getName()); 072 // check if targetDirectory and f are the same path - this may 073 // happen if the nextEntry.getName() is "./" 074 if (!nullTarget && !targetPath.normalize().startsWith(targetDirPath) && !Files.isSameFile(targetDirectory, targetPath)) { 075 throw new IOException("Expanding " + nextEntry.getName() + " would create file outside of " + targetDirectory); 076 } 077 if (nextEntry.isDirectory()) { 078 if (!nullTarget && !Files.isDirectory(targetPath) && Files.createDirectories(targetPath) == null) { 079 throw new IOException("Failed to create directory " + targetPath); 080 } 081 } else { 082 final Path parent = nullTarget ? null : targetPath.getParent(); 083 if (!nullTarget && !Files.isDirectory(parent) && Files.createDirectories(parent) == null) { 084 throw new IOException("Failed to create directory " + parent); 085 } 086 if (nullTarget) { 087 writer.accept(nextEntry, null); 088 } else { 089 try (OutputStream outputStream = Files.newOutputStream(targetPath)) { 090 writer.accept(nextEntry, outputStream); 091 } 092 } 093 } 094 nextEntry = supplier.get(); 095 } 096 } 097 098 /** 099 * Expands {@code archive} into {@code targetDirectory}. 100 * 101 * @param archive the file to expand 102 * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows. 103 * @throws IOException if an I/O error occurs 104 */ 105 public void expand(final ArchiveInputStream archive, final File targetDirectory) throws IOException { 106 expand(archive, toPath(targetDirectory)); 107 } 108 109 /** 110 * Expands {@code archive} into {@code targetDirectory}. 111 * 112 * @param archive the file to expand 113 * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows. 114 * @throws IOException if an I/O error occurs 115 * @since 1.22 116 */ 117 public void expand(final ArchiveInputStream archive, final Path targetDirectory) throws IOException { 118 expand(() -> { 119 ArchiveEntry next = archive.getNextEntry(); 120 while (next != null && !archive.canReadEntryData(next)) { 121 next = archive.getNextEntry(); 122 } 123 return next; 124 }, (entry, out) -> IOUtils.copy(archive, out), targetDirectory); 125 } 126 127 /** 128 * Expands {@code archive} into {@code targetDirectory}. 129 * 130 * <p>Tries to auto-detect the archive's format.</p> 131 * 132 * @param archive the file to expand 133 * @param targetDirectory the target directory 134 * @throws IOException if an I/O error occurs 135 * @throws ArchiveException if the archive cannot be read for other reasons 136 */ 137 public void expand(final File archive, final File targetDirectory) throws IOException, ArchiveException { 138 expand(archive.toPath(), toPath(targetDirectory)); 139 } 140 141 /** 142 * Expands {@code archive} into {@code targetDirectory}. 143 * 144 * <p>Tries to auto-detect the archive's format.</p> 145 * 146 * <p>This method creates a wrapper around the archive stream 147 * which is never closed and thus leaks resources, please use 148 * {@link #expand(InputStream,File,CloseableConsumer)} 149 * instead.</p> 150 * 151 * @param archive the file to expand 152 * @param targetDirectory the target directory 153 * @throws IOException if an I/O error occurs 154 * @throws ArchiveException if the archive cannot be read for other reasons 155 * @deprecated this method leaks resources 156 */ 157 @Deprecated 158 public void expand(final InputStream archive, final File targetDirectory) throws IOException, ArchiveException { 159 expand(archive, targetDirectory, CloseableConsumer.NULL_CONSUMER); 160 } 161 162 /** 163 * Expands {@code archive} into {@code targetDirectory}. 164 * 165 * <p>Tries to auto-detect the archive's format.</p> 166 * 167 * <p>This method creates a wrapper around the archive stream and 168 * the caller of this method is responsible for closing it - 169 * probably at the same time as closing the stream itself. The 170 * caller is informed about the wrapper object via the {@code 171 * closeableConsumer} callback as soon as it is no longer needed 172 * by this class.</p> 173 * 174 * @param archive the file to expand 175 * @param targetDirectory the target directory 176 * @param closeableConsumer is informed about the stream wrapped around the passed in stream 177 * @throws IOException if an I/O error occurs 178 * @throws ArchiveException if the archive cannot be read for other reasons 179 * @since 1.19 180 */ 181 public void expand(final InputStream archive, final File targetDirectory, final CloseableConsumer closeableConsumer) 182 throws IOException, ArchiveException { 183 try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) { 184 expand(c.track(ArchiveStreamFactory.DEFAULT.createArchiveInputStream(archive)), 185 targetDirectory); 186 } 187 } 188 189 /** 190 * Expands {@code archive} into {@code targetDirectory}. 191 * 192 * <p>Tries to auto-detect the archive's format.</p> 193 * 194 * @param archive the file to expand 195 * @param targetDirectory the target directory 196 * @throws IOException if an I/O error occurs 197 * @throws ArchiveException if the archive cannot be read for other reasons 198 * @since 1.22 199 */ 200 public void expand(final Path archive, final Path targetDirectory) throws IOException, ArchiveException { 201 try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(archive))) { 202 String format = ArchiveStreamFactory.detect(inputStream); 203 expand(format, archive, targetDirectory); 204 } 205 } 206 207 /** 208 * Expands {@code archive} into {@code targetDirectory}. 209 * 210 * @param archive the file to expand 211 * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows. 212 * @throws IOException if an I/O error occurs 213 */ 214 public void expand(final SevenZFile archive, final File targetDirectory) throws IOException { 215 expand(archive, toPath(targetDirectory)); 216 } 217 218 /** 219 * Expands {@code archive} into {@code targetDirectory}. 220 * 221 * @param archive the file to expand 222 * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows. 223 * @throws IOException if an I/O error occurs 224 * @since 1.22 225 */ 226 public void expand(final SevenZFile archive, final Path targetDirectory) 227 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 245 * accepted by {@link ArchiveStreamFactory}. 246 * @throws IOException if an I/O error occurs 247 * @throws ArchiveException if the archive cannot be read for other reasons 248 */ 249 public void expand(final String format, final File archive, final File targetDirectory) throws IOException, ArchiveException { 250 expand(format, archive.toPath(), toPath(targetDirectory)); 251 } 252 253 /** 254 * Expands {@code archive} into {@code targetDirectory}. 255 * 256 * <p>This method creates a wrapper around the archive stream 257 * which is never closed and thus leaks resources, please use 258 * {@link #expand(String,InputStream,File,CloseableConsumer)} 259 * instead.</p> 260 * 261 * @param archive the file to expand 262 * @param targetDirectory the target directory 263 * @param format the archive format. This uses the same format as 264 * accepted by {@link ArchiveStreamFactory}. 265 * @throws IOException if an I/O error occurs 266 * @throws ArchiveException if the archive cannot be read for other reasons 267 * @deprecated this method leaks resources 268 */ 269 @Deprecated 270 public void expand(final String format, final InputStream archive, final File targetDirectory) 271 throws IOException, ArchiveException { 272 expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER); 273 } 274 275 /** 276 * Expands {@code archive} into {@code targetDirectory}. 277 * 278 * <p>This method creates a wrapper around the archive stream and 279 * the caller of this method is responsible for closing it - 280 * probably at the same time as closing the stream itself. The 281 * caller is informed about the wrapper object via the {@code 282 * closeableConsumer} callback as soon as it is no longer needed 283 * by this class.</p> 284 * 285 * @param archive the file to expand 286 * @param targetDirectory the target directory 287 * @param format the archive format. This uses the same format as 288 * accepted by {@link ArchiveStreamFactory}. 289 * @param closeableConsumer is informed about the stream wrapped around the passed in stream 290 * @throws IOException if an I/O error occurs 291 * @throws ArchiveException if the archive cannot be read for other reasons 292 * @since 1.19 293 */ 294 public void expand(final String format, final InputStream archive, final File targetDirectory, final CloseableConsumer closeableConsumer) 295 throws IOException, ArchiveException { 296 expand(format, archive, toPath(targetDirectory), closeableConsumer); 297 } 298 299 /** 300 * Expands {@code archive} into {@code targetDirectory}. 301 * 302 * <p>This method creates a wrapper around the archive stream and 303 * the caller of this method is responsible for closing it - 304 * probably at the same time as closing the stream itself. The 305 * caller is informed about the wrapper object via the {@code 306 * closeableConsumer} callback as soon as it is no longer needed 307 * by this class.</p> 308 * 309 * @param archive the file to expand 310 * @param targetDirectory the target directory 311 * @param format the archive format. This uses the same format as 312 * accepted by {@link ArchiveStreamFactory}. 313 * @param closeableConsumer is informed about the stream wrapped around the passed in stream 314 * @throws IOException if an I/O error occurs 315 * @throws ArchiveException if the archive cannot be read for other reasons 316 * @since 1.22 317 */ 318 public void expand(final String format, final InputStream archive, final Path targetDirectory, final CloseableConsumer closeableConsumer) 319 throws IOException, ArchiveException { 320 try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) { 321 expand(c.track(ArchiveStreamFactory.DEFAULT.createArchiveInputStream(format, archive)), 322 targetDirectory); 323 } 324 } 325 326 /** 327 * Expands {@code archive} into {@code targetDirectory}. 328 * 329 * @param archive the file to expand 330 * @param targetDirectory the target directory 331 * @param format the archive format. This uses the same format as 332 * accepted by {@link ArchiveStreamFactory}. 333 * @throws IOException if an I/O error occurs 334 * @throws ArchiveException if the archive cannot be read for other reasons 335 * @since 1.22 336 */ 337 public void expand(final String format, final Path archive, final Path targetDirectory) throws IOException, ArchiveException { 338 if (prefersSeekableByteChannel(format)) { 339 try (SeekableByteChannel channel = FileChannel.open(archive, StandardOpenOption.READ)) { 340 expand(format, channel, targetDirectory, CloseableConsumer.CLOSING_CONSUMER); 341 } 342 return; 343 } 344 try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(archive))) { 345 expand(format, inputStream, targetDirectory, CloseableConsumer.CLOSING_CONSUMER); 346 } 347 } 348 349 /** 350 * Expands {@code archive} into {@code targetDirectory}. 351 * 352 * <p>This method creates a wrapper around the archive channel 353 * which is never closed and thus leaks resources, please use 354 * {@link #expand(String,SeekableByteChannel,File,CloseableConsumer)} 355 * instead.</p> 356 * 357 * @param archive the file to expand 358 * @param targetDirectory the target directory 359 * @param format the archive format. This uses the same format as 360 * accepted by {@link ArchiveStreamFactory}. 361 * @throws IOException if an I/O error occurs 362 * @throws ArchiveException if the archive cannot be read for other reasons 363 * @deprecated this method leaks resources 364 */ 365 @Deprecated 366 public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory) 367 throws IOException, ArchiveException { 368 expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER); 369 } 370 371 /** 372 * Expands {@code archive} into {@code targetDirectory}. 373 * 374 * <p>This method creates a wrapper around the archive channel and 375 * the caller of this method is responsible for closing it - 376 * probably at the same time as closing the channel itself. The 377 * caller is informed about the wrapper object via the {@code 378 * closeableConsumer} callback as soon as it is no longer needed 379 * by this class.</p> 380 * 381 * @param archive the file to expand 382 * @param targetDirectory the target directory 383 * @param format the archive format. This uses the same format as 384 * accepted by {@link ArchiveStreamFactory}. 385 * @param closeableConsumer is informed about the stream wrapped around the passed in channel 386 * @throws IOException if an I/O error occurs 387 * @throws ArchiveException if the archive cannot be read for other reasons 388 * @since 1.19 389 */ 390 public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory, final CloseableConsumer closeableConsumer) 391 throws IOException, ArchiveException { 392 expand(format, archive, toPath(targetDirectory), closeableConsumer); 393 } 394 395 /** 396 * Expands {@code archive} into {@code targetDirectory}. 397 * 398 * <p>This method creates a wrapper around the archive channel and 399 * the caller of this method is responsible for closing it - 400 * probably at the same time as closing the channel itself. The 401 * caller is informed about the wrapper object via the {@code 402 * closeableConsumer} callback as soon as it is no longer needed 403 * by this class.</p> 404 * 405 * @param archive the file to expand 406 * @param targetDirectory the target directory 407 * @param format the archive format. This uses the same format as 408 * accepted by {@link ArchiveStreamFactory}. 409 * @param closeableConsumer is informed about the stream wrapped around the passed in channel 410 * @throws IOException if an I/O error occurs 411 * @throws ArchiveException if the archive cannot be read for other reasons 412 * @since 1.22 413 */ 414 public void expand(final String format, final SeekableByteChannel archive, final Path targetDirectory, 415 final CloseableConsumer closeableConsumer) 416 throws IOException, ArchiveException { 417 try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) { 418 if (!prefersSeekableByteChannel(format)) { 419 expand(format, c.track(Channels.newInputStream(archive)), targetDirectory, CloseableConsumer.NULL_CONSUMER); 420 } else if (ArchiveStreamFactory.TAR.equalsIgnoreCase(format)) { 421 expand(c.track(new TarFile(archive)), targetDirectory); 422 } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) { 423 expand(c.track(new ZipFile(archive)), targetDirectory); 424 } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) { 425 expand(c.track(new SevenZFile(archive)), targetDirectory); 426 } else { 427 // never reached as prefersSeekableByteChannel only returns true for TAR, ZIP and 7z 428 throw new ArchiveException("Don't know how to handle format " + format); 429 } 430 } 431 } 432 433 /** 434 * Expands {@code archive} into {@code targetDirectory}. 435 * 436 * @param archive the file to expand 437 * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows. 438 * @throws IOException if an I/O error occurs 439 * @since 1.21 440 */ 441 public void expand(final TarFile archive, final File targetDirectory) throws IOException { 442 expand(archive, toPath(targetDirectory)); 443 } 444 445 /** 446 * Expands {@code archive} into {@code targetDirectory}. 447 * 448 * @param archive the file to expand 449 * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows. 450 * @throws IOException if an I/O error occurs 451 * @since 1.22 452 */ 453 public void expand(final TarFile archive, final Path targetDirectory) 454 throws IOException { 455 final Iterator<TarArchiveEntry> entryIterator = archive.getEntries().iterator(); 456 expand(() -> entryIterator.hasNext() ? entryIterator.next() : null, 457 (entry, out) -> { 458 try (InputStream in = archive.getInputStream(entry)) { 459 IOUtils.copy(in, out); 460 } 461 }, targetDirectory); 462 } 463 464 /** 465 * Expands {@code archive} into {@code targetDirectory}. 466 * 467 * @param archive the file to expand 468 * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows. 469 * @throws IOException if an I/O error occurs 470 */ 471 public void expand(final ZipFile archive, final File targetDirectory) throws IOException { 472 expand(archive, toPath(targetDirectory)); 473 } 474 475 /** 476 * Expands {@code archive} into {@code targetDirectory}. 477 * 478 * @param archive the file to expand 479 * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows. 480 * @throws IOException if an I/O error occurs 481 * @since 1.22 482 */ 483 public void expand(final ZipFile archive, final Path targetDirectory) 484 throws IOException { 485 final Enumeration<ZipArchiveEntry> entries = archive.getEntries(); 486 expand(() -> { 487 ZipArchiveEntry next = entries.hasMoreElements() ? entries.nextElement() : null; 488 while (next != null && !archive.canReadEntryData(next)) { 489 next = entries.hasMoreElements() ? entries.nextElement() : null; 490 } 491 return next; 492 }, (entry, out) -> { 493 try (InputStream in = archive.getInputStream(entry)) { 494 IOUtils.copy(in, out); 495 } 496 }, targetDirectory); 497 } 498 499 private boolean prefersSeekableByteChannel(final String format) { 500 return ArchiveStreamFactory.TAR.equalsIgnoreCase(format) 501 || ArchiveStreamFactory.ZIP.equalsIgnoreCase(format) 502 || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format); 503 } 504 505 private Path toPath(final File targetDirectory) { 506 return targetDirectory != null ? targetDirectory.toPath() : null; 507 } 508 509}