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 String format = null; 202 try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(archive))) { 203 format = ArchiveStreamFactory.detect(inputStream); 204 } 205 expand(format, archive, targetDirectory); 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) 228 throws IOException { 229 expand(archive::getNextEntry, (entry, out) -> { 230 final byte[] buffer = new byte[8192]; 231 int n; 232 while (-1 != (n = archive.read(buffer))) { 233 if (out != null) { 234 out.write(buffer, 0, n); 235 } 236 } 237 }, targetDirectory); 238 } 239 240 /** 241 * Expands {@code archive} into {@code targetDirectory}. 242 * 243 * @param archive the file to expand 244 * @param targetDirectory the target directory 245 * @param format the archive format. This uses the same format as 246 * accepted by {@link ArchiveStreamFactory}. 247 * @throws IOException if an I/O error occurs 248 * @throws ArchiveException if the archive cannot be read for other reasons 249 */ 250 public void expand(final String format, final File archive, final File targetDirectory) throws IOException, ArchiveException { 251 expand(format, archive.toPath(), toPath(targetDirectory)); 252 } 253 254 /** 255 * Expands {@code archive} into {@code targetDirectory}. 256 * 257 * <p>This method creates a wrapper around the archive stream 258 * which is never closed and thus leaks resources, please use 259 * {@link #expand(String,InputStream,File,CloseableConsumer)} 260 * instead.</p> 261 * 262 * @param archive the file to expand 263 * @param targetDirectory the target directory 264 * @param format the archive format. This uses the same format as 265 * accepted by {@link ArchiveStreamFactory}. 266 * @throws IOException if an I/O error occurs 267 * @throws ArchiveException if the archive cannot be read for other reasons 268 * @deprecated this method leaks resources 269 */ 270 @Deprecated 271 public void expand(final String format, final InputStream archive, final File targetDirectory) 272 throws IOException, ArchiveException { 273 expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER); 274 } 275 276 /** 277 * Expands {@code archive} into {@code targetDirectory}. 278 * 279 * <p>This method creates a wrapper around the archive stream and 280 * the caller of this method is responsible for closing it - 281 * probably at the same time as closing the stream itself. The 282 * caller is informed about the wrapper object via the {@code 283 * closeableConsumer} callback as soon as it is no longer needed 284 * by this class.</p> 285 * 286 * @param archive the file to expand 287 * @param targetDirectory the target directory 288 * @param format the archive format. This uses the same format as 289 * accepted by {@link ArchiveStreamFactory}. 290 * @param closeableConsumer is informed about the stream wrapped around the passed in stream 291 * @throws IOException if an I/O error occurs 292 * @throws ArchiveException if the archive cannot be read for other reasons 293 * @since 1.19 294 */ 295 public void expand(final String format, final InputStream archive, final File targetDirectory, final CloseableConsumer closeableConsumer) 296 throws IOException, ArchiveException { 297 expand(format, archive, toPath(targetDirectory), closeableConsumer); 298 } 299 300 /** 301 * Expands {@code archive} into {@code targetDirectory}. 302 * 303 * <p>This method creates a wrapper around the archive stream and 304 * the caller of this method is responsible for closing it - 305 * probably at the same time as closing the stream itself. The 306 * caller is informed about the wrapper object via the {@code 307 * closeableConsumer} callback as soon as it is no longer needed 308 * by this class.</p> 309 * 310 * @param archive the file to expand 311 * @param targetDirectory the target directory 312 * @param format the archive format. This uses the same format as 313 * accepted by {@link ArchiveStreamFactory}. 314 * @param closeableConsumer is informed about the stream wrapped around the passed in stream 315 * @throws IOException if an I/O error occurs 316 * @throws ArchiveException if the archive cannot be read for other reasons 317 * @since 1.22 318 */ 319 public void expand(final String format, final InputStream archive, final Path targetDirectory, final CloseableConsumer closeableConsumer) 320 throws IOException, ArchiveException { 321 try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) { 322 expand(c.track(ArchiveStreamFactory.DEFAULT.createArchiveInputStream(format, archive)), 323 targetDirectory); 324 } 325 } 326 327 /** 328 * Expands {@code archive} into {@code targetDirectory}. 329 * 330 * @param archive the file to expand 331 * @param targetDirectory the target directory 332 * @param format the archive format. This uses the same format as 333 * accepted by {@link ArchiveStreamFactory}. 334 * @throws IOException if an I/O error occurs 335 * @throws ArchiveException if the archive cannot be read for other reasons 336 * @since 1.22 337 */ 338 public void expand(final String format, final Path archive, final Path targetDirectory) throws IOException, ArchiveException { 339 if (prefersSeekableByteChannel(format)) { 340 try (SeekableByteChannel channel = FileChannel.open(archive, StandardOpenOption.READ)) { 341 expand(format, channel, targetDirectory, CloseableConsumer.CLOSING_CONSUMER); 342 } 343 return; 344 } 345 try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(archive))) { 346 expand(format, inputStream, targetDirectory, CloseableConsumer.CLOSING_CONSUMER); 347 } 348 } 349 350 /** 351 * Expands {@code archive} into {@code targetDirectory}. 352 * 353 * <p>This method creates a wrapper around the archive channel 354 * which is never closed and thus leaks resources, please use 355 * {@link #expand(String,SeekableByteChannel,File,CloseableConsumer)} 356 * instead.</p> 357 * 358 * @param archive the file to expand 359 * @param targetDirectory the target directory 360 * @param format the archive format. This uses the same format as 361 * accepted by {@link ArchiveStreamFactory}. 362 * @throws IOException if an I/O error occurs 363 * @throws ArchiveException if the archive cannot be read for other reasons 364 * @deprecated this method leaks resources 365 */ 366 @Deprecated 367 public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory) 368 throws IOException, ArchiveException { 369 expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER); 370 } 371 372 /** 373 * Expands {@code archive} into {@code targetDirectory}. 374 * 375 * <p>This method creates a wrapper around the archive channel and 376 * the caller of this method is responsible for closing it - 377 * probably at the same time as closing the channel itself. The 378 * caller is informed about the wrapper object via the {@code 379 * closeableConsumer} callback as soon as it is no longer needed 380 * by this class.</p> 381 * 382 * @param archive the file to expand 383 * @param targetDirectory the target directory 384 * @param format the archive format. This uses the same format as 385 * accepted by {@link ArchiveStreamFactory}. 386 * @param closeableConsumer is informed about the stream wrapped around the passed in channel 387 * @throws IOException if an I/O error occurs 388 * @throws ArchiveException if the archive cannot be read for other reasons 389 * @since 1.19 390 */ 391 public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory, final CloseableConsumer closeableConsumer) 392 throws IOException, ArchiveException { 393 expand(format, archive, toPath(targetDirectory), closeableConsumer); 394 } 395 396 /** 397 * Expands {@code archive} into {@code targetDirectory}. 398 * 399 * <p>This method creates a wrapper around the archive channel and 400 * the caller of this method is responsible for closing it - 401 * probably at the same time as closing the channel itself. The 402 * caller is informed about the wrapper object via the {@code 403 * closeableConsumer} callback as soon as it is no longer needed 404 * by this class.</p> 405 * 406 * @param archive the file to expand 407 * @param targetDirectory the target directory 408 * @param format the archive format. This uses the same format as 409 * accepted by {@link ArchiveStreamFactory}. 410 * @param closeableConsumer is informed about the stream wrapped around the passed in channel 411 * @throws IOException if an I/O error occurs 412 * @throws ArchiveException if the archive cannot be read for other reasons 413 * @since 1.22 414 */ 415 public void expand(final String format, final SeekableByteChannel archive, final Path targetDirectory, 416 final CloseableConsumer closeableConsumer) 417 throws IOException, ArchiveException { 418 try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) { 419 if (!prefersSeekableByteChannel(format)) { 420 expand(format, c.track(Channels.newInputStream(archive)), targetDirectory, CloseableConsumer.NULL_CONSUMER); 421 } else if (ArchiveStreamFactory.TAR.equalsIgnoreCase(format)) { 422 expand(c.track(new TarFile(archive)), targetDirectory); 423 } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) { 424 expand(c.track(new ZipFile(archive)), targetDirectory); 425 } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) { 426 expand(c.track(new SevenZFile(archive)), targetDirectory); 427 } else { 428 // never reached as prefersSeekableByteChannel only returns true for TAR, ZIP and 7z 429 throw new ArchiveException("Don't know how to handle format " + format); 430 } 431 } 432 } 433 434 /** 435 * Expands {@code archive} into {@code targetDirectory}. 436 * 437 * @param archive the file to expand 438 * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows. 439 * @throws IOException if an I/O error occurs 440 * @since 1.21 441 */ 442 public void expand(final TarFile archive, final File targetDirectory) throws IOException { 443 expand(archive, toPath(targetDirectory)); 444 } 445 446 /** 447 * Expands {@code archive} into {@code targetDirectory}. 448 * 449 * @param archive the file to expand 450 * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows. 451 * @throws IOException if an I/O error occurs 452 * @since 1.22 453 */ 454 public void expand(final TarFile archive, final Path targetDirectory) 455 throws IOException { 456 final Iterator<TarArchiveEntry> entryIterator = archive.getEntries().iterator(); 457 expand(() -> entryIterator.hasNext() ? entryIterator.next() : null, 458 (entry, out) -> { 459 try (InputStream in = archive.getInputStream(entry)) { 460 IOUtils.copy(in, out); 461 } 462 }, targetDirectory); 463 } 464 465 /** 466 * Expands {@code archive} into {@code targetDirectory}. 467 * 468 * @param archive the file to expand 469 * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows. 470 * @throws IOException if an I/O error occurs 471 */ 472 public void expand(final ZipFile archive, final File targetDirectory) throws IOException { 473 expand(archive, toPath(targetDirectory)); 474 } 475 476 /** 477 * Expands {@code archive} into {@code targetDirectory}. 478 * 479 * @param archive the file to expand 480 * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows. 481 * @throws IOException if an I/O error occurs 482 * @since 1.22 483 */ 484 public void expand(final ZipFile archive, final Path targetDirectory) 485 throws IOException { 486 final Enumeration<ZipArchiveEntry> entries = archive.getEntries(); 487 expand(() -> { 488 ZipArchiveEntry next = entries.hasMoreElements() ? entries.nextElement() : null; 489 while (next != null && !archive.canReadEntryData(next)) { 490 next = entries.hasMoreElements() ? entries.nextElement() : null; 491 } 492 return next; 493 }, (entry, out) -> { 494 try (InputStream in = archive.getInputStream(entry)) { 495 IOUtils.copy(in, out); 496 } 497 }, targetDirectory); 498 } 499 500 private boolean prefersSeekableByteChannel(final String format) { 501 return ArchiveStreamFactory.TAR.equalsIgnoreCase(format) 502 || ArchiveStreamFactory.ZIP.equalsIgnoreCase(format) 503 || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format); 504 } 505 506 private Path toPath(final File targetDirectory) { 507 return targetDirectory != null ? targetDirectory.toPath() : null; 508 } 509 510}