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.File; 022import java.io.IOException; 023import java.io.OutputStream; 024import java.nio.channels.Channels; 025import java.nio.channels.FileChannel; 026import java.nio.channels.SeekableByteChannel; 027import java.nio.file.FileVisitOption; 028import java.nio.file.FileVisitResult; 029import java.nio.file.Files; 030import java.nio.file.LinkOption; 031import java.nio.file.Path; 032import java.nio.file.SimpleFileVisitor; 033import java.nio.file.StandardOpenOption; 034import java.nio.file.attribute.BasicFileAttributes; 035import java.util.EnumSet; 036import java.util.Objects; 037 038import org.apache.commons.compress.archivers.ArchiveEntry; 039import org.apache.commons.compress.archivers.ArchiveException; 040import org.apache.commons.compress.archivers.ArchiveOutputStream; 041import org.apache.commons.compress.archivers.ArchiveStreamFactory; 042import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile; 043import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; 044import org.apache.commons.compress.utils.IOUtils; 045 046/** 047 * Provides a high level API for creating archives. 048 * 049 * @since 1.17 050 * @since 1.21 Supports {@link Path}. 051 */ 052public class Archiver { 053 054 private static class ArchiverFileVisitor extends SimpleFileVisitor<Path> { 055 056 private final ArchiveOutputStream target; 057 private final Path directory; 058 private final LinkOption[] linkOptions; 059 060 private ArchiverFileVisitor(final ArchiveOutputStream target, final Path directory, 061 final LinkOption... linkOptions) { 062 this.target = target; 063 this.directory = directory; 064 this.linkOptions = linkOptions == null ? IOUtils.EMPTY_LINK_OPTIONS : linkOptions.clone(); 065 } 066 067 @Override 068 public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) throws IOException { 069 return visit(dir, attrs, false); 070 } 071 072 protected FileVisitResult visit(final Path path, final BasicFileAttributes attrs, final boolean isFile) 073 throws IOException { 074 Objects.requireNonNull(path); 075 Objects.requireNonNull(attrs); 076 final String name = directory.relativize(path).toString().replace('\\', '/'); 077 if (!name.isEmpty()) { 078 final ArchiveEntry archiveEntry = target.createArchiveEntry(path, 079 isFile || name.endsWith("/") ? name : name + "/", linkOptions); 080 target.putArchiveEntry(archiveEntry); 081 if (isFile) { 082 // Refactor this as a BiConsumer on Java 8 083 Files.copy(path, target); 084 } 085 target.closeArchiveEntry(); 086 } 087 return FileVisitResult.CONTINUE; 088 } 089 090 @Override 091 public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException { 092 return visit(file, attrs, true); 093 } 094 } 095 096 /** 097 * No {@link FileVisitOption}. 098 */ 099 public static final EnumSet<FileVisitOption> EMPTY_FileVisitOption = EnumSet.noneOf(FileVisitOption.class); 100 101 /** 102 * Creates an archive {@code target} by recursively including all files and directories in {@code directory}. 103 * 104 * @param target the stream to write the new archive to. 105 * @param directory the directory that contains the files to archive. 106 * @throws IOException if an I/O error occurs 107 * @throws ArchiveException if the archive cannot be created for other reasons 108 */ 109 public void create(final ArchiveOutputStream target, final File directory) throws IOException, ArchiveException { 110 create(target, directory.toPath(), EMPTY_FileVisitOption); 111 } 112 113 /** 114 * Creates an archive {@code target} by recursively including all files and directories in {@code directory}. 115 * 116 * @param target the stream to write the new archive to. 117 * @param directory the directory that contains the files to archive. 118 * @param fileVisitOptions linkOptions to configure the traversal of the source {@code directory}. 119 * @param linkOptions indicating how symbolic links are handled. 120 * @throws IOException if an I/O error occurs or the archive cannot be created for other reasons. 121 * @since 1.21 122 */ 123 public void create(final ArchiveOutputStream target, final Path directory, 124 final EnumSet<FileVisitOption> fileVisitOptions, final LinkOption... linkOptions) throws IOException { 125 Files.walkFileTree(directory, fileVisitOptions, Integer.MAX_VALUE, 126 new ArchiverFileVisitor(target, directory, linkOptions)); 127 target.finish(); 128 } 129 130 /** 131 * Creates an archive {@code target} by recursively including all files and directories in {@code directory}. 132 * 133 * @param target the stream to write the new archive to. 134 * @param directory the directory that contains the files to archive. 135 * @throws IOException if an I/O error occurs or the archive cannot be created for other reasons. 136 * @since 1.21 137 */ 138 public void create(final ArchiveOutputStream target, final Path directory) throws IOException { 139 create(target, directory, EMPTY_FileVisitOption); 140 } 141 142 /** 143 * Creates an archive {@code target} by recursively including all files and directories in {@code directory}. 144 * 145 * @param target the file to write the new archive to. 146 * @param directory the directory that contains the files to archive. 147 * @throws IOException if an I/O error occurs 148 */ 149 public void create(final SevenZOutputFile target, final File directory) throws IOException { 150 create(target, directory.toPath()); 151 } 152 153 /** 154 * Creates an archive {@code target} by recursively including all files and directories in {@code directory}. 155 * 156 * @param target the file to write the new archive to. 157 * @param directory the directory that contains the files to archive. 158 * @throws IOException if an I/O error occurs 159 * @since 1.21 160 */ 161 public void create(final SevenZOutputFile target, final Path directory) throws IOException { 162 // This custom SimpleFileVisitor goes away with Java 8's BiConsumer. 163 Files.walkFileTree(directory, new ArchiverFileVisitor(null, directory) { 164 165 @Override 166 protected FileVisitResult visit(final Path path, final BasicFileAttributes attrs, final boolean isFile) 167 throws IOException { 168 Objects.requireNonNull(path); 169 Objects.requireNonNull(attrs); 170 final String name = directory.relativize(path).toString().replace('\\', '/'); 171 if (!name.isEmpty()) { 172 final ArchiveEntry archiveEntry = target.createArchiveEntry(path, 173 isFile || name.endsWith("/") ? name : name + "/"); 174 target.putArchiveEntry(archiveEntry); 175 if (isFile) { 176 // Refactor this as a BiConsumer on Java 8 177 target.write(path); 178 } 179 target.closeArchiveEntry(); 180 } 181 return FileVisitResult.CONTINUE; 182 } 183 184 }); 185 target.finish(); 186 } 187 188 /** 189 * Creates an archive {@code target} using the format {@code 190 * format} by recursively including all files and directories in {@code directory}. 191 * 192 * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}. 193 * @param target the file to write the new archive to. 194 * @param directory the directory that contains the files to archive. 195 * @throws IOException if an I/O error occurs 196 * @throws ArchiveException if the archive cannot be created for other reasons 197 */ 198 public void create(final String format, final File target, final File directory) 199 throws IOException, ArchiveException { 200 create(format, target.toPath(), directory.toPath()); 201 } 202 203 /** 204 * Creates an archive {@code target} using the format {@code 205 * format} by recursively including all files and directories in {@code directory}. 206 * 207 * <p> 208 * This method creates a wrapper around the target stream which is never closed and thus leaks resources, please use 209 * {@link #create(String,OutputStream,File,CloseableConsumer)} instead. 210 * </p> 211 * 212 * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}. 213 * @param target the stream to write the new archive to. 214 * @param directory the directory that contains the files to archive. 215 * @throws IOException if an I/O error occurs 216 * @throws ArchiveException if the archive cannot be created for other reasons 217 * @deprecated this method leaks resources 218 */ 219 @Deprecated 220 public void create(final String format, final OutputStream target, final File directory) 221 throws IOException, ArchiveException { 222 create(format, target, directory, CloseableConsumer.NULL_CONSUMER); 223 } 224 225 /** 226 * Creates an archive {@code target} using the format {@code 227 * format} by recursively including all files and directories in {@code directory}. 228 * 229 * <p> 230 * This method creates a wrapper around the archive stream and the caller of this method is responsible for closing 231 * it - probably at the same time as closing the stream itself. The caller is informed about the wrapper object via 232 * the {@code 233 * closeableConsumer} callback as soon as it is no longer needed by this class. 234 * </p> 235 * 236 * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}. 237 * @param target the stream to write the new archive to. 238 * @param directory the directory that contains the files to archive. 239 * @param closeableConsumer is informed about the stream wrapped around the passed in stream 240 * @throws IOException if an I/O error occurs 241 * @throws ArchiveException if the archive cannot be created for other reasons 242 * @since 1.19 243 */ 244 public void create(final String format, final OutputStream target, final File directory, 245 final CloseableConsumer closeableConsumer) throws IOException, ArchiveException { 246 try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) { 247 create(c.track(ArchiveStreamFactory.DEFAULT.createArchiveOutputStream(format, target)), directory); 248 } 249 } 250 251 /** 252 * Creates an archive {@code target} using the format {@code 253 * format} by recursively including all files and directories in {@code directory}. 254 * 255 * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}. 256 * @param target the file to write the new archive to. 257 * @param directory the directory that contains the files to archive. 258 * @throws IOException if an I/O error occurs 259 * @throws ArchiveException if the archive cannot be created for other reasons 260 * @since 1.21 261 */ 262 public void create(final String format, final Path target, final Path directory) 263 throws IOException, ArchiveException { 264 if (prefersSeekableByteChannel(format)) { 265 try (SeekableByteChannel channel = FileChannel.open(target, StandardOpenOption.WRITE, 266 StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { 267 create(format, channel, directory); 268 return; 269 } 270 } 271 try (@SuppressWarnings("resource") // ArchiveOutputStream wraps newOutputStream result 272 ArchiveOutputStream outputStream = ArchiveStreamFactory.DEFAULT.createArchiveOutputStream(format, 273 Files.newOutputStream(target))) { 274 create(outputStream, directory, EMPTY_FileVisitOption); 275 } 276 } 277 278 /** 279 * Creates an archive {@code target} using the format {@code 280 * format} by recursively including all files and directories in {@code directory}. 281 * 282 * <p> 283 * This method creates a wrapper around the target channel which is never closed and thus leaks resources, please 284 * use {@link #create(String,SeekableByteChannel,File,CloseableConsumer)} instead. 285 * </p> 286 * 287 * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}. 288 * @param target the channel to write the new archive to. 289 * @param directory the directory that contains the files to archive. 290 * @throws IOException if an I/O error occurs 291 * @throws ArchiveException if the archive cannot be created for other reasons 292 * @deprecated this method leaks resources 293 */ 294 @Deprecated 295 public void create(final String format, final SeekableByteChannel target, final File directory) 296 throws IOException, ArchiveException { 297 create(format, target, directory, CloseableConsumer.NULL_CONSUMER); 298 } 299 300 /** 301 * Creates an archive {@code target} using the format {@code 302 * format} by recursively including all files and directories in {@code directory}. 303 * 304 * <p> 305 * This method creates a wrapper around the archive channel and the caller of this method is responsible for closing 306 * it - probably at the same time as closing the channel itself. The caller is informed about the wrapper object via 307 * the {@code 308 * closeableConsumer} callback as soon as it is no longer needed by this class. 309 * </p> 310 * 311 * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}. 312 * @param target the channel to write the new archive to. 313 * @param directory the directory that contains the files to archive. 314 * @param closeableConsumer is informed about the stream wrapped around the passed in stream 315 * @throws IOException if an I/O error occurs 316 * @throws ArchiveException if the archive cannot be created for other reasons 317 * @since 1.19 318 */ 319 public void create(final String format, final SeekableByteChannel target, final File directory, 320 final CloseableConsumer closeableConsumer) throws IOException, ArchiveException { 321 try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) { 322 if (!prefersSeekableByteChannel(format)) { 323 create(format, c.track(Channels.newOutputStream(target)), directory); 324 } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) { 325 create(c.track(new ZipArchiveOutputStream(target)), directory); 326 } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) { 327 create(c.track(new SevenZOutputFile(target)), directory); 328 } else { 329 // never reached as prefersSeekableByteChannel only returns true for ZIP and 7z 330 throw new ArchiveException("Don't know how to handle format " + format); 331 } 332 } 333 } 334 335 /** 336 * Creates an archive {@code target} using the format {@code 337 * format} by recursively including all files and directories in {@code directory}. 338 * 339 * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}. 340 * @param target the channel to write the new archive to. 341 * @param directory the directory that contains the files to archive. 342 * @throws IOException if an I/O error occurs 343 * @throws IllegalStateException if the format does not support {@code SeekableByteChannel}. 344 */ 345 public void create(final String format, final SeekableByteChannel target, final Path directory) throws IOException { 346 if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) { 347 try (SevenZOutputFile sevenZFile = new SevenZOutputFile(target)) { 348 create(sevenZFile, directory); 349 } 350 } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) { 351 try (ArchiveOutputStream archiveOutputStream = new ZipArchiveOutputStream(target)) { 352 create(archiveOutputStream, directory, EMPTY_FileVisitOption); 353 } 354 } else { 355 throw new IllegalStateException(format); 356 } 357 } 358 359 private boolean prefersSeekableByteChannel(final String format) { 360 return ArchiveStreamFactory.ZIP.equalsIgnoreCase(format) 361 || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format); 362 } 363}