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