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   *   https://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 outputStream;
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.outputStream = 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 = outputStream.createArchiveEntry(path, isFile || name.endsWith("/") ? name : name + "/", linkOptions);
78                  outputStream.putArchiveEntry(archiveEntry);
79                  if (isFile) {
80                      // Refactor this as a BiConsumer on Java 8?
81                      outputStream.write(path);
82                  }
83                  outputStream.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      * 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 }