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