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 }