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