Repository: commons-compress Updated Branches: refs/heads/master a29131675 -> 97867f6fa
COMPRESS-118 provide a high-level API for expanding archives Project: http://git-wip-us.apache.org/repos/asf/commons-compress/repo Commit: http://git-wip-us.apache.org/repos/asf/commons-compress/commit/97867f6f Tree: http://git-wip-us.apache.org/repos/asf/commons-compress/tree/97867f6f Diff: http://git-wip-us.apache.org/repos/asf/commons-compress/diff/97867f6f Branch: refs/heads/master Commit: 97867f6fa3634c77dfafd76c89ecb1087f5cd1ae Parents: a291316 Author: Stefan Bodewig <[email protected]> Authored: Mon Apr 23 20:33:18 2018 +0200 Committer: Stefan Bodewig <[email protected]> Committed: Mon Apr 23 20:33:18 2018 +0200 ---------------------------------------------------------------------- .../commons/compress/archivers/Expander.java | 375 +++++++++++++++++++ 1 file changed, 375 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/commons-compress/blob/97867f6f/src/main/java/org/apache/commons/compress/archivers/Expander.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/commons/compress/archivers/Expander.java b/src/main/java/org/apache/commons/compress/archivers/Expander.java new file mode 100644 index 0000000..5f66965 --- /dev/null +++ b/src/main/java/org/apache/commons/compress/archivers/Expander.java @@ -0,0 +1,375 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.commons.compress.archivers; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.StandardOpenOption; +import java.util.Enumeration; + +import org.apache.commons.compress.archivers.sevenz.SevenZFile; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipFile; +import org.apache.commons.compress.utils.IOUtils; + +/** + * Provides a high level API for expanding archives. + * @since 1.17 + */ +public class Expander { + /** + * Used to filter the entries to be extracted. + */ + public interface ArchiveEntryFilter { + /** + * @return true if the entry shall be expanded + */ + boolean accept(ArchiveEntry entry); + } + + private static final ArchiveEntryFilter ACCEPT_ALL = new ArchiveEntryFilter() { + @Override + public boolean accept(ArchiveEntry e) { + return true; + } + }; + + private interface ArchiveEntrySupplier { + ArchiveEntry getNextReadableEntry() throws IOException; + } + + private interface EntryWriter { + void writeEntryDataTo(ArchiveEntry entry, OutputStream out) throws IOException; + } + + /** + * Expands {@code archive} into {@code targetDirectory}. + * + * <p>Tries to auto-detect the archive's format.</p> + * + * @param archive the file to expand + * @param targetDirectory the directory to write to + */ + public void expand(File archive, File targetDirectory) throws IOException, ArchiveException { + expand(archive, targetDirectory, ACCEPT_ALL); + } + + /** + * Expands {@code archive} into {@code targetDirectory}. + * + * @param archive the file to expand + * @param targetDirectory the directory to write to + * @param format the archive format. This uses the same format as + * accepted by {@link ArchiveStreamFactory}. + */ + public void expand(String format, File archive, File targetDirectory) throws IOException, ArchiveException { + expand(format, archive, targetDirectory, ACCEPT_ALL); + } + + /** + * Expands {@code archive} into {@code targetDirectory}, using + * only the entries accepted by the {@code filter}. + * + * <p>Tries to auto-detect the archive's format.</p> + * + * @param archive the file to expand + * @param targetDirectory the directory to write to + * @param filter selects the entries to expand + */ + public void expand(File archive, File targetDirectory, ArchiveEntryFilter filter) + throws IOException, ArchiveException { + String format = null; + try (InputStream i = new BufferedInputStream(new FileInputStream(archive))) { + format = new ArchiveStreamFactory().detect(i); + } + expand(format, archive, targetDirectory, filter); + } + + /** + * Expands {@code archive} into {@code targetDirectory}, using + * only the entries accepted by the {@code filter}. + * + * @param archive the file to expand + * @param targetDirectory the directory to write to + * @param format the archive format. This uses the same format as + * accepted by {@link ArchiveStreamFactory}. + * @param filter selects the entries to expand + */ + public void expand(String format, File archive, File targetDirectory, ArchiveEntryFilter filter) + throws IOException, ArchiveException { + if (prefersSeekableByteChannel(format)) { + try (SeekableByteChannel c = FileChannel.open(archive.toPath(), StandardOpenOption.READ)) { + expand(format, c, targetDirectory, filter); + } + return; + } + try (InputStream i = new BufferedInputStream(new FileInputStream(archive))) { + expand(format, i, targetDirectory, filter); + } + } + + /** + * Expands {@code archive} into {@code targetDirectory}. + * + * <p>Tries to auto-detect the archive's format.</p> + * + * @param archive the file to expand + * @param targetDirectory the directory to write to + */ + public void expand(InputStream archive, File targetDirectory) throws IOException, ArchiveException { + expand(archive, targetDirectory, ACCEPT_ALL); + } + + /** + * Expands {@code archive} into {@code targetDirectory}. + * + * @param archive the file to expand + * @param targetDirectory the directory to write to + * @param format the archive format. This uses the same format as + * accepted by {@link ArchiveStreamFactory}. + */ + public void expand(String format, InputStream archive, File targetDirectory) + throws IOException, ArchiveException { + expand(format, archive, targetDirectory, ACCEPT_ALL); + } + + /** + * Expands {@code archive} into {@code targetDirectory}, using + * only the entries accepted by the {@code filter}. + * + * <p>Tries to auto-detect the archive's format.</p> + * + * @param archive the file to expand + * @param targetDirectory the directory to write to + * @param filter selects the entries to expand + */ + public void expand(InputStream archive, File targetDirectory, ArchiveEntryFilter filter) + throws IOException, ArchiveException { + expand(new ArchiveStreamFactory().createArchiveInputStream(archive), targetDirectory, filter); + } + + /** + * Expands {@code archive} into {@code targetDirectory}, using + * only the entries accepted by the {@code filter}. + * + * @param archive the file to expand + * @param targetDirectory the directory to write to + * @param format the archive format. This uses the same format as + * accepted by {@link ArchiveStreamFactory}. + * @param filter selects the entries to expand + */ + public void expand(String format, InputStream archive, File targetDirectory, ArchiveEntryFilter filter) + throws IOException, ArchiveException { + expand(new ArchiveStreamFactory().createArchiveInputStream(format, archive), targetDirectory, filter); + } + + /** + * Expands {@code archive} into {@code targetDirectory}. + * + * @param archive the file to expand + * @param targetDirectory the directory to write to + * @param format the archive format. This uses the same format as + * accepted by {@link ArchiveStreamFactory}. + */ + public void expand(String format, SeekableByteChannel archive, File targetDirectory) + throws IOException, ArchiveException { + expand(format, archive, targetDirectory, ACCEPT_ALL); + } + + /** + * Expands {@code archive} into {@code targetDirectory}, using + * only the entries accepted by the {@code filter}. + * + * @param archive the file to expand + * @param targetDirectory the directory to write to + * @param the format of the archive + * @param format the archive format. This uses the same format as + * accepted by {@link ArchiveStreamFactory}. + * @param filter selects the entries to expand + */ + public void expand(String format, SeekableByteChannel archive, File targetDirectory, ArchiveEntryFilter filter) + throws IOException, ArchiveException { + if (!prefersSeekableByteChannel(format)) { + expand(format, Channels.newInputStream(archive), targetDirectory, filter); + } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) { + expand(new ZipFile(archive), targetDirectory, filter); + } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) { + expand(new SevenZFile(archive), targetDirectory, filter); + } else { + throw new ArchiveException("don't know how to handle format " + format); + } + } + + /** + * Expands {@code archive} into {@code targetDirectory}. + * + * @param archive the file to expand + * @param targetDirectory the directory to write to + */ + public void expand(ArchiveInputStream archive, File targetDirectory) + throws IOException, ArchiveException { + expand(archive, targetDirectory, ACCEPT_ALL); + } + + /** + * Expands {@code archive} into {@code targetDirectory}, using + * only the entries accepted by the {@code filter}. + * + * @param archive the file to expand + * @param targetDirectory the directory to write to + * @param filter selects the entries to expand + */ + public void expand(final ArchiveInputStream archive, File targetDirectory, ArchiveEntryFilter filter) + throws IOException, ArchiveException { + expand(new ArchiveEntrySupplier() { + @Override + public ArchiveEntry getNextReadableEntry() throws IOException { + ArchiveEntry next = archive.getNextEntry(); + while (next != null && !archive.canReadEntryData(next)) { + next = archive.getNextEntry(); + } + return next; + } + }, new EntryWriter() { + @Override + public void writeEntryDataTo(ArchiveEntry entry, OutputStream out) throws IOException { + IOUtils.copy(archive, out); + } + }, targetDirectory, filter); + } + + /** + * Expands {@code archive} into {@code targetDirectory}. + * + * @param archive the file to expand + * @param targetDirectory the directory to write to + */ + public void expand(ZipFile archive, File targetDirectory) + throws IOException, ArchiveException { + expand(archive, targetDirectory, ACCEPT_ALL); + } + + /** + * Expands {@code archive} into {@code targetDirectory}, using + * only the entries accepted by the {@code filter}. + * + * @param archive the file to expand + * @param targetDirectory the directory to write to + * @param filter selects the entries to expand + */ + public void expand(final ZipFile archive, File targetDirectory, ArchiveEntryFilter filter) + throws IOException, ArchiveException { + final Enumeration<ZipArchiveEntry> entries = archive.getEntries(); + expand(new ArchiveEntrySupplier() { + @Override + public ArchiveEntry getNextReadableEntry() throws IOException { + ZipArchiveEntry next = entries.hasMoreElements() ? entries.nextElement() : null; + while (next != null && !archive.canReadEntryData(next)) { + next = entries.hasMoreElements() ? entries.nextElement() : null; + } + return next; + } + }, new EntryWriter() { + @Override + public void writeEntryDataTo(ArchiveEntry entry, OutputStream out) throws IOException { + try (InputStream in = archive.getInputStream((ZipArchiveEntry) entry)) { + IOUtils.copy(in, out); + } + } + }, targetDirectory, filter); + } + + /** + * Expands {@code archive} into {@code targetDirectory}. + * + * @param archive the file to expand + * @param targetDirectory the directory to write to + */ + public void expand(SevenZFile archive, File targetDirectory) + throws IOException, ArchiveException { + expand(archive, targetDirectory, ACCEPT_ALL); + } + + /** + * Expands {@code archive} into {@code targetDirectory}, using + * only the entries accepted by the {@code filter}. + * + * @param archive the file to expand + * @param targetDirectory the directory to write to + * @param filter selects the entries to expand + */ + public void expand(final SevenZFile archive, File targetDirectory, ArchiveEntryFilter filter) + throws IOException, ArchiveException { + expand(new ArchiveEntrySupplier() { + @Override + public ArchiveEntry getNextReadableEntry() throws IOException { + return archive.getNextEntry(); + } + }, new EntryWriter() { + @Override + public void writeEntryDataTo(ArchiveEntry entry, OutputStream out) throws IOException { + final byte[] buffer = new byte[8024]; + int n = 0; + long count = 0; + while (-1 != (n = archive.read(buffer))) { + out.write(buffer, 0, n); + count += n; + } + } + }, targetDirectory, filter); + } + + private boolean prefersSeekableByteChannel(String format) { + return ArchiveStreamFactory.ZIP.equalsIgnoreCase(format) || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format); + } + + private void expand(ArchiveEntrySupplier supplier, EntryWriter writer, File targetDirectory, ArchiveEntryFilter filter) + throws IOException { + String targetDirPath = targetDirectory.getCanonicalPath(); + ArchiveEntry nextEntry = supplier.getNextReadableEntry(); + while (nextEntry != null) { + if (!filter.accept(nextEntry)) { + continue; + } + File f = new File(targetDirectory, nextEntry.getName()); + if (!f.getCanonicalPath().startsWith(targetDirPath)) { + throw new IOException("expanding " + nextEntry.getName() + + " would craete file outside of " + targetDirectory); + } + if (nextEntry.isDirectory()) { + f.mkdirs(); + } else { + f.getParentFile().mkdirs(); + try (OutputStream o = new FileOutputStream(f)) { + writer.writeEntryDataTo(nextEntry, o); + } + } + nextEntry = supplier.getNextReadableEntry(); + } + } + +}
