View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.commons.compress.archivers.examples;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.OutputStream;
24  import java.nio.channels.Channels;
25  import java.nio.channels.FileChannel;
26  import java.nio.channels.SeekableByteChannel;
27  import java.nio.file.FileVisitOption;
28  import java.nio.file.FileVisitResult;
29  import java.nio.file.Files;
30  import java.nio.file.LinkOption;
31  import java.nio.file.Path;
32  import java.nio.file.SimpleFileVisitor;
33  import java.nio.file.StandardOpenOption;
34  import java.nio.file.attribute.BasicFileAttributes;
35  import java.util.EnumSet;
36  import java.util.Objects;
37  
38  import org.apache.commons.compress.archivers.ArchiveEntry;
39  import org.apache.commons.compress.archivers.ArchiveException;
40  import org.apache.commons.compress.archivers.ArchiveOutputStream;
41  import org.apache.commons.compress.archivers.ArchiveStreamFactory;
42  import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry;
43  import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile;
44  import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
45  import org.apache.commons.compress.utils.IOUtils;
46  
47  /**
48   * Provides a high level API for creating archives.
49   *
50   * @since 1.17
51   * @since 1.21 Supports {@link Path}.
52   */
53  public class Archiver {
54  
55      private static class ArchiverFileVisitor<O extends ArchiveOutputStream<E>, E extends ArchiveEntry> extends SimpleFileVisitor<Path> {
56  
57          private final O target;
58          private final Path directory;
59          private final LinkOption[] linkOptions;
60  
61          private ArchiverFileVisitor(final O target, final Path directory, final LinkOption... linkOptions) {
62              this.target = target;
63              this.directory = directory;
64              this.linkOptions = linkOptions == null ? IOUtils.EMPTY_LINK_OPTIONS : linkOptions.clone();
65          }
66  
67          @Override
68          public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) throws IOException {
69              return visit(dir, attrs, false);
70          }
71  
72          protected FileVisitResult visit(final Path path, final BasicFileAttributes attrs, final boolean isFile) throws IOException {
73              Objects.requireNonNull(path);
74              Objects.requireNonNull(attrs);
75              final String name = directory.relativize(path).toString().replace('\\', '/');
76              if (!name.isEmpty()) {
77                  final E archiveEntry = target.createArchiveEntry(path, isFile || name.endsWith("/") ? name : name + "/", linkOptions);
78                  target.putArchiveEntry(archiveEntry);
79                  if (isFile) {
80                      // Refactor this as a BiConsumer on Java 8
81                      Files.copy(path, target);
82                  }
83                  target.closeArchiveEntry();
84              }
85              return FileVisitResult.CONTINUE;
86          }
87  
88          @Override
89          public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
90              return visit(file, attrs, true);
91          }
92      }
93  
94      /**
95       * No {@link FileVisitOption}.
96       */
97      public static final EnumSet<FileVisitOption> EMPTY_FileVisitOption = EnumSet.noneOf(FileVisitOption.class);
98  
99      /**
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 }