Skip to content

Commit 97867f6

Browse files
committed
COMPRESS-118 provide a high-level API for expanding archives
1 parent a291316 commit 97867f6

File tree

1 file changed

+375
-0
lines changed

1 file changed

+375
-0
lines changed
Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.commons.compress.archivers;
20+
21+
import java.io.BufferedInputStream;
22+
import java.io.File;
23+
import java.io.FileInputStream;
24+
import java.io.FileOutputStream;
25+
import java.io.IOException;
26+
import java.io.InputStream;
27+
import java.io.OutputStream;
28+
import java.nio.channels.Channels;
29+
import java.nio.channels.FileChannel;
30+
import java.nio.channels.SeekableByteChannel;
31+
import java.nio.file.StandardOpenOption;
32+
import java.util.Enumeration;
33+
34+
import org.apache.commons.compress.archivers.sevenz.SevenZFile;
35+
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
36+
import org.apache.commons.compress.archivers.zip.ZipFile;
37+
import org.apache.commons.compress.utils.IOUtils;
38+
39+
/**
40+
* Provides a high level API for expanding archives.
41+
* @since 1.17
42+
*/
43+
public class Expander {
44+
/**
45+
* Used to filter the entries to be extracted.
46+
*/
47+
public interface ArchiveEntryFilter {
48+
/**
49+
* @return true if the entry shall be expanded
50+
*/
51+
boolean accept(ArchiveEntry entry);
52+
}
53+
54+
private static final ArchiveEntryFilter ACCEPT_ALL = new ArchiveEntryFilter() {
55+
@Override
56+
public boolean accept(ArchiveEntry e) {
57+
return true;
58+
}
59+
};
60+
61+
private interface ArchiveEntrySupplier {
62+
ArchiveEntry getNextReadableEntry() throws IOException;
63+
}
64+
65+
private interface EntryWriter {
66+
void writeEntryDataTo(ArchiveEntry entry, OutputStream out) throws IOException;
67+
}
68+
69+
/**
70+
* Expands {@code archive} into {@code targetDirectory}.
71+
*
72+
* <p>Tries to auto-detect the archive's format.</p>
73+
*
74+
* @param archive the file to expand
75+
* @param targetDirectory the directory to write to
76+
*/
77+
public void expand(File archive, File targetDirectory) throws IOException, ArchiveException {
78+
expand(archive, targetDirectory, ACCEPT_ALL);
79+
}
80+
81+
/**
82+
* Expands {@code archive} into {@code targetDirectory}.
83+
*
84+
* @param archive the file to expand
85+
* @param targetDirectory the directory to write to
86+
* @param format the archive format. This uses the same format as
87+
* accepted by {@link ArchiveStreamFactory}.
88+
*/
89+
public void expand(String format, File archive, File targetDirectory) throws IOException, ArchiveException {
90+
expand(format, archive, targetDirectory, ACCEPT_ALL);
91+
}
92+
93+
/**
94+
* Expands {@code archive} into {@code targetDirectory}, using
95+
* only the entries accepted by the {@code filter}.
96+
*
97+
* <p>Tries to auto-detect the archive's format.</p>
98+
*
99+
* @param archive the file to expand
100+
* @param targetDirectory the directory to write to
101+
* @param filter selects the entries to expand
102+
*/
103+
public void expand(File archive, File targetDirectory, ArchiveEntryFilter filter)
104+
throws IOException, ArchiveException {
105+
String format = null;
106+
try (InputStream i = new BufferedInputStream(new FileInputStream(archive))) {
107+
format = new ArchiveStreamFactory().detect(i);
108+
}
109+
expand(format, archive, targetDirectory, filter);
110+
}
111+
112+
/**
113+
* Expands {@code archive} into {@code targetDirectory}, using
114+
* only the entries accepted by the {@code filter}.
115+
*
116+
* @param archive the file to expand
117+
* @param targetDirectory the directory to write to
118+
* @param format the archive format. This uses the same format as
119+
* accepted by {@link ArchiveStreamFactory}.
120+
* @param filter selects the entries to expand
121+
*/
122+
public void expand(String format, File archive, File targetDirectory, ArchiveEntryFilter filter)
123+
throws IOException, ArchiveException {
124+
if (prefersSeekableByteChannel(format)) {
125+
try (SeekableByteChannel c = FileChannel.open(archive.toPath(), StandardOpenOption.READ)) {
126+
expand(format, c, targetDirectory, filter);
127+
}
128+
return;
129+
}
130+
try (InputStream i = new BufferedInputStream(new FileInputStream(archive))) {
131+
expand(format, i, targetDirectory, filter);
132+
}
133+
}
134+
135+
/**
136+
* Expands {@code archive} into {@code targetDirectory}.
137+
*
138+
* <p>Tries to auto-detect the archive's format.</p>
139+
*
140+
* @param archive the file to expand
141+
* @param targetDirectory the directory to write to
142+
*/
143+
public void expand(InputStream archive, File targetDirectory) throws IOException, ArchiveException {
144+
expand(archive, targetDirectory, ACCEPT_ALL);
145+
}
146+
147+
/**
148+
* Expands {@code archive} into {@code targetDirectory}.
149+
*
150+
* @param archive the file to expand
151+
* @param targetDirectory the directory to write to
152+
* @param format the archive format. This uses the same format as
153+
* accepted by {@link ArchiveStreamFactory}.
154+
*/
155+
public void expand(String format, InputStream archive, File targetDirectory)
156+
throws IOException, ArchiveException {
157+
expand(format, archive, targetDirectory, ACCEPT_ALL);
158+
}
159+
160+
/**
161+
* Expands {@code archive} into {@code targetDirectory}, using
162+
* only the entries accepted by the {@code filter}.
163+
*
164+
* <p>Tries to auto-detect the archive's format.</p>
165+
*
166+
* @param archive the file to expand
167+
* @param targetDirectory the directory to write to
168+
* @param filter selects the entries to expand
169+
*/
170+
public void expand(InputStream archive, File targetDirectory, ArchiveEntryFilter filter)
171+
throws IOException, ArchiveException {
172+
expand(new ArchiveStreamFactory().createArchiveInputStream(archive), targetDirectory, filter);
173+
}
174+
175+
/**
176+
* Expands {@code archive} into {@code targetDirectory}, using
177+
* only the entries accepted by the {@code filter}.
178+
*
179+
* @param archive the file to expand
180+
* @param targetDirectory the directory to write to
181+
* @param format the archive format. This uses the same format as
182+
* accepted by {@link ArchiveStreamFactory}.
183+
* @param filter selects the entries to expand
184+
*/
185+
public void expand(String format, InputStream archive, File targetDirectory, ArchiveEntryFilter filter)
186+
throws IOException, ArchiveException {
187+
expand(new ArchiveStreamFactory().createArchiveInputStream(format, archive), targetDirectory, filter);
188+
}
189+
190+
/**
191+
* Expands {@code archive} into {@code targetDirectory}.
192+
*
193+
* @param archive the file to expand
194+
* @param targetDirectory the directory to write to
195+
* @param format the archive format. This uses the same format as
196+
* accepted by {@link ArchiveStreamFactory}.
197+
*/
198+
public void expand(String format, SeekableByteChannel archive, File targetDirectory)
199+
throws IOException, ArchiveException {
200+
expand(format, archive, targetDirectory, ACCEPT_ALL);
201+
}
202+
203+
/**
204+
* Expands {@code archive} into {@code targetDirectory}, using
205+
* only the entries accepted by the {@code filter}.
206+
*
207+
* @param archive the file to expand
208+
* @param targetDirectory the directory to write to
209+
* @param the format of the archive
210+
* @param format the archive format. This uses the same format as
211+
* accepted by {@link ArchiveStreamFactory}.
212+
* @param filter selects the entries to expand
213+
*/
214+
public void expand(String format, SeekableByteChannel archive, File targetDirectory, ArchiveEntryFilter filter)
215+
throws IOException, ArchiveException {
216+
if (!prefersSeekableByteChannel(format)) {
217+
expand(format, Channels.newInputStream(archive), targetDirectory, filter);
218+
} else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
219+
expand(new ZipFile(archive), targetDirectory, filter);
220+
} else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
221+
expand(new SevenZFile(archive), targetDirectory, filter);
222+
} else {
223+
throw new ArchiveException("don't know how to handle format " + format);
224+
}
225+
}
226+
227+
/**
228+
* Expands {@code archive} into {@code targetDirectory}.
229+
*
230+
* @param archive the file to expand
231+
* @param targetDirectory the directory to write to
232+
*/
233+
public void expand(ArchiveInputStream archive, File targetDirectory)
234+
throws IOException, ArchiveException {
235+
expand(archive, targetDirectory, ACCEPT_ALL);
236+
}
237+
238+
/**
239+
* Expands {@code archive} into {@code targetDirectory}, using
240+
* only the entries accepted by the {@code filter}.
241+
*
242+
* @param archive the file to expand
243+
* @param targetDirectory the directory to write to
244+
* @param filter selects the entries to expand
245+
*/
246+
public void expand(final ArchiveInputStream archive, File targetDirectory, ArchiveEntryFilter filter)
247+
throws IOException, ArchiveException {
248+
expand(new ArchiveEntrySupplier() {
249+
@Override
250+
public ArchiveEntry getNextReadableEntry() throws IOException {
251+
ArchiveEntry next = archive.getNextEntry();
252+
while (next != null && !archive.canReadEntryData(next)) {
253+
next = archive.getNextEntry();
254+
}
255+
return next;
256+
}
257+
}, new EntryWriter() {
258+
@Override
259+
public void writeEntryDataTo(ArchiveEntry entry, OutputStream out) throws IOException {
260+
IOUtils.copy(archive, out);
261+
}
262+
}, targetDirectory, filter);
263+
}
264+
265+
/**
266+
* Expands {@code archive} into {@code targetDirectory}.
267+
*
268+
* @param archive the file to expand
269+
* @param targetDirectory the directory to write to
270+
*/
271+
public void expand(ZipFile archive, File targetDirectory)
272+
throws IOException, ArchiveException {
273+
expand(archive, targetDirectory, ACCEPT_ALL);
274+
}
275+
276+
/**
277+
* Expands {@code archive} into {@code targetDirectory}, using
278+
* only the entries accepted by the {@code filter}.
279+
*
280+
* @param archive the file to expand
281+
* @param targetDirectory the directory to write to
282+
* @param filter selects the entries to expand
283+
*/
284+
public void expand(final ZipFile archive, File targetDirectory, ArchiveEntryFilter filter)
285+
throws IOException, ArchiveException {
286+
final Enumeration<ZipArchiveEntry> entries = archive.getEntries();
287+
expand(new ArchiveEntrySupplier() {
288+
@Override
289+
public ArchiveEntry getNextReadableEntry() throws IOException {
290+
ZipArchiveEntry next = entries.hasMoreElements() ? entries.nextElement() : null;
291+
while (next != null && !archive.canReadEntryData(next)) {
292+
next = entries.hasMoreElements() ? entries.nextElement() : null;
293+
}
294+
return next;
295+
}
296+
}, new EntryWriter() {
297+
@Override
298+
public void writeEntryDataTo(ArchiveEntry entry, OutputStream out) throws IOException {
299+
try (InputStream in = archive.getInputStream((ZipArchiveEntry) entry)) {
300+
IOUtils.copy(in, out);
301+
}
302+
}
303+
}, targetDirectory, filter);
304+
}
305+
306+
/**
307+
* Expands {@code archive} into {@code targetDirectory}.
308+
*
309+
* @param archive the file to expand
310+
* @param targetDirectory the directory to write to
311+
*/
312+
public void expand(SevenZFile archive, File targetDirectory)
313+
throws IOException, ArchiveException {
314+
expand(archive, targetDirectory, ACCEPT_ALL);
315+
}
316+
317+
/**
318+
* Expands {@code archive} into {@code targetDirectory}, using
319+
* only the entries accepted by the {@code filter}.
320+
*
321+
* @param archive the file to expand
322+
* @param targetDirectory the directory to write to
323+
* @param filter selects the entries to expand
324+
*/
325+
public void expand(final SevenZFile archive, File targetDirectory, ArchiveEntryFilter filter)
326+
throws IOException, ArchiveException {
327+
expand(new ArchiveEntrySupplier() {
328+
@Override
329+
public ArchiveEntry getNextReadableEntry() throws IOException {
330+
return archive.getNextEntry();
331+
}
332+
}, new EntryWriter() {
333+
@Override
334+
public void writeEntryDataTo(ArchiveEntry entry, OutputStream out) throws IOException {
335+
final byte[] buffer = new byte[8024];
336+
int n = 0;
337+
long count = 0;
338+
while (-1 != (n = archive.read(buffer))) {
339+
out.write(buffer, 0, n);
340+
count += n;
341+
}
342+
}
343+
}, targetDirectory, filter);
344+
}
345+
346+
private boolean prefersSeekableByteChannel(String format) {
347+
return ArchiveStreamFactory.ZIP.equalsIgnoreCase(format) || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format);
348+
}
349+
350+
private void expand(ArchiveEntrySupplier supplier, EntryWriter writer, File targetDirectory, ArchiveEntryFilter filter)
351+
throws IOException {
352+
String targetDirPath = targetDirectory.getCanonicalPath();
353+
ArchiveEntry nextEntry = supplier.getNextReadableEntry();
354+
while (nextEntry != null) {
355+
if (!filter.accept(nextEntry)) {
356+
continue;
357+
}
358+
File f = new File(targetDirectory, nextEntry.getName());
359+
if (!f.getCanonicalPath().startsWith(targetDirPath)) {
360+
throw new IOException("expanding " + nextEntry.getName()
361+
+ " would craete file outside of " + targetDirectory);
362+
}
363+
if (nextEntry.isDirectory()) {
364+
f.mkdirs();
365+
} else {
366+
f.getParentFile().mkdirs();
367+
try (OutputStream o = new FileOutputStream(f)) {
368+
writer.writeEntryDataTo(nextEntry, o);
369+
}
370+
}
371+
nextEntry = supplier.getNextReadableEntry();
372+
}
373+
}
374+
375+
}

0 commit comments

Comments
 (0)