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.StandardOpenOption; 031import java.util.Enumeration; 032import java.util.Iterator; 033 034import org.apache.commons.compress.archivers.ArchiveEntry; 035import org.apache.commons.compress.archivers.ArchiveException; 036import org.apache.commons.compress.archivers.ArchiveInputStream; 037import org.apache.commons.compress.archivers.ArchiveStreamFactory; 038import org.apache.commons.compress.archivers.sevenz.SevenZFile; 039import org.apache.commons.compress.archivers.tar.TarArchiveEntry; 040import org.apache.commons.compress.archivers.tar.TarFile; 041import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; 042import org.apache.commons.compress.archivers.zip.ZipFile; 043import org.apache.commons.compress.utils.IOUtils; 044 045/** 046 * Provides a high level API for expanding archives. 047 * @since 1.17 048 */ 049public class Expander { 050 051 private interface ArchiveEntrySupplier { 052 ArchiveEntry getNextReadableEntry() throws IOException; 053 } 054 055 private interface EntryWriter { 056 void writeEntryDataTo(ArchiveEntry entry, OutputStream out) throws IOException; 057 } 058 059 /** 060 * Expands {@code archive} into {@code targetDirectory}. 061 * 062 * <p>Tries to auto-detect the archive's format.</p> 063 * 064 * @param archive the file to expand 065 * @param targetDirectory the directory to write to 066 * @throws IOException if an I/O error occurs 067 * @throws ArchiveException if the archive cannot be read for other reasons 068 */ 069 public void expand(final File archive, final File targetDirectory) throws IOException, ArchiveException { 070 String format = null; 071 try (InputStream i = new BufferedInputStream(Files.newInputStream(archive.toPath()))) { 072 format = ArchiveStreamFactory.detect(i); 073 } 074 expand(format, archive, targetDirectory); 075 } 076 077 /** 078 * Expands {@code archive} into {@code targetDirectory}. 079 * 080 * @param archive the file to expand 081 * @param targetDirectory the directory to write to 082 * @param format the archive format. This uses the same format as 083 * accepted by {@link ArchiveStreamFactory}. 084 * @throws IOException if an I/O error occurs 085 * @throws ArchiveException if the archive cannot be read for other reasons 086 */ 087 public void expand(final String format, final File archive, final File targetDirectory) throws IOException, ArchiveException { 088 if (prefersSeekableByteChannel(format)) { 089 try (SeekableByteChannel c = FileChannel.open(archive.toPath(), StandardOpenOption.READ)) { 090 expand(format, c, targetDirectory, CloseableConsumer.CLOSING_CONSUMER); 091 } 092 return; 093 } 094 try (InputStream i = new BufferedInputStream(Files.newInputStream(archive.toPath()))) { 095 expand(format, i, targetDirectory, CloseableConsumer.CLOSING_CONSUMER); 096 } 097 } 098 099 /** 100 * Expands {@code archive} into {@code targetDirectory}. 101 * 102 * <p>Tries to auto-detect the archive's format.</p> 103 * 104 * <p>This method creates a wrapper around the archive stream 105 * which is never closed and thus leaks resources, please use 106 * {@link #expand(InputStream,File,CloseableConsumer)} 107 * instead.</p> 108 * 109 * @param archive the file to expand 110 * @param targetDirectory the directory to write to 111 * @throws IOException if an I/O error occurs 112 * @throws ArchiveException if the archive cannot be read for other reasons 113 * @deprecated this method leaks resources 114 */ 115 @Deprecated 116 public void expand(final InputStream archive, final File targetDirectory) throws IOException, ArchiveException { 117 expand(archive, targetDirectory, CloseableConsumer.NULL_CONSUMER); 118 } 119 120 /** 121 * Expands {@code archive} into {@code targetDirectory}. 122 * 123 * <p>Tries to auto-detect the archive's format.</p> 124 * 125 * <p>This method creates a wrapper around the archive stream and 126 * the caller of this method is responsible for closing it - 127 * probably at the same time as closing the stream itself. The 128 * caller is informed about the wrapper object via the {@code 129 * closeableConsumer} callback as soon as it is no longer needed 130 * by this class.</p> 131 * 132 * @param archive the file to expand 133 * @param targetDirectory the directory to write to 134 * @param closeableConsumer is informed about the stream wrapped around the passed in stream 135 * @throws IOException if an I/O error occurs 136 * @throws ArchiveException if the archive cannot be read for other reasons 137 * @since 1.19 138 */ 139 public void expand(final InputStream archive, final File targetDirectory, final CloseableConsumer closeableConsumer) 140 throws IOException, ArchiveException { 141 try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) { 142 expand(c.track(ArchiveStreamFactory.DEFAULT.createArchiveInputStream(archive)), 143 targetDirectory); 144 } 145 } 146 147 /** 148 * Expands {@code archive} into {@code targetDirectory}. 149 * 150 * <p>This method creates a wrapper around the archive stream 151 * which is never closed and thus leaks resources, please use 152 * {@link #expand(String,InputStream,File,CloseableConsumer)} 153 * instead.</p> 154 * 155 * @param archive the file to expand 156 * @param targetDirectory the directory to write to 157 * @param format the archive format. This uses the same format as 158 * accepted by {@link ArchiveStreamFactory}. 159 * @throws IOException if an I/O error occurs 160 * @throws ArchiveException if the archive cannot be read for other reasons 161 * @deprecated this method leaks resources 162 */ 163 @Deprecated 164 public void expand(final String format, final InputStream archive, final File targetDirectory) 165 throws IOException, ArchiveException { 166 expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER); 167 } 168 169 /** 170 * Expands {@code archive} into {@code targetDirectory}. 171 * 172 * <p>This method creates a wrapper around the archive stream and 173 * the caller of this method is responsible for closing it - 174 * probably at the same time as closing the stream itself. The 175 * caller is informed about the wrapper object via the {@code 176 * closeableConsumer} callback as soon as it is no longer needed 177 * by this class.</p> 178 * 179 * @param archive the file to expand 180 * @param targetDirectory the directory to write to 181 * @param format the archive format. This uses the same format as 182 * accepted by {@link ArchiveStreamFactory}. 183 * @param closeableConsumer is informed about the stream wrapped around the passed in stream 184 * @throws IOException if an I/O error occurs 185 * @throws ArchiveException if the archive cannot be read for other reasons 186 * @since 1.19 187 */ 188 public void expand(final String format, final InputStream archive, final File targetDirectory, final CloseableConsumer closeableConsumer) 189 throws IOException, ArchiveException { 190 try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) { 191 expand(c.track(ArchiveStreamFactory.DEFAULT.createArchiveInputStream(format, archive)), 192 targetDirectory); 193 } 194 } 195 196 /** 197 * Expands {@code archive} into {@code targetDirectory}. 198 * 199 * <p>This method creates a wrapper around the archive channel 200 * which is never closed and thus leaks resources, please use 201 * {@link #expand(String,SeekableByteChannel,File,CloseableConsumer)} 202 * instead.</p> 203 * 204 * @param archive the file to expand 205 * @param targetDirectory the directory to write to 206 * @param format the archive format. This uses the same format as 207 * accepted by {@link ArchiveStreamFactory}. 208 * @throws IOException if an I/O error occurs 209 * @throws ArchiveException if the archive cannot be read for other reasons 210 * @deprecated this method leaks resources 211 */ 212 @Deprecated 213 public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory) 214 throws IOException, ArchiveException { 215 expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER); 216 } 217 218 /** 219 * Expands {@code archive} into {@code targetDirectory}. 220 * 221 * <p>This method creates a wrapper around the archive channel and 222 * the caller of this method is responsible for closing it - 223 * probably at the same time as closing the channel itself. The 224 * caller is informed about the wrapper object via the {@code 225 * closeableConsumer} callback as soon as it is no longer needed 226 * by this class.</p> 227 * 228 * @param archive the file to expand 229 * @param targetDirectory the directory to write to 230 * @param format the archive format. This uses the same format as 231 * accepted by {@link ArchiveStreamFactory}. 232 * @param closeableConsumer is informed about the stream wrapped around the passed in channel 233 * @throws IOException if an I/O error occurs 234 * @throws ArchiveException if the archive cannot be read for other reasons 235 * @since 1.19 236 */ 237 public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory, 238 final CloseableConsumer closeableConsumer) 239 throws IOException, ArchiveException { 240 try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) { 241 if (!prefersSeekableByteChannel(format)) { 242 expand(format, c.track(Channels.newInputStream(archive)), targetDirectory); 243 } else if (ArchiveStreamFactory.TAR.equalsIgnoreCase(format)) { 244 expand(c.track(new TarFile(archive)), targetDirectory); 245 } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) { 246 expand(c.track(new ZipFile(archive)), targetDirectory); 247 } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) { 248 expand(c.track(new SevenZFile(archive)), targetDirectory); 249 } else { 250 // never reached as prefersSeekableByteChannel only returns true for TAR, ZIP and 7z 251 throw new ArchiveException("Don't know how to handle format " + format); 252 } 253 } 254 } 255 256 /** 257 * Expands {@code archive} into {@code targetDirectory}. 258 * 259 * @param archive the file to expand 260 * @param targetDirectory the directory to write to 261 * @throws IOException if an I/O error occurs 262 * @throws ArchiveException if the archive cannot be read for other reasons 263 */ 264 public void expand(final ArchiveInputStream archive, final File targetDirectory) 265 throws IOException, ArchiveException { 266 expand(() -> { 267 ArchiveEntry next = archive.getNextEntry(); 268 while (next != null && !archive.canReadEntryData(next)) { 269 next = archive.getNextEntry(); 270 } 271 return next; 272 }, (entry, out) -> IOUtils.copy(archive, out), targetDirectory); 273 } 274 275 /** 276 * Expands {@code archive} into {@code targetDirectory}. 277 * 278 * @param archive the file to expand 279 * @param targetDirectory the directory to write to 280 * @throws IOException if an I/O error occurs 281 * @throws ArchiveException if the archive cannot be read for other reasons 282 * @since 1.21 283 */ 284 public void expand(final TarFile archive, final File targetDirectory) 285 throws IOException, ArchiveException { 286 final Iterator<TarArchiveEntry> entryIterator = archive.getEntries().iterator(); 287 expand(() -> entryIterator.hasNext() ? entryIterator.next() : null, 288 (entry, out) -> { 289 try (InputStream in = archive.getInputStream((TarArchiveEntry) entry)) { 290 IOUtils.copy(in, out); 291 } 292 }, targetDirectory); 293 } 294 295 /** 296 * Expands {@code archive} into {@code targetDirectory}. 297 * 298 * @param archive the file to expand 299 * @param targetDirectory the directory to write to 300 * @throws IOException if an I/O error occurs 301 * @throws ArchiveException if the archive cannot be read for other reasons 302 */ 303 public void expand(final ZipFile archive, final File targetDirectory) 304 throws IOException, ArchiveException { 305 final Enumeration<ZipArchiveEntry> entries = archive.getEntries(); 306 expand(() -> { 307 ZipArchiveEntry next = entries.hasMoreElements() ? entries.nextElement() : null; 308 while (next != null && !archive.canReadEntryData(next)) { 309 next = entries.hasMoreElements() ? entries.nextElement() : null; 310 } 311 return next; 312 }, (entry, out) -> { 313 try (InputStream in = archive.getInputStream((ZipArchiveEntry) entry)) { 314 IOUtils.copy(in, out); 315 } 316 }, targetDirectory); 317 } 318 319 /** 320 * Expands {@code archive} into {@code targetDirectory}. 321 * 322 * @param archive the file to expand 323 * @param targetDirectory the directory to write to 324 * @throws IOException if an I/O error occurs 325 * @throws ArchiveException if the archive cannot be read for other reasons 326 */ 327 public void expand(final SevenZFile archive, final File targetDirectory) 328 throws IOException, ArchiveException { 329 expand(archive::getNextEntry, (entry, out) -> { 330 final byte[] buffer = new byte[8192]; 331 int n; 332 while (-1 != (n = archive.read(buffer))) { 333 out.write(buffer, 0, n); 334 } 335 }, targetDirectory); 336 } 337 338 private boolean prefersSeekableByteChannel(final String format) { 339 return ArchiveStreamFactory.TAR.equalsIgnoreCase(format) 340 || ArchiveStreamFactory.ZIP.equalsIgnoreCase(format) 341 || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format); 342 } 343 344 private void expand(final ArchiveEntrySupplier supplier, final EntryWriter writer, final File targetDirectory) 345 throws IOException { 346 String targetDirPath = targetDirectory.getCanonicalPath(); 347 if (!targetDirPath.endsWith(File.separator)) { 348 targetDirPath += File.separator; 349 } 350 ArchiveEntry nextEntry = supplier.getNextReadableEntry(); 351 while (nextEntry != null) { 352 final File f = new File(targetDirectory, nextEntry.getName()); 353 if (!f.getCanonicalPath().startsWith(targetDirPath)) { 354 throw new IOException("Expanding " + nextEntry.getName() 355 + " would create file outside of " + targetDirectory); 356 } 357 if (nextEntry.isDirectory()) { 358 if (!f.isDirectory() && !f.mkdirs()) { 359 throw new IOException("Failed to create directory " + f); 360 } 361 } else { 362 final File parent = f.getParentFile(); 363 if (!parent.isDirectory() && !parent.mkdirs()) { 364 throw new IOException("Failed to create directory " + parent); 365 } 366 try (OutputStream o = Files.newOutputStream(f.toPath())) { 367 writer.writeEntryDataTo(nextEntry, o); 368 } 369 } 370 nextEntry = supplier.getNextReadableEntry(); 371 } 372 } 373 374}