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 *   https://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 outputStream;
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.outputStream = 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 = outputStream.createArchiveEntry(path, isFile || name.endsWith("/") ? name : name + "/", linkOptions);
078                outputStream.putArchiveEntry(archiveEntry);
079                if (isFile) {
080                    // Refactor this as a BiConsumer on Java 8?
081                    outputStream.write(path);
082                }
083                outputStream.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     * 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}