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); 088 } 089 return; 090 } 091 try (InputStream i = new BufferedInputStream(Files.newInputStream(archive.toPath()))) { 092 expand(format, i, targetDirectory); 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 * @param archive the file to expand 102 * @param targetDirectory the directory to write to 103 * @throws IOException if an I/O error occurs 104 * @throws ArchiveException if the archive cannot be read for other reasons 105 */ 106 public void expand(InputStream archive, File targetDirectory) throws IOException, ArchiveException { 107 expand(new ArchiveStreamFactory().createArchiveInputStream(archive), targetDirectory); 108 } 109 110 /** 111 * Expands {@code archive} into {@code targetDirectory}. 112 * 113 * @param archive the file to expand 114 * @param targetDirectory the directory to write to 115 * @param format the archive format. This uses the same format as 116 * accepted by {@link ArchiveStreamFactory}. 117 * @throws IOException if an I/O error occurs 118 * @throws ArchiveException if the archive cannot be read for other reasons 119 */ 120 public void expand(String format, InputStream archive, File targetDirectory) 121 throws IOException, ArchiveException { 122 expand(new ArchiveStreamFactory().createArchiveInputStream(format, archive), targetDirectory); 123 } 124 125 /** 126 * Expands {@code archive} into {@code targetDirectory}. 127 * 128 * @param archive the file to expand 129 * @param targetDirectory the directory to write to 130 * @param format the archive format. This uses the same format as 131 * accepted by {@link ArchiveStreamFactory}. 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(String format, SeekableByteChannel archive, File targetDirectory) 136 throws IOException, ArchiveException { 137 if (!prefersSeekableByteChannel(format)) { 138 expand(format, Channels.newInputStream(archive), targetDirectory); 139 } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) { 140 expand(new ZipFile(archive), targetDirectory); 141 } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) { 142 expand(new SevenZFile(archive), targetDirectory); 143 } else { 144 // never reached as prefersSeekableByteChannel only returns true for ZIP and 7z 145 throw new ArchiveException("don't know how to handle format " + format); 146 } 147 } 148 149 /** 150 * Expands {@code archive} into {@code targetDirectory}. 151 * 152 * @param archive the file to expand 153 * @param targetDirectory the directory to write to 154 * @throws IOException if an I/O error occurs 155 * @throws ArchiveException if the archive cannot be read for other reasons 156 */ 157 public void expand(final ArchiveInputStream archive, File targetDirectory) 158 throws IOException, ArchiveException { 159 expand(new ArchiveEntrySupplier() { 160 @Override 161 public ArchiveEntry getNextReadableEntry() throws IOException { 162 ArchiveEntry next = archive.getNextEntry(); 163 while (next != null && !archive.canReadEntryData(next)) { 164 next = archive.getNextEntry(); 165 } 166 return next; 167 } 168 }, new EntryWriter() { 169 @Override 170 public void writeEntryDataTo(ArchiveEntry entry, OutputStream out) throws IOException { 171 IOUtils.copy(archive, out); 172 } 173 }, targetDirectory); 174 } 175 176 /** 177 * Expands {@code archive} into {@code targetDirectory}. 178 * 179 * @param archive the file to expand 180 * @param targetDirectory the directory to write to 181 * @throws IOException if an I/O error occurs 182 * @throws ArchiveException if the archive cannot be read for other reasons 183 */ 184 public void expand(final ZipFile archive, File targetDirectory) 185 throws IOException, ArchiveException { 186 final Enumeration<ZipArchiveEntry> entries = archive.getEntries(); 187 expand(new ArchiveEntrySupplier() { 188 @Override 189 public ArchiveEntry getNextReadableEntry() throws IOException { 190 ZipArchiveEntry next = entries.hasMoreElements() ? entries.nextElement() : null; 191 while (next != null && !archive.canReadEntryData(next)) { 192 next = entries.hasMoreElements() ? entries.nextElement() : null; 193 } 194 return next; 195 } 196 }, new EntryWriter() { 197 @Override 198 public void writeEntryDataTo(ArchiveEntry entry, OutputStream out) throws IOException { 199 try (InputStream in = archive.getInputStream((ZipArchiveEntry) entry)) { 200 IOUtils.copy(in, out); 201 } 202 } 203 }, targetDirectory); 204 } 205 206 /** 207 * Expands {@code archive} into {@code targetDirectory}. 208 * 209 * @param archive the file to expand 210 * @param targetDirectory the directory to write to 211 * @throws IOException if an I/O error occurs 212 * @throws ArchiveException if the archive cannot be read for other reasons 213 */ 214 public void expand(final SevenZFile archive, File targetDirectory) 215 throws IOException, ArchiveException { 216 expand(new ArchiveEntrySupplier() { 217 @Override 218 public ArchiveEntry getNextReadableEntry() throws IOException { 219 return archive.getNextEntry(); 220 } 221 }, new EntryWriter() { 222 @Override 223 public void writeEntryDataTo(ArchiveEntry entry, OutputStream out) throws IOException { 224 final byte[] buffer = new byte[8024]; 225 int n = 0; 226 long count = 0; 227 while (-1 != (n = archive.read(buffer))) { 228 out.write(buffer, 0, n); 229 count += n; 230 } 231 } 232 }, targetDirectory); 233 } 234 235 private boolean prefersSeekableByteChannel(String format) { 236 return ArchiveStreamFactory.ZIP.equalsIgnoreCase(format) || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format); 237 } 238 239 private void expand(ArchiveEntrySupplier supplier, EntryWriter writer, File targetDirectory) 240 throws IOException { 241 String targetDirPath = targetDirectory.getCanonicalPath(); 242 if (!targetDirPath.endsWith(File.separator)) { 243 targetDirPath += File.separatorChar; 244 } 245 ArchiveEntry nextEntry = supplier.getNextReadableEntry(); 246 while (nextEntry != null) { 247 File f = new File(targetDirectory, nextEntry.getName()); 248 if (!f.getCanonicalPath().startsWith(targetDirPath)) { 249 throw new IOException("expanding " + nextEntry.getName() 250 + " would create file outside of " + targetDirectory); 251 } 252 if (nextEntry.isDirectory()) { 253 if (!f.isDirectory() && !f.mkdirs()) { 254 throw new IOException("failed to create directory " + f); 255 } 256 } else { 257 File parent = f.getParentFile(); 258 if (!parent.isDirectory() && !parent.mkdirs()) { 259 throw new IOException("failed to create directory " + parent); 260 } 261 try (OutputStream o = Files.newOutputStream(f.toPath())) { 262 writer.writeEntryDataTo(nextEntry, o); 263 } 264 } 265 nextEntry = supplier.getNextReadableEntry(); 266 } 267 } 268 269}