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 private interface ArchiveEntrySupplier { 053 ArchiveEntry getNextReadableEntry() throws IOException; 054 } 055 056 private interface EntryWriter { 057 void writeEntryDataTo(ArchiveEntry entry, OutputStream out) throws IOException; 058 } 059 060 /** 061 * @param targetDirectory May be null to simulate output to dev/null on Linux and NUL on Windows. 062 */ 063 private void expand(final ArchiveEntrySupplier supplier, final EntryWriter writer, final Path targetDirectory) 064 throws IOException { 065 final boolean nullTarget = targetDirectory == null; 066 final Path targetDirPath = nullTarget ? null : targetDirectory.normalize(); 067 ArchiveEntry nextEntry = supplier.getNextReadableEntry(); 068 while (nextEntry != null) { 069 final Path targetPath = nullTarget ? null : targetDirectory.resolve(nextEntry.getName()); 070 // check if targetDirectory and f are the same path - this may 071 // happen if the nextEntry.getName() is "./" 072 if (!nullTarget && !targetPath.normalize().startsWith(targetDirPath) && !Files.isSameFile(targetDirectory, targetPath)) { 073 throw new IOException("Expanding " + nextEntry.getName() + " would create file outside of " + targetDirectory); 074 } 075 if (nextEntry.isDirectory()) { 076 if (!nullTarget && !Files.isDirectory(targetPath) && Files.createDirectories(targetPath) == null) { 077 throw new IOException("Failed to create directory " + targetPath); 078 } 079 } else { 080 final Path parent = nullTarget ? null : targetPath.getParent(); 081 if (!nullTarget && !Files.isDirectory(parent) && Files.createDirectories(parent) == null) { 082 throw new IOException("Failed to create directory " + parent); 083 } 084 if (nullTarget) { 085 writer.writeEntryDataTo(nextEntry, null); 086 } else { 087 try (OutputStream outputStream = Files.newOutputStream(targetPath)) { 088 writer.writeEntryDataTo(nextEntry, outputStream); 089 } 090 } 091 } 092 nextEntry = supplier.getNextReadableEntry(); 093 } 094 } 095 096 /** 097 * Expands {@code archive} into {@code targetDirectory}. 098 * 099 * @param archive the file to expand 100 * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows. 101 * @throws IOException if an I/O error occurs 102 */ 103 public void expand(final ArchiveInputStream archive, final File targetDirectory) throws IOException { 104 expand(archive, toPath(targetDirectory)); 105 } 106 107 /** 108 * Expands {@code archive} into {@code targetDirectory}. 109 * 110 * @param archive the file to expand 111 * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows. 112 * @throws IOException if an I/O error occurs 113 * @since 1.22 114 */ 115 public void expand(final ArchiveInputStream archive, final Path targetDirectory) throws IOException { 116 expand(() -> { 117 ArchiveEntry next = archive.getNextEntry(); 118 while (next != null && !archive.canReadEntryData(next)) { 119 next = archive.getNextEntry(); 120 } 121 return next; 122 }, (entry, out) -> IOUtils.copy(archive, out), targetDirectory); 123 } 124 125 /** 126 * Expands {@code archive} into {@code targetDirectory}. 127 * 128 * <p>Tries to auto-detect the archive's format.</p> 129 * 130 * @param archive the file to expand 131 * @param targetDirectory the target directory 132 * @throws IOException if an I/O error occurs 133 * @throws ArchiveException if the archive cannot be read for other reasons 134 */ 135 public void expand(final File archive, final File targetDirectory) throws IOException, ArchiveException { 136 expand(archive.toPath(), toPath(targetDirectory)); 137 } 138 139 /** 140 * Expands {@code archive} into {@code targetDirectory}. 141 * 142 * <p>Tries to auto-detect the archive's format.</p> 143 * 144 * <p>This method creates a wrapper around the archive stream 145 * which is never closed and thus leaks resources, please use 146 * {@link #expand(InputStream,File,CloseableConsumer)} 147 * instead.</p> 148 * 149 * @param archive the file to expand 150 * @param targetDirectory the target directory 151 * @throws IOException if an I/O error occurs 152 * @throws ArchiveException if the archive cannot be read for other reasons 153 * @deprecated this method leaks resources 154 */ 155 @Deprecated 156 public void expand(final InputStream archive, final File targetDirectory) throws IOException, ArchiveException { 157 expand(archive, targetDirectory, CloseableConsumer.NULL_CONSUMER); 158 } 159 160 /** 161 * Expands {@code archive} into {@code targetDirectory}. 162 * 163 * <p>Tries to auto-detect the archive's format.</p> 164 * 165 * <p>This method creates a wrapper around the archive stream and 166 * the caller of this method is responsible for closing it - 167 * probably at the same time as closing the stream itself. The 168 * caller is informed about the wrapper object via the {@code 169 * closeableConsumer} callback as soon as it is no longer needed 170 * by this class.</p> 171 * 172 * @param archive the file to expand 173 * @param targetDirectory the target directory 174 * @param closeableConsumer is informed about the stream wrapped around the passed in stream 175 * @throws IOException if an I/O error occurs 176 * @throws ArchiveException if the archive cannot be read for other reasons 177 * @since 1.19 178 */ 179 public void expand(final InputStream archive, final File targetDirectory, final CloseableConsumer closeableConsumer) 180 throws IOException, ArchiveException { 181 try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) { 182 expand(c.track(ArchiveStreamFactory.DEFAULT.createArchiveInputStream(archive)), 183 targetDirectory); 184 } 185 } 186 187 /** 188 * Expands {@code archive} into {@code targetDirectory}. 189 * 190 * <p>Tries to auto-detect the archive's format.</p> 191 * 192 * @param archive the file to expand 193 * @param targetDirectory the target directory 194 * @throws IOException if an I/O error occurs 195 * @throws ArchiveException if the archive cannot be read for other reasons 196 * @since 1.22 197 */ 198 public void expand(final Path archive, final Path targetDirectory) throws IOException, ArchiveException { 199 String format = null; 200 try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(archive))) { 201 format = ArchiveStreamFactory.detect(inputStream); 202 } 203 expand(format, archive, targetDirectory); 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 expand(c.track(ArchiveStreamFactory.DEFAULT.createArchiveInputStream(format, archive)), 321 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((TarArchiveEntry) 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((ZipArchiveEntry) 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}