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}