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     */
108    public void create(final ArchiveOutputStream target, final File directory) throws IOException {
109        create(target, directory.toPath(), EMPTY_FileVisitOption);
110    }
111
112    /**
113     * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
114     *
115     * @param target the stream to write the new archive to.
116     * @param directory the directory that contains the files to archive.
117     * @throws IOException if an I/O error occurs or the archive cannot be created for other reasons.
118     * @since 1.21
119     */
120    public void create(final ArchiveOutputStream target, final Path directory) throws IOException {
121        create(target, directory, EMPTY_FileVisitOption);
122    }
123
124    /**
125     * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
126     *
127     * @param target the stream to write the new archive to.
128     * @param directory the directory that contains the files to archive.
129     * @param fileVisitOptions linkOptions to configure the traversal of the source {@code directory}.
130     * @param linkOptions indicating how symbolic links are handled.
131     * @throws IOException if an I/O error occurs or the archive cannot be created for other reasons.
132     * @since 1.21
133     */
134    public void create(final ArchiveOutputStream target, final Path directory,
135        final EnumSet<FileVisitOption> fileVisitOptions, final LinkOption... linkOptions) throws IOException {
136        Files.walkFileTree(directory, fileVisitOptions, Integer.MAX_VALUE,
137            new ArchiverFileVisitor(target, directory, linkOptions));
138        target.finish();
139    }
140
141    /**
142     * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
143     *
144     * @param target the file to write the new archive to.
145     * @param directory the directory that contains the files to archive.
146     * @throws IOException if an I/O error occurs
147     */
148    public void create(final SevenZOutputFile target, final File directory) throws IOException {
149        create(target, directory.toPath());
150    }
151
152    /**
153     * Creates an archive {@code target} by recursively including all files and directories in {@code directory}.
154     *
155     * @param target the file to write the new archive to.
156     * @param directory the directory that contains the files to archive.
157     * @throws IOException if an I/O error occurs
158     * @since 1.21
159     */
160    public void create(final SevenZOutputFile target, final Path directory) throws IOException {
161        // This custom SimpleFileVisitor goes away with Java 8's BiConsumer.
162        Files.walkFileTree(directory, new ArchiverFileVisitor(null, directory) {
163
164            @Override
165            protected FileVisitResult visit(final Path path, final BasicFileAttributes attrs, final boolean isFile)
166                throws IOException {
167                Objects.requireNonNull(path);
168                Objects.requireNonNull(attrs);
169                final String name = directory.relativize(path).toString().replace('\\', '/');
170                if (!name.isEmpty()) {
171                    final ArchiveEntry archiveEntry = target.createArchiveEntry(path,
172                        isFile || name.endsWith("/") ? name : name + "/");
173                    target.putArchiveEntry(archiveEntry);
174                    if (isFile) {
175                        // Refactor this as a BiConsumer on Java 8
176                        target.write(path);
177                    }
178                    target.closeArchiveEntry();
179                }
180                return FileVisitResult.CONTINUE;
181            }
182
183        });
184        target.finish();
185    }
186
187    /**
188     * Creates an archive {@code target} using the format {@code
189     * format} by recursively including all files and directories in {@code directory}.
190     *
191     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
192     * @param target the file to write the new archive to.
193     * @param directory the directory that contains the files to archive.
194     * @throws IOException if an I/O error occurs
195     * @throws ArchiveException if the archive cannot be created for other reasons
196     */
197    public void create(final String format, final File target, final File directory)
198        throws IOException, ArchiveException {
199        create(format, target.toPath(), directory.toPath());
200    }
201
202    /**
203     * Creates an archive {@code target} using the format {@code
204     * format} by recursively including all files and directories in {@code directory}.
205     *
206     * <p>
207     * This method creates a wrapper around the target stream which is never closed and thus leaks resources, please use
208     * {@link #create(String,OutputStream,File,CloseableConsumer)} instead.
209     * </p>
210     *
211     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
212     * @param target the stream to write the new archive to.
213     * @param directory the directory that contains the files to archive.
214     * @throws IOException if an I/O error occurs
215     * @throws ArchiveException if the archive cannot be created for other reasons
216     * @deprecated this method leaks resources
217     */
218    @Deprecated
219    public void create(final String format, final OutputStream target, final File directory)
220        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
230     * it - probably at the same time as closing the stream itself. The caller is informed about the wrapper object via
231     * the {@code
232     * closeableConsumer} callback as soon as it is no longer needed by this class.
233     * </p>
234     *
235     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
236     * @param target the stream to write the new archive to.
237     * @param directory the directory that contains the files to archive.
238     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
239     * @throws IOException if an I/O error occurs
240     * @throws ArchiveException if the archive cannot be created for other reasons
241     * @since 1.19
242     */
243    public void create(final String format, final OutputStream target, final File directory,
244        final CloseableConsumer closeableConsumer) throws IOException, ArchiveException {
245        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
246            create(c.track(ArchiveStreamFactory.DEFAULT.createArchiveOutputStream(format, target)), 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)
262        throws IOException, ArchiveException {
263        if (prefersSeekableByteChannel(format)) {
264            try (SeekableByteChannel channel = FileChannel.open(target, StandardOpenOption.WRITE,
265                StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
266                create(format, channel, directory);
267                return;
268            }
269        }
270        try (@SuppressWarnings("resource") // ArchiveOutputStream wraps newOutputStream result
271        ArchiveOutputStream outputStream = ArchiveStreamFactory.DEFAULT.createArchiveOutputStream(format,
272            Files.newOutputStream(target))) {
273            create(outputStream, directory, EMPTY_FileVisitOption);
274        }
275    }
276
277    /**
278     * Creates an archive {@code target} using the format {@code
279     * format} by recursively including all files and directories in {@code directory}.
280     *
281     * <p>
282     * This method creates a wrapper around the target channel which is never closed and thus leaks resources, please
283     * use {@link #create(String,SeekableByteChannel,File,CloseableConsumer)} instead.
284     * </p>
285     *
286     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
287     * @param target the channel to write the new archive to.
288     * @param directory the directory that contains the files to archive.
289     * @throws IOException if an I/O error occurs
290     * @throws ArchiveException if the archive cannot be created for other reasons
291     * @deprecated this method leaks resources
292     */
293    @Deprecated
294    public void create(final String format, final SeekableByteChannel target, final File directory)
295        throws IOException, ArchiveException {
296        create(format, target, directory, CloseableConsumer.NULL_CONSUMER);
297    }
298
299    /**
300     * Creates an archive {@code target} using the format {@code
301     * format} by recursively including all files and directories in {@code directory}.
302     *
303     * <p>
304     * This method creates a wrapper around the archive channel and the caller of this method is responsible for closing
305     * it - probably at the same time as closing the channel itself. The caller is informed about the wrapper object via
306     * the {@code
307     * closeableConsumer} callback as soon as it is no longer needed by this class.
308     * </p>
309     *
310     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
311     * @param target the channel to write the new archive to.
312     * @param directory the directory that contains the files to archive.
313     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
314     * @throws IOException if an I/O error occurs
315     * @throws ArchiveException if the archive cannot be created for other reasons
316     * @since 1.19
317     */
318    public void create(final String format, final SeekableByteChannel target, final File directory,
319        final CloseableConsumer closeableConsumer) throws IOException, ArchiveException {
320        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
321            if (!prefersSeekableByteChannel(format)) {
322                create(format, c.track(Channels.newOutputStream(target)), directory);
323            } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
324                create(c.track(new ZipArchiveOutputStream(target)), directory);
325            } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
326                create(c.track(new SevenZOutputFile(target)), directory);
327            } else {
328                // never reached as prefersSeekableByteChannel only returns true for ZIP and 7z
329                throw new ArchiveException("Don't know how to handle format " + format);
330            }
331        }
332    }
333
334    /**
335     * Creates an archive {@code target} using the format {@code
336     * format} by recursively including all files and directories in {@code directory}.
337     *
338     * @param format the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
339     * @param target the channel to write the new archive to.
340     * @param directory the directory that contains the files to archive.
341     * @throws IOException if an I/O error occurs
342     * @throws IllegalStateException if the format does not support {@code SeekableByteChannel}.
343     */
344    public void create(final String format, final SeekableByteChannel target, final Path directory) throws IOException {
345        if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
346            try (SevenZOutputFile sevenZFile = new SevenZOutputFile(target)) {
347                create(sevenZFile, directory);
348            }
349        } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
350            try (ArchiveOutputStream archiveOutputStream = new ZipArchiveOutputStream(target)) {
351                create(archiveOutputStream, directory, EMPTY_FileVisitOption);
352            }
353        } else {
354            throw new IllegalStateException(format);
355        }
356    }
357
358    private boolean prefersSeekableByteChannel(final String format) {
359        return ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)
360            || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format);
361    }
362}