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