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