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 */ 108 public void create(final ArchiveOutputStream target, final File directory) throws IOException { 109 create(target, directory.toPath(), EMPTY_FileVisitOption); 110 } 111 112 /** 113 * Creates an archive {@code target} by recursively including all files and directories in {@code directory}. 114 * 115 * @param target the stream to write the new archive to. 116 * @param directory the directory that contains the files to archive. 117 * @throws IOException if an I/O error occurs or the archive cannot be created for other reasons. 118 * @since 1.21 119 */ 120 public void create(final ArchiveOutputStream target, final Path directory) throws IOException { 121 create(target, directory, EMPTY_FileVisitOption); 122 } 123 124 /** 125 * Creates an archive {@code target} by recursively including all files and directories in {@code directory}. 126 * 127 * @param target the stream to write the new archive to. 128 * @param directory the directory that contains the files to archive. 129 * @param fileVisitOptions linkOptions to configure the traversal of the source {@code directory}. 130 * @param linkOptions indicating how symbolic links are handled. 131 * @throws IOException if an I/O error occurs or the archive cannot be created for other reasons. 132 * @since 1.21 133 */ 134 public void create(final ArchiveOutputStream target, final Path directory, 135 final EnumSet<FileVisitOption> fileVisitOptions, final LinkOption... linkOptions) throws IOException { 136 Files.walkFileTree(directory, fileVisitOptions, Integer.MAX_VALUE, 137 new ArchiverFileVisitor(target, directory, linkOptions)); 138 target.finish(); 139 } 140 141 /** 142 * Creates an archive {@code target} by recursively including all files and directories in {@code directory}. 143 * 144 * @param target the file to write the new archive to. 145 * @param directory the directory that contains the files to archive. 146 * @throws IOException if an I/O error occurs 147 */ 148 public void create(final SevenZOutputFile target, final File directory) throws IOException { 149 create(target, directory.toPath()); 150 } 151 152 /** 153 * Creates an archive {@code target} by recursively including all files and directories in {@code directory}. 154 * 155 * @param target the file to write the new archive to. 156 * @param directory the directory that contains the files to archive. 157 * @throws IOException if an I/O error occurs 158 * @since 1.21 159 */ 160 public void create(final SevenZOutputFile target, final Path directory) throws IOException { 161 // This custom SimpleFileVisitor goes away with Java 8's BiConsumer. 162 Files.walkFileTree(directory, new ArchiverFileVisitor(null, directory) { 163 164 @Override 165 protected FileVisitResult visit(final Path path, final BasicFileAttributes attrs, final boolean isFile) 166 throws IOException { 167 Objects.requireNonNull(path); 168 Objects.requireNonNull(attrs); 169 final String name = directory.relativize(path).toString().replace('\\', '/'); 170 if (!name.isEmpty()) { 171 final ArchiveEntry archiveEntry = target.createArchiveEntry(path, 172 isFile || name.endsWith("/") ? name : name + "/"); 173 target.putArchiveEntry(archiveEntry); 174 if (isFile) { 175 // Refactor this as a BiConsumer on Java 8 176 target.write(path); 177 } 178 target.closeArchiveEntry(); 179 } 180 return FileVisitResult.CONTINUE; 181 } 182 183 }); 184 target.finish(); 185 } 186 187 /** 188 * Creates an archive {@code target} using the format {@code 189 * format} by recursively including all files and directories in {@code directory}. 190 * 191 * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}. 192 * @param target the file to write the new archive to. 193 * @param directory the directory that contains the files to archive. 194 * @throws IOException if an I/O error occurs 195 * @throws ArchiveException if the archive cannot be created for other reasons 196 */ 197 public void create(final String format, final File target, final File directory) 198 throws IOException, ArchiveException { 199 create(format, target.toPath(), directory.toPath()); 200 } 201 202 /** 203 * Creates an archive {@code target} using the format {@code 204 * format} by recursively including all files and directories in {@code directory}. 205 * 206 * <p> 207 * This method creates a wrapper around the target stream which is never closed and thus leaks resources, please use 208 * {@link #create(String,OutputStream,File,CloseableConsumer)} instead. 209 * </p> 210 * 211 * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}. 212 * @param target the stream to write the new archive to. 213 * @param directory the directory that contains the files to archive. 214 * @throws IOException if an I/O error occurs 215 * @throws ArchiveException if the archive cannot be created for other reasons 216 * @deprecated this method leaks resources 217 */ 218 @Deprecated 219 public void create(final String format, final OutputStream target, final File directory) 220 throws IOException, ArchiveException { 221 create(format, target, directory, CloseableConsumer.NULL_CONSUMER); 222 } 223 224 /** 225 * Creates an archive {@code target} using the format {@code 226 * format} by recursively including all files and directories in {@code directory}. 227 * 228 * <p> 229 * This method creates a wrapper around the archive stream and the caller of this method is responsible for closing 230 * it - probably at the same time as closing the stream itself. The caller is informed about the wrapper object via 231 * the {@code 232 * closeableConsumer} callback as soon as it is no longer needed by this class. 233 * </p> 234 * 235 * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}. 236 * @param target the stream to write the new archive to. 237 * @param directory the directory that contains the files to archive. 238 * @param closeableConsumer is informed about the stream wrapped around the passed in stream 239 * @throws IOException if an I/O error occurs 240 * @throws ArchiveException if the archive cannot be created for other reasons 241 * @since 1.19 242 */ 243 public void create(final String format, final OutputStream target, final File directory, 244 final CloseableConsumer closeableConsumer) throws IOException, ArchiveException { 245 try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) { 246 create(c.track(ArchiveStreamFactory.DEFAULT.createArchiveOutputStream(format, target)), directory); 247 } 248 } 249 250 /** 251 * Creates an archive {@code target} using the format {@code 252 * format} by recursively including all files and directories in {@code directory}. 253 * 254 * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}. 255 * @param target the file to write the new archive to. 256 * @param directory the directory that contains the files to archive. 257 * @throws IOException if an I/O error occurs 258 * @throws ArchiveException if the archive cannot be created for other reasons 259 * @since 1.21 260 */ 261 public void create(final String format, final Path target, final Path directory) 262 throws IOException, ArchiveException { 263 if (prefersSeekableByteChannel(format)) { 264 try (SeekableByteChannel channel = FileChannel.open(target, StandardOpenOption.WRITE, 265 StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { 266 create(format, channel, directory); 267 return; 268 } 269 } 270 try (@SuppressWarnings("resource") // ArchiveOutputStream wraps newOutputStream result 271 ArchiveOutputStream outputStream = ArchiveStreamFactory.DEFAULT.createArchiveOutputStream(format, 272 Files.newOutputStream(target))) { 273 create(outputStream, directory, EMPTY_FileVisitOption); 274 } 275 } 276 277 /** 278 * Creates an archive {@code target} using the format {@code 279 * format} by recursively including all files and directories in {@code directory}. 280 * 281 * <p> 282 * This method creates a wrapper around the target channel which is never closed and thus leaks resources, please 283 * use {@link #create(String,SeekableByteChannel,File,CloseableConsumer)} instead. 284 * </p> 285 * 286 * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}. 287 * @param target the channel to write the new archive to. 288 * @param directory the directory that contains the files to archive. 289 * @throws IOException if an I/O error occurs 290 * @throws ArchiveException if the archive cannot be created for other reasons 291 * @deprecated this method leaks resources 292 */ 293 @Deprecated 294 public void create(final String format, final SeekableByteChannel target, final File directory) 295 throws IOException, ArchiveException { 296 create(format, target, directory, CloseableConsumer.NULL_CONSUMER); 297 } 298 299 /** 300 * Creates an archive {@code target} using the format {@code 301 * format} by recursively including all files and directories in {@code directory}. 302 * 303 * <p> 304 * This method creates a wrapper around the archive channel and the caller of this method is responsible for closing 305 * it - probably at the same time as closing the channel itself. The caller is informed about the wrapper object via 306 * the {@code 307 * closeableConsumer} callback as soon as it is no longer needed by this class. 308 * </p> 309 * 310 * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}. 311 * @param target the channel to write the new archive to. 312 * @param directory the directory that contains the files to archive. 313 * @param closeableConsumer is informed about the stream wrapped around the passed in stream 314 * @throws IOException if an I/O error occurs 315 * @throws ArchiveException if the archive cannot be created for other reasons 316 * @since 1.19 317 */ 318 public void create(final String format, final SeekableByteChannel target, final File directory, 319 final CloseableConsumer closeableConsumer) throws IOException, ArchiveException { 320 try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) { 321 if (!prefersSeekableByteChannel(format)) { 322 create(format, c.track(Channels.newOutputStream(target)), directory); 323 } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) { 324 create(c.track(new ZipArchiveOutputStream(target)), directory); 325 } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) { 326 create(c.track(new SevenZOutputFile(target)), directory); 327 } else { 328 // never reached as prefersSeekableByteChannel only returns true for ZIP and 7z 329 throw new ArchiveException("Don't know how to handle format " + format); 330 } 331 } 332 } 333 334 /** 335 * Creates an archive {@code target} using the format {@code 336 * format} by recursively including all files and directories in {@code directory}. 337 * 338 * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}. 339 * @param target the channel to write the new archive to. 340 * @param directory the directory that contains the files to archive. 341 * @throws IOException if an I/O error occurs 342 * @throws IllegalStateException if the format does not support {@code SeekableByteChannel}. 343 */ 344 public void create(final String format, final SeekableByteChannel target, final Path directory) throws IOException { 345 if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) { 346 try (SevenZOutputFile sevenZFile = new SevenZOutputFile(target)) { 347 create(sevenZFile, directory); 348 } 349 } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) { 350 try (ArchiveOutputStream archiveOutputStream = new ZipArchiveOutputStream(target)) { 351 create(archiveOutputStream, directory, EMPTY_FileVisitOption); 352 } 353 } else { 354 throw new IllegalStateException(format); 355 } 356 } 357 358 private boolean prefersSeekableByteChannel(final String format) { 359 return ArchiveStreamFactory.ZIP.equalsIgnoreCase(format) 360 || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format); 361 } 362}