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,
133            final EnumSet<FileVisitOption> fileVisitOptions, 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(null, directory) {
160
161            @Override
162            protected FileVisitResult visit(final Path path, final BasicFileAttributes attrs, final boolean isFile)
163                throws IOException {
164                Objects.requireNonNull(path);
165                Objects.requireNonNull(attrs);
166                final String name = directory.relativize(path).toString().replace('\\', '/');
167                if (!name.isEmpty()) {
168                    final SevenZArchiveEntry archiveEntry = target.createArchiveEntry(path,
169                        isFile || name.endsWith("/") ? name : name + "/");
170                    target.putArchiveEntry(archiveEntry);
171                    if (isFile) {
172                        // Refactor this as a BiConsumer on Java 8
173                        target.write(path);
174                    }
175                    target.closeArchiveEntry();
176                }
177                return FileVisitResult.CONTINUE;
178            }
179
180        });
181        target.finish();
182    }
183
184    /**
185     * Creates an archive {@code target} using the format {@code
186     * format} by recursively including all files and directories in {@code directory}.
187     *
188     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
189     * @param target the file to write the new archive to.
190     * @param directory the directory that contains the files to archive.
191     * @throws IOException if an I/O error occurs
192     * @throws ArchiveException if the archive cannot be created for other reasons
193     */
194    public void create(final String format, final File target, final File directory)
195        throws IOException, ArchiveException {
196        create(format, target.toPath(), directory.toPath());
197    }
198
199    /**
200     * Creates an archive {@code target} using the format {@code
201     * format} by recursively including all files and directories in {@code directory}.
202     *
203     * <p>
204     * This method creates a wrapper around the target stream which is never closed and thus leaks resources, please use
205     * {@link #create(String,OutputStream,File,CloseableConsumer)} instead.
206     * </p>
207     *
208     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
209     * @param target the stream to write the new archive to.
210     * @param directory the directory that contains the files to archive.
211     * @throws IOException if an I/O error occurs
212     * @throws ArchiveException if the archive cannot be created for other reasons
213     * @deprecated this method leaks resources
214     */
215    @Deprecated
216    public void create(final String format, final OutputStream target, final File directory)
217        throws IOException, ArchiveException {
218        create(format, target, directory, CloseableConsumer.NULL_CONSUMER);
219    }
220
221    /**
222     * Creates an archive {@code target} using the format {@code
223     * format} by recursively including all files and directories in {@code directory}.
224     *
225     * <p>
226     * This method creates a wrapper around the archive stream and the caller of this method is responsible for closing
227     * it - probably at the same time as closing the stream itself. The caller is informed about the wrapper object via
228     * the {@code
229     * closeableConsumer} callback as soon as it is no longer needed by this class.
230     * </p>
231     *
232     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
233     * @param target the stream to write the new archive to.
234     * @param directory the directory that contains the files to archive.
235     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
236     * @throws IOException if an I/O error occurs
237     * @throws ArchiveException if the archive cannot be created for other reasons
238     * @since 1.19
239     */
240    public void create(final String format, final OutputStream target, final File directory,
241        final CloseableConsumer closeableConsumer) throws IOException, ArchiveException {
242        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
243            ArchiveOutputStream<? extends ArchiveEntry> archiveOutputStream = ArchiveStreamFactory.DEFAULT.createArchiveOutputStream(format, target);
244            create(c.track(archiveOutputStream), directory);
245        }
246    }
247
248    /**
249     * Creates an archive {@code target} using the format {@code
250     * format} by recursively including all files and directories in {@code directory}.
251     *
252     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
253     * @param target the file to write the new archive to.
254     * @param directory the directory that contains the files to archive.
255     * @throws IOException if an I/O error occurs
256     * @throws ArchiveException if the archive cannot be created for other reasons
257     * @since 1.21
258     */
259    public void create(final String format, final Path target, final Path directory)
260            throws IOException, ArchiveException {
261        if (prefersSeekableByteChannel(format)) {
262            try (SeekableByteChannel channel = FileChannel.open(target, StandardOpenOption.WRITE, StandardOpenOption.CREATE,
263                    StandardOpenOption.TRUNCATE_EXISTING)) {
264                create(format, channel, directory);
265                return;
266            }
267        }
268        try (@SuppressWarnings("resource") // ArchiveOutputStream wraps newOutputStream result
269        ArchiveOutputStream<?> outputStream = ArchiveStreamFactory.DEFAULT.createArchiveOutputStream(format, Files.newOutputStream(target))) {
270            create(outputStream, directory, EMPTY_FileVisitOption);
271        }
272    }
273
274    /**
275     * Creates an archive {@code target} using the format {@code
276     * format} by recursively including all files and directories in {@code directory}.
277     *
278     * <p>
279     * This method creates a wrapper around the target channel which is never closed and thus leaks resources, please
280     * use {@link #create(String,SeekableByteChannel,File,CloseableConsumer)} instead.
281     * </p>
282     *
283     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
284     * @param target the channel to write the new archive to.
285     * @param directory the directory that contains the files to archive.
286     * @throws IOException if an I/O error occurs
287     * @throws ArchiveException if the archive cannot be created for other reasons
288     * @deprecated this method leaks resources
289     */
290    @Deprecated
291    public void create(final String format, final SeekableByteChannel target, final File directory)
292        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
302     * it - probably at the same time as closing the channel itself. The caller is informed about the wrapper object via
303     * the {@code
304     * closeableConsumer} callback as soon as it is no longer needed by this class.
305     * </p>
306     *
307     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
308     * @param target the channel to write the new archive to.
309     * @param directory the directory that contains the files to archive.
310     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
311     * @throws IOException if an I/O error occurs
312     * @throws ArchiveException if the archive cannot be created for other reasons
313     * @since 1.19
314     */
315    public void create(final String format, final SeekableByteChannel target, final File directory,
316        final CloseableConsumer closeableConsumer) throws IOException, ArchiveException {
317        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
318            if (!prefersSeekableByteChannel(format)) {
319                create(format, c.track(Channels.newOutputStream(target)), directory);
320            } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
321                create(c.track(new ZipArchiveOutputStream(target)), directory);
322            } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
323                create(c.track(new SevenZOutputFile(target)), directory);
324            } else {
325                // never reached as prefersSeekableByteChannel only returns true for ZIP and 7z
326                throw new ArchiveException("Don't know how to handle format " + format);
327            }
328        }
329    }
330
331    /**
332     * Creates an archive {@code target} using the format {@code
333     * format} by recursively including all files and directories in {@code directory}.
334     *
335     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
336     * @param target the channel to write the new archive to.
337     * @param directory the directory that contains the files to archive.
338     * @throws IOException if an I/O error occurs
339     * @throws IllegalStateException if the format does not support {@code SeekableByteChannel}.
340     */
341    public void create(final String format, final SeekableByteChannel target, final Path directory) throws IOException {
342        if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
343            try (SevenZOutputFile sevenZFile = new SevenZOutputFile(target)) {
344                create(sevenZFile, directory);
345            }
346        } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
347            try (ZipArchiveOutputStream archiveOutputStream = new ZipArchiveOutputStream(target)) {
348                create(archiveOutputStream, directory, EMPTY_FileVisitOption);
349            }
350        } else {
351            throw new IllegalStateException(format);
352        }
353    }
354
355    private boolean prefersSeekableByteChannel(final String format) {
356        return ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)
357            || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format);
358    }
359}