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.SevenZOutputFile;
043import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
044import org.apache.commons.compress.utils.IOUtils;
045
046/**
047 * Provides a high level API for creating archives.
048 *
049 * @since 1.17
050 * @since 1.21 Supports {@link Path}.
051 */
052public class Archiver {
053
054    private static class ArchiverFileVisitor extends SimpleFileVisitor<Path> {
055
056        private final ArchiveOutputStream target;
057        private final Path directory;
058        private final LinkOption[] linkOptions;
059
060        private ArchiverFileVisitor(final ArchiveOutputStream target, final Path directory,
061            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)
073            throws IOException {
074            Objects.requireNonNull(path);
075            Objects.requireNonNull(attrs);
076            final String name = directory.relativize(path).toString().replace('\\', '/');
077            if (!name.isEmpty()) {
078                final ArchiveEntry archiveEntry = target.createArchiveEntry(path,
079                    isFile || name.endsWith("/") ? name : name + "/", linkOptions);
080                target.putArchiveEntry(archiveEntry);
081                if (isFile) {
082                    // Refactor this as a BiConsumer on Java 8
083                    Files.copy(path, target);
084                }
085                target.closeArchiveEntry();
086            }
087            return FileVisitResult.CONTINUE;
088        }
089
090        @Override
091        public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
092            return visit(file, attrs, true);
093        }
094    }
095
096    /**
097     * No {@link FileVisitOption}.
098     */
099    public static final EnumSet<FileVisitOption> EMPTY_FileVisitOption = EnumSet.noneOf(FileVisitOption.class);
100
101    /**
102     * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
103     *
104     * @param target the stream to write the new archive to.
105     * @param directory the directory that contains the files to archive.
106     * @throws IOException if an I/O error occurs
107     * @throws ArchiveException if the archive cannot be created for other reasons
108     */
109    public void create(final ArchiveOutputStream target, final File directory) throws IOException, ArchiveException {
110        create(target, directory.toPath(), EMPTY_FileVisitOption);
111    }
112
113    /**
114     * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
115     *
116     * @param target the stream to write the new archive to.
117     * @param directory the directory that contains the files to archive.
118     * @param fileVisitOptions linkOptions to configure the traversal of the source {@code directory}.
119     * @param linkOptions indicating how symbolic links are handled.
120     * @throws IOException if an I/O error occurs or the archive cannot be created for other reasons.
121     * @since 1.21
122     */
123    public void create(final ArchiveOutputStream target, final Path directory,
124        final EnumSet<FileVisitOption> fileVisitOptions, final LinkOption... linkOptions) throws IOException {
125        Files.walkFileTree(directory, fileVisitOptions, Integer.MAX_VALUE,
126            new ArchiverFileVisitor(target, directory, linkOptions));
127        target.finish();
128    }
129
130    /**
131     * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
132     *
133     * @param target the stream to write the new archive to.
134     * @param directory the directory that contains the files to archive.
135     * @throws IOException if an I/O error occurs or the archive cannot be created for other reasons.
136     * @since 1.21
137     */
138    public void create(final ArchiveOutputStream target, final Path directory) throws IOException {
139        create(target, directory, EMPTY_FileVisitOption);
140    }
141
142    /**
143     * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
144     *
145     * @param target the file to write the new archive to.
146     * @param directory the directory that contains the files to archive.
147     * @throws IOException if an I/O error occurs
148     */
149    public void create(final SevenZOutputFile target, final File directory) throws IOException {
150        create(target, directory.toPath());
151    }
152
153    /**
154     * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
155     *
156     * @param target the file to write the new archive to.
157     * @param directory the directory that contains the files to archive.
158     * @throws IOException if an I/O error occurs
159     * @since 1.21
160     */
161    public void create(final SevenZOutputFile target, final Path directory) throws IOException {
162        // This custom SimpleFileVisitor goes away with Java 8's BiConsumer.
163        Files.walkFileTree(directory, new ArchiverFileVisitor(null, directory) {
164
165            @Override
166            protected FileVisitResult visit(final Path path, final BasicFileAttributes attrs, final boolean isFile)
167                throws IOException {
168                Objects.requireNonNull(path);
169                Objects.requireNonNull(attrs);
170                final String name = directory.relativize(path).toString().replace('\\', '/');
171                if (!name.isEmpty()) {
172                    final ArchiveEntry archiveEntry = target.createArchiveEntry(path,
173                        isFile || name.endsWith("/") ? name : name + "/");
174                    target.putArchiveEntry(archiveEntry);
175                    if (isFile) {
176                        // Refactor this as a BiConsumer on Java 8
177                        target.write(path);
178                    }
179                    target.closeArchiveEntry();
180                }
181                return FileVisitResult.CONTINUE;
182            }
183
184        });
185        target.finish();
186    }
187
188    /**
189     * Creates an archive {@code target} using the format {@code
190     * format} by recursively including all files and directories in {@code directory}.
191     *
192     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
193     * @param target the file to write the new archive to.
194     * @param directory the directory that contains the files to archive.
195     * @throws IOException if an I/O error occurs
196     * @throws ArchiveException if the archive cannot be created for other reasons
197     */
198    public void create(final String format, final File target, final File directory)
199        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)
221        throws IOException, ArchiveException {
222        create(format, target, directory, CloseableConsumer.NULL_CONSUMER);
223    }
224
225    /**
226     * Creates an archive {@code target} using the format {@code
227     * format} by recursively including all files and directories in {@code directory}.
228     *
229     * <p>
230     * This method creates a wrapper around the archive stream and the caller of this method is responsible for closing
231     * it - probably at the same time as closing the stream itself. The caller is informed about the wrapper object via
232     * the {@code
233     * closeableConsumer} callback as soon as it is no longer needed by this class.
234     * </p>
235     *
236     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
237     * @param target the stream to write the new archive to.
238     * @param directory the directory that contains the files to archive.
239     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
240     * @throws IOException if an I/O error occurs
241     * @throws ArchiveException if the archive cannot be created for other reasons
242     * @since 1.19
243     */
244    public void create(final String format, final OutputStream target, final File directory,
245        final CloseableConsumer closeableConsumer) throws IOException, ArchiveException {
246        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
247            create(c.track(ArchiveStreamFactory.DEFAULT.createArchiveOutputStream(format, target)), directory);
248        }
249    }
250
251    /**
252     * Creates an archive {@code target} using the format {@code
253     * format} by recursively including all files and directories in {@code directory}.
254     *
255     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
256     * @param target the file to write the new archive to.
257     * @param directory the directory that contains the files to archive.
258     * @throws IOException if an I/O error occurs
259     * @throws ArchiveException if the archive cannot be created for other reasons
260     * @since 1.21
261     */
262    public void create(final String format, final Path target, final Path directory)
263        throws IOException, ArchiveException {
264        if (prefersSeekableByteChannel(format)) {
265            try (SeekableByteChannel channel = FileChannel.open(target, StandardOpenOption.WRITE,
266                StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
267                create(format, channel, directory);
268                return;
269            }
270        }
271        try (@SuppressWarnings("resource") // ArchiveOutputStream wraps newOutputStream result
272        ArchiveOutputStream outputStream = ArchiveStreamFactory.DEFAULT.createArchiveOutputStream(format,
273            Files.newOutputStream(target))) {
274            create(outputStream, directory, EMPTY_FileVisitOption);
275        }
276    }
277
278    /**
279     * Creates an archive {@code target} using the format {@code
280     * format} by recursively including all files and directories in {@code directory}.
281     *
282     * <p>
283     * This method creates a wrapper around the target channel which is never closed and thus leaks resources, please
284     * use {@link #create(String,SeekableByteChannel,File,CloseableConsumer)} instead.
285     * </p>
286     *
287     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
288     * @param target the channel to write the new archive to.
289     * @param directory the directory that contains the files to archive.
290     * @throws IOException if an I/O error occurs
291     * @throws ArchiveException if the archive cannot be created for other reasons
292     * @deprecated this method leaks resources
293     */
294    @Deprecated
295    public void create(final String format, final SeekableByteChannel target, final File directory)
296        throws IOException, ArchiveException {
297        create(format, target, directory, CloseableConsumer.NULL_CONSUMER);
298    }
299
300    /**
301     * Creates an archive {@code target} using the format {@code
302     * format} by recursively including all files and directories in {@code directory}.
303     *
304     * <p>
305     * This method creates a wrapper around the archive channel and the caller of this method is responsible for closing
306     * it - probably at the same time as closing the channel itself. The caller is informed about the wrapper object via
307     * the {@code
308     * closeableConsumer} callback as soon as it is no longer needed by this class.
309     * </p>
310     *
311     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
312     * @param target the channel to write the new archive to.
313     * @param directory the directory that contains the files to archive.
314     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
315     * @throws IOException if an I/O error occurs
316     * @throws ArchiveException if the archive cannot be created for other reasons
317     * @since 1.19
318     */
319    public void create(final String format, final SeekableByteChannel target, final File directory,
320        final CloseableConsumer closeableConsumer) throws IOException, ArchiveException {
321        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
322            if (!prefersSeekableByteChannel(format)) {
323                create(format, c.track(Channels.newOutputStream(target)), directory);
324            } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
325                create(c.track(new ZipArchiveOutputStream(target)), directory);
326            } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
327                create(c.track(new SevenZOutputFile(target)), directory);
328            } else {
329                // never reached as prefersSeekableByteChannel only returns true for ZIP and 7z
330                throw new ArchiveException("Don't know how to handle format " + format);
331            }
332        }
333    }
334
335    /**
336     * Creates an archive {@code target} using the format {@code
337     * format} by recursively including all files and directories in {@code directory}.
338     *
339     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
340     * @param target the channel to write the new archive to.
341     * @param directory the directory that contains the files to archive.
342     * @throws IOException if an I/O error occurs
343     * @throws IllegalStateException if the format does not support {@code SeekableByteChannel}.
344     */
345    public void create(final String format, final SeekableByteChannel target, final Path directory) throws IOException {
346        if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
347            try (SevenZOutputFile sevenZFile = new SevenZOutputFile(target)) {
348                create(sevenZFile, directory);
349            }
350        } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
351            try (ArchiveOutputStream archiveOutputStream = new ZipArchiveOutputStream(target)) {
352                create(archiveOutputStream, directory, EMPTY_FileVisitOption);
353            }
354        } else {
355            throw new IllegalStateException(format);
356        }
357    }
358
359    private boolean prefersSeekableByteChannel(final String format) {
360        return ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)
361            || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format);
362    }
363}