001/*
002 * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Nuxeo - initial API and implementation
018 */
019package org.nuxeo.common.utils;
020
021import static java.nio.charset.StandardCharsets.UTF_8;
022
023import java.io.BufferedInputStream;
024import java.io.BufferedOutputStream;
025import java.io.File;
026import java.io.FileInputStream;
027import java.io.FileOutputStream;
028import java.io.IOException;
029import java.io.InputStream;
030import java.io.OutputStream;
031import java.net.URL;
032import java.util.ArrayList;
033import java.util.Enumeration;
034import java.util.List;
035import java.util.function.Function;
036import java.util.function.Predicate;
037import java.util.zip.ZipEntry;
038import java.util.zip.ZipFile;
039import java.util.zip.ZipInputStream;
040import java.util.zip.ZipOutputStream;
041
042import org.apache.commons.io.FileUtils;
043import org.apache.commons.io.IOUtils;
044import org.apache.commons.lang3.StringUtils;
045
046/**
047 * @author bstefanescu
048 */
049public final class ZipUtils {
050
051    // This is an utility class
052    private ZipUtils() {
053    }
054
055    // _____________________________ ZIP ________________________________
056
057    public static void _putDirectoryEntry(String entryName, ZipOutputStream out) throws IOException {
058        ZipEntry zentry = new ZipEntry(entryName + '/');
059        out.putNextEntry(zentry);
060        out.closeEntry();
061    }
062
063    public static void _putFileEntry(File file, String entryName, ZipOutputStream out) throws IOException {
064        try (FileInputStream in = new FileInputStream(file)) {
065            _zip(entryName, in, out);
066        }
067    }
068
069    public static void _zip(String entryName, InputStream in, ZipOutputStream out) throws IOException {
070        ZipEntry zentry = new ZipEntry(entryName);
071        out.putNextEntry(zentry);
072        // Transfer bytes from the input stream to the ZIP file
073        IOUtils.copy(in, out);
074        out.closeEntry();
075    }
076
077    public static void _zip(String entryName, File file, ZipOutputStream out) throws IOException {
078        // System.out.println("Compressing "+entryName);
079        if (file.isDirectory()) {
080            entryName += '/';
081            ZipEntry zentry = new ZipEntry(entryName);
082            out.putNextEntry(zentry);
083            out.closeEntry();
084            File[] files = file.listFiles();
085            for (File child : files) {
086                _zip(entryName + child.getName(), child, out);
087            }
088        } else {
089            InputStream in = null;
090            try {
091                in = new BufferedInputStream(new FileInputStream(file));
092                _zip(entryName, in, out);
093            } finally {
094                if (in != null) {
095                    in.close();
096                }
097            }
098        }
099    }
100
101    public static void _zip(File[] files, ZipOutputStream out, String prefix) throws IOException {
102        String normalizedPrefix = normalizePrefix(prefix);
103        for (File file : files) {
104            String name = normalizedPrefix + file.getName();
105            _zip(name, file, out);
106        }
107    }
108
109    public static void zip(File file, OutputStream out, String prefix) throws IOException {
110        prefix = normalizePrefix(prefix);
111        String name = normalizePrefix(prefix) + file.getName();
112        try (ZipOutputStream zout = new ZipOutputStream(out)) {
113            _zip(name, file, zout);
114        }
115    }
116
117    /**
118     * @return the empty string if prefix is null or empty, otherwise append a '/' at the end if needed
119     */
120    protected static String normalizePrefix(String prefix) {
121        String result = StringUtils.defaultString(prefix);
122        int len = result.length();
123        if (len > 0 && result.charAt(len - 1) != '/') {
124            result += '/';
125        }
126        return result;
127    }
128
129    public static void zip(File[] files, OutputStream out, String prefix) throws IOException {
130        try (ZipOutputStream zout = new ZipOutputStream(out)) {
131            _zip(files, zout, prefix);
132        }
133    }
134
135    public static void zip(File file, File zip) throws IOException {
136        try (OutputStream out = new BufferedOutputStream(new FileOutputStream(zip))) {
137            zip(file, out, null);
138        }
139    }
140
141    public static void zip(File[] files, File zip) throws IOException {
142        try (OutputStream out = new BufferedOutputStream(new FileOutputStream(zip))) {
143            zip(files, out, null);
144        }
145    }
146
147    public static void zip(File file, File zip, String prefix) throws IOException {
148        try (OutputStream out = new BufferedOutputStream(new FileOutputStream(zip))) {
149            zip(file, out, prefix);
150        }
151    }
152
153    public static void zip(File[] files, File zip, String prefix) throws IOException {
154        try (OutputStream out = new BufferedOutputStream(new FileOutputStream(zip))) {
155            zip(files, out, prefix);
156        }
157    }
158
159    public static void zipFilesUsingPrefix(String prefix, File[] files, OutputStream out) throws IOException {
160        try (ZipOutputStream zout = new ZipOutputStream(out)) {
161            if (prefix != null && prefix.length() > 0) {
162                int p = prefix.indexOf('/');
163                while (p > -1) {
164                    _putDirectoryEntry(prefix.substring(0, p), zout);
165                    p = prefix.indexOf(p + 1, '/');
166                }
167                _putDirectoryEntry(prefix, zout);
168                prefix += '/';
169            } else {
170                prefix = "";
171            }
172            // prefix = prefix + '/';
173            for (File file : files) {
174                _putFileEntry(file, prefix + file.getName(), zout);
175            }
176        }
177    }
178
179    // _____________________________ UNZIP ________________________________
180
181    public static void unzip(String prefix, InputStream zipStream, File dir) throws IOException {
182        try (ZipInputStream in = new ZipInputStream(new BufferedInputStream(zipStream))) {
183            unzip(prefix, in, dir);
184        }
185    }
186
187    public static void unzip(InputStream zipStream, File dir) throws IOException {
188        try (ZipInputStream in = new ZipInputStream(new BufferedInputStream(zipStream))) {
189            unzip(in, dir);
190        }
191    }
192
193    public static void unzip(String prefix, URL zip, File dir) throws IOException {
194        try (ZipInputStream in = new ZipInputStream(new BufferedInputStream(zip.openStream()))) {
195            unzip(prefix, in, dir);
196        }
197    }
198
199    public static void unzip(URL zip, File dir) throws IOException {
200        try (ZipInputStream in = new ZipInputStream(new BufferedInputStream(zip.openStream()))) {
201            unzip(in, dir);
202        }
203    }
204
205    public static void unzip(String prefix, File zip, File dir) throws IOException {
206        try (ZipInputStream in = new ZipInputStream(new BufferedInputStream(new FileInputStream(zip)))) {
207            unzip(prefix, in, dir);
208        }
209    }
210
211    public static void unzip(File zip, File dir) throws IOException {
212        try (ZipInputStream in = new ZipInputStream(new BufferedInputStream(new FileInputStream(zip)))) {
213            unzip(in, dir);
214        }
215    }
216
217    public static void unzip(String prefix, ZipInputStream in, File dir) throws IOException {
218        unzip(in, dir, entry -> entry.getName().startsWith(prefix), name -> name.substring(prefix.length()));
219    }
220
221    public static void unzip(ZipInputStream in, File dir) throws IOException {
222        unzip(in, dir, entry -> true, Function.identity());
223    }
224
225    private static void unzip(ZipInputStream in, File dir, Predicate<ZipEntry> filter,
226            Function<String, String> nameFormatter) throws IOException {
227        dir.mkdirs();
228        ZipEntry entry;
229        while ((entry = in.getNextEntry()) != null) {
230            if (!entry.getName().contains("..") && filter.test(entry)) {
231                File file = new File(dir, nameFormatter.apply(entry.getName()));
232                if (entry.isDirectory()) {
233                    file.mkdirs();
234                } else {
235                    file.getParentFile().mkdirs();
236                    try (FileOutputStream output = FileUtils.openOutputStream(file)) {
237                        IOUtils.copy(in, output);
238                    }
239                }
240            }
241        }
242    }
243
244    public static void unzipIgnoreDirs(ZipInputStream in, File dir) throws IOException {
245        unzip(in, dir, entry -> !entry.isDirectory(), Function.identity());
246    }
247
248    public static void unzipIgnoreDirs(InputStream zipStream, File dir) throws IOException {
249        try (ZipInputStream in = new ZipInputStream(new BufferedInputStream(zipStream))) {
250            unzipIgnoreDirs(in, dir);
251        }
252    }
253
254    public static void unzip(File zip, File dir, PathFilter filter) throws IOException {
255        try (ZipInputStream in = new ZipInputStream(new BufferedInputStream(new FileInputStream(zip)))) {
256            unzip(in, dir, filter);
257        }
258    }
259
260    public static void unzip(ZipInputStream in, File dir, PathFilter filter) throws IOException {
261        if (filter == null) {
262            unzip(in, dir);
263        } else {
264            unzip(in, dir, toPredicate(filter), Function.identity());
265        }
266    }
267
268    public static void unzip(String prefix, File zip, File dir, PathFilter filter) throws IOException {
269        try (ZipInputStream in = new ZipInputStream(new BufferedInputStream(new FileInputStream(zip)))) {
270            unzip(prefix, in, dir, filter);
271        }
272    }
273
274    public static void unzip(String prefix, ZipInputStream in, File dir, PathFilter filter) throws IOException {
275        if (filter == null) {
276            unzip(prefix, in, dir);
277        } else {
278            unzip(in, dir, toPredicate(filter).and(entry -> entry.getName().startsWith(prefix)),
279                    name -> name.substring(prefix.length()));
280        }
281    }
282
283    private static Predicate<ZipEntry> toPredicate(PathFilter filter) {
284        return entry -> filter.accept(new org.nuxeo.common.utils.Path(entry.getName()));
285    }
286
287    // ________________ Entries ________________
288    /**
289     * Unzip directly the entry. The returned InputStream has to be closed.
290     *
291     * @return the input stream of the desired entry - has to be closed by the caller, or null if not found
292     * @param file the source file
293     * @param entryName the entry name that has to be extracted
294     * @deprecated since 10.1 (unused and fails to close a ZipFile)
295     */
296    @Deprecated
297    public static InputStream getEntryContentAsStream(File file, String entryName) throws IOException {
298        InputStream result = null;
299        ZipFile zip = new ZipFile(file);
300        ZipEntry entry = zip.getEntry(entryName);
301        if (entry != null) {
302            result = zip.getInputStream(entry);
303        }
304        return result;
305    }
306
307    /**
308     * Unzip directly the entry.
309     *
310     * @return the String content of the entry with name entryName
311     * @param file the source file
312     * @param entryName the entry name that has to be extracted
313     * @deprecated since 10.1 (unused and fails to close a ZipFile)
314     */
315    @Deprecated
316    public static String getEntryContentAsString(File file, String entryName) throws IOException {
317        try (InputStream resultStream = getEntryContentAsStream(file, entryName)) {
318            return IOUtils.toString(resultStream, UTF_8);
319        }
320    }
321
322    /**
323     * Unzips directly the entry.
324     *
325     * @return The byte array content of the entry with name entryName
326     * @param file the source file
327     * @param entryName the entry name that has to be extracted
328     * @deprecated since 10.1 (unused and fails to close a ZipFile)
329     */
330    @Deprecated
331    public static byte[] getEntryContentAsBytes(File file, String entryName) throws IOException {
332        try (InputStream resultStream = getEntryContentAsStream(file, entryName)) {
333            return IOUtils.toByteArray(resultStream);
334        }
335    }
336
337    /**
338     * Lists the entries on the zip file.
339     *
340     * @param file The zip file
341     * @return The list of entries
342     */
343    public static List<String> getEntryNames(File file) throws IOException {
344        List<String> result = new ArrayList<>();
345        try (ZipFile zip = new ZipFile(file)) {
346            Enumeration<? extends ZipEntry> entries = zip.entries();
347            while (entries.hasMoreElements()) {
348                ZipEntry entry = entries.nextElement();
349                result.add(entry.getName());
350            }
351        }
352        return result;
353    }
354
355    /**
356     * Checks if a zip file contains a specified entry name.
357     *
358     * @param file the zip file
359     * @param entryName The content to be checked
360     * @return True if the file contains entryName. False otherwise
361     */
362    public static boolean hasEntry(File file, String entryName) throws IOException {
363        List<String> elements = getEntryNames(file);
364        return elements.contains(entryName);
365    }
366
367    public static InputStream getEntryContentAsStream(InputStream stream, String entryName) throws IOException {
368        ZipInputStream zip = new ZipInputStream(stream);
369        ZipEntry entry = zip.getNextEntry();
370        while (entry != null) {
371            if (entry.getName().equals(entryName)) {
372                return zip;
373            }
374            entry = zip.getNextEntry();
375        }
376        return null;
377    }
378
379    public static String getEntryContentAsString(InputStream stream, String searchedEntryName) throws IOException {
380        try (InputStream resultStream = getEntryContentAsStream(stream, searchedEntryName)) {
381            return IOUtils.toString(resultStream, UTF_8);
382        }
383    }
384
385    public static byte[] getEntryContentAsBytes(InputStream stream, String searchedEntryName) throws IOException {
386        try (InputStream resultStream = getEntryContentAsStream(stream, searchedEntryName)) {
387            return IOUtils.toByteArray(resultStream);
388        }
389    }
390
391    public static List<String> getEntryNames(InputStream stream) throws IOException {
392
393        List<String> result = new ArrayList<>();
394        try (ZipInputStream zip = new ZipInputStream(stream)) {
395            while (zip.available() == 1) {
396                ZipEntry entry = zip.getNextEntry();
397                if (entry != null) {
398                    result.add(entry.getName());
399                }
400            }
401        }
402        return result;
403    }
404
405    public static boolean hasEntry(InputStream stream, String entryName) throws IOException {
406        List<String> elements = getEntryNames(stream);
407        return elements.contains(entryName);
408    }
409
410    public static InputStream getEntryContentAsStream(URL url, String entryName) throws IOException {
411        return getEntryContentAsStream(url.openStream(), entryName);
412    }
413
414    public static String getEntryContentAsString(URL url, String entryName) throws IOException {
415        try (InputStream resultStream = getEntryContentAsStream(url, entryName)) {
416            return IOUtils.toString(resultStream, UTF_8);
417        }
418    }
419
420    public static byte[] getEntryContentAsBytes(URL url, String entryName) throws IOException {
421        try (InputStream resultStream = getEntryContentAsStream(url, entryName)) {
422            return IOUtils.toByteArray(resultStream);
423        }
424    }
425
426    public static List<String> getEntryNames(URL url) throws IOException {
427        return getEntryNames(url.openStream());
428    }
429
430    public static boolean hasEntry(URL url, String entryName) throws IOException {
431        return hasEntry(url.openStream(), entryName);
432    }
433
434    /**
435     * Checks if the content of the {@link InputStream} is a valid zip.
436     * The method does not close the stream.
437     * @param stream the {@link InputStream} to be validated
438     * @return true if the {@link InputStream} is a valid zip, false otherwise
439     */
440    public static boolean isValid(InputStream stream) {
441        try (ZipInputStream zip = new ZipInputStream(stream)) {
442            return zip.getNextEntry() != null;
443        } catch (IOException e) {
444            return false;
445        }
446    }
447
448}