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