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.BufferedInputStream;
022import java.io.File;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.OutputStream;
026import java.nio.channels.Channels;
027import java.nio.channels.FileChannel;
028import java.nio.channels.SeekableByteChannel;
029import java.nio.file.Files;
030import java.nio.file.StandardOpenOption;
031
032import org.apache.commons.compress.archivers.ArchiveEntry;
033import org.apache.commons.compress.archivers.ArchiveException;
034import org.apache.commons.compress.archivers.ArchiveOutputStream;
035import org.apache.commons.compress.archivers.ArchiveStreamFactory;
036import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile;
037import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
038import org.apache.commons.compress.utils.IOUtils;
039
040/**
041 * Provides a high level API for creating archives.
042 * @since 1.17
043 */
044public class Archiver {
045
046    private interface ArchiveEntryCreator {
047        ArchiveEntry create(File f, String entryName) throws IOException;
048    }
049
050    private interface ArchiveEntryConsumer {
051        void accept(File source, ArchiveEntry entry) throws IOException;
052    }
053
054    private interface Finisher {
055        void finish() throws IOException;
056    }
057
058    /**
059     * Creates an archive {@code target} using the format {@code
060     * format} by recursively including all files and directories in
061     * {@code directory}.
062     *
063     * @param format the archive format. This uses the same format as
064     * accepted by {@link ArchiveStreamFactory}.
065     * @param target the file to write the new archive to.
066     * @param directory the directory that contains the files to archive.
067     * @throws IOException if an I/O error occurs
068     * @throws ArchiveException if the archive cannot be created for other reasons
069     */
070    public void create(String format, File target, File directory) throws IOException, ArchiveException {
071        if (prefersSeekableByteChannel(format)) {
072            try (SeekableByteChannel c = FileChannel.open(target.toPath(), StandardOpenOption.WRITE,
073                StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
074                create(format, c, directory, CloseableConsumer.CLOSING_CONSUMER);
075            }
076            return;
077        }
078        try (OutputStream o = Files.newOutputStream(target.toPath())) {
079            create(format, o, directory, CloseableConsumer.CLOSING_CONSUMER);
080        }
081    }
082
083    /**
084     * Creates an archive {@code target} using the format {@code
085     * format} by recursively including all files and directories in
086     * {@code directory}.
087     *
088     * <p>This method creates a wrapper around the target stream
089     * which is never closed and thus leaks resources, please use
090     * {@link #create(String,OutputStream,File,CloseableConsumer)}
091     * instead.</p>
092     *
093     * @param format the archive format. This uses the same format as
094     * accepted by {@link ArchiveStreamFactory}.
095     * @param target the stream to write the new archive to.
096     * @param directory the directory that contains the files to archive.
097     * @throws IOException if an I/O error occurs
098     * @throws ArchiveException if the archive cannot be created for other reasons
099     * @deprecated this method leaks resources
100     */
101    @Deprecated
102    public void create(String format, OutputStream target, File directory) throws IOException, ArchiveException {
103        create(format, target, directory, CloseableConsumer.NULL_CONSUMER);
104    }
105
106    /**
107     * Creates an archive {@code target} using the format {@code
108     * format} by recursively including all files and directories in
109     * {@code directory}.
110     *
111     * <p>This method creates a wrapper around the archive stream and
112     * the caller of this method is responsible for closing it -
113     * probably at the same time as closing the stream itself. The
114     * caller is informed about the wrapper object via the {@code
115     * closeableConsumer} callback as soon as it is no longer needed
116     * by this class.</p>
117     *
118     * @param format the archive format. This uses the same format as
119     * accepted by {@link ArchiveStreamFactory}.
120     * @param target the stream to write the new archive to.
121     * @param directory the directory that contains the files to archive.
122     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
123     * @throws IOException if an I/O error occurs
124     * @throws ArchiveException if the archive cannot be created for other reasons
125     * @since 1.19
126     */
127    public void create(String format, OutputStream target, File directory,
128        CloseableConsumer closeableConsumer) throws IOException, ArchiveException {
129        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
130            create(c.track(new ArchiveStreamFactory().createArchiveOutputStream(format, target)),
131                directory);
132        }
133    }
134
135    /**
136     * Creates an archive {@code target} using the format {@code
137     * format} by recursively including all files and directories in
138     * {@code directory}.
139     *
140     * <p>This method creates a wrapper around the target channel
141     * which is never closed and thus leaks resources, please use
142     * {@link #create(String,SeekableByteChannel,File,CloseableConsumer)}
143     * instead.</p>
144     *
145     * @param format the archive format. This uses the same format as
146     * accepted by {@link ArchiveStreamFactory}.
147     * @param target the channel to write the new archive to.
148     * @param directory the directory that contains the files to archive.
149     * @throws IOException if an I/O error occurs
150     * @throws ArchiveException if the archive cannot be created for other reasons
151     * @deprecated this method leaks resources
152     */
153    @Deprecated
154    public void create(String format, SeekableByteChannel target, File directory)
155        throws IOException, ArchiveException {
156        create(format, target, directory, CloseableConsumer.NULL_CONSUMER);
157    }
158
159    /**
160     * Creates an archive {@code target} using the format {@code
161     * format} by recursively including all files and directories in
162     * {@code directory}.
163     *
164     * <p>This method creates a wrapper around the archive channel and
165     * the caller of this method is responsible for closing it -
166     * probably at the same time as closing the channel itself. The
167     * caller is informed about the wrapper object via the {@code
168     * closeableConsumer} callback as soon as it is no longer needed
169     * by this class.</p>
170     *
171     * @param format the archive format. This uses the same format as
172     * accepted by {@link ArchiveStreamFactory}.
173     * @param target the channel to write the new archive to.
174     * @param directory the directory that contains the files to archive.
175     * @param closeableConsumer is informed about the stream wrapped around the passed in stream
176     * @throws IOException if an I/O error occurs
177     * @throws ArchiveException if the archive cannot be created for other reasons
178     * @since 1.19
179     */
180    public void create(String format, SeekableByteChannel target, File directory,
181        CloseableConsumer closeableConsumer)
182        throws IOException, ArchiveException {
183        try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
184        if (!prefersSeekableByteChannel(format)) {
185            create(format, c.track(Channels.newOutputStream(target)), directory);
186        } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
187            create(c.track(new ZipArchiveOutputStream(target)), directory);
188        } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
189            create(c.track(new SevenZOutputFile(target)), directory);
190        } else {
191            // never reached as prefersSeekableByteChannel only returns true for ZIP and 7z
192            throw new ArchiveException("Don't know how to handle format " + format);
193        }
194        }
195    }
196
197    /**
198     * Creates an archive {@code target} by recursively including all
199     * files and directories in {@code directory}.
200     *
201     * @param target the stream to write the new archive to.
202     * @param directory the directory that contains the files to archive.
203     * @throws IOException if an I/O error occurs
204     * @throws ArchiveException if the archive cannot be created for other reasons
205     */
206    public void create(final ArchiveOutputStream target, File directory)
207        throws IOException, ArchiveException {
208        create(directory, new ArchiveEntryCreator() {
209            public ArchiveEntry create(File f, String entryName) throws IOException {
210                return target.createArchiveEntry(f, entryName);
211            }
212        }, new ArchiveEntryConsumer() {
213            public void accept(File source, ArchiveEntry e) throws IOException {
214                target.putArchiveEntry(e);
215                if (!e.isDirectory()) {
216                    try (InputStream in = new BufferedInputStream(Files.newInputStream(source.toPath()))) {
217                        IOUtils.copy(in, target);
218                    }
219                }
220                target.closeArchiveEntry();
221            }
222        }, new Finisher() {
223            public void finish() throws IOException {
224                target.finish();
225            }
226        });
227    }
228
229    /**
230     * Creates an archive {@code target} by recursively including all
231     * files and directories in {@code directory}.
232     *
233     * @param target the file to write the new archive to.
234     * @param directory the directory that contains the files to archive.
235     * @throws IOException if an I/O error occurs
236     */
237    public void create(final SevenZOutputFile target, File directory) throws IOException {
238        create(directory, new ArchiveEntryCreator() {
239            public ArchiveEntry create(File f, String entryName) throws IOException {
240                return target.createArchiveEntry(f, entryName);
241            }
242        }, new ArchiveEntryConsumer() {
243            public void accept(File source, ArchiveEntry e) throws IOException {
244                target.putArchiveEntry(e);
245                if (!e.isDirectory()) {
246                    final byte[] buffer = new byte[8024];
247                    int n = 0;
248                    long count = 0;
249                    try (InputStream in = new BufferedInputStream(Files.newInputStream(source.toPath()))) {
250                        while (-1 != (n = in.read(buffer))) {
251                            target.write(buffer, 0, n);
252                            count += n;
253                        }
254                    }
255                }
256                target.closeArchiveEntry();
257            }
258        }, new Finisher() {
259            public void finish() throws IOException {
260                target.finish();
261            }
262        });
263    }
264
265    private boolean prefersSeekableByteChannel(String format) {
266        return ArchiveStreamFactory.ZIP.equalsIgnoreCase(format) || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format);
267    }
268
269    private void create(File directory, ArchiveEntryCreator creator, ArchiveEntryConsumer consumer,
270        Finisher finisher) throws IOException {
271        create("", directory, creator, consumer);
272        finisher.finish();
273    }
274
275    private void create(String prefix, File directory, ArchiveEntryCreator creator, ArchiveEntryConsumer consumer)
276        throws IOException {
277        File[] children = directory.listFiles();
278        if (children == null) {
279            return;
280        }
281        for (File f : children) {
282            String entryName = prefix + f.getName() + (f.isDirectory() ? "/" : "");
283            consumer.accept(f, creator.create(f, entryName));
284            if (f.isDirectory()) {
285                create(entryName, f, creator, consumer);
286            }
287        }
288    }
289}