View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.util;
23  
24  import org.slf4j.Logger;
25  import org.slf4j.LoggerFactory;
26  
27  import java.io.FileNotFoundException;
28  import java.io.IOException;
29  import java.util.Vector;
30  import java.util.zip.Deflater;
31  
32  import de.schlichtherle.io.ArchiveDetector;
33  import de.schlichtherle.io.ArchiveException;
34  import de.schlichtherle.io.ArchiveWarningException;
35  import de.schlichtherle.io.DefaultArchiveDetector;
36  import de.schlichtherle.io.File;
37  import de.schlichtherle.io.archive.zip.ZipDriver;
38  
39  /*
40   * WARNING:
41   * Some archivers, such as file-roller in Ubuntu, seem not to be able to uncompress zip archives containg files with
42   * special characters.
43   * The pure zip standard uses the CP437 encoding which CAN'T represent special characters, but applications have
44   * implemented their own methods to overcome this inconvenience. TrueZip also. However, it seems file-roller (and
45   * probably others) doesn't "understand" how to restore the original filenames and shows strange (and un-readable)
46   * characters in the unzipped filenames.
47   * However, the inverse process (unzipping zip archives made with file-roller and containing files with special
48   * characters) seems to work fine.
49   *
50   * N.B. By "special characters" I mean those not present in the original ASCII specification, such as accents,
51   * special letters, etc
52   *
53   * ruben.perez
54   */
55  
56  /**
57   * Provides static methods for compressing and extracting zip files using zip64 extensions when necessary.
58   */
59  public final class ZipUtil {
60  
61    private static final Logger logger = LoggerFactory.getLogger(ZipUtil.class);
62  
63    public static final int BEST_SPEED = Deflater.BEST_SPEED;
64    public static final int BEST_COMPRESSION = Deflater.BEST_COMPRESSION;
65    public static final int DEFAULT_COMPRESSION = Deflater.DEFAULT_COMPRESSION;
66    public static final int NO_COMPRESSION = Deflater.NO_COMPRESSION;
67  
68    /** Disable construction of this utility class */
69    private ZipUtil() {
70    }
71  
72    /**
73     * Utility class to ease the process of umounting a zip file
74     *
75     * @param zipFile
76     *          The file to umount
77     * @throws IOException
78     *           If some problem occurs on unmounting
79     */
80    private static void umount(File zipFile) throws IOException {
81      try {
82        File.umount(zipFile);
83      } catch (ArchiveWarningException awe) {
84        logger.warn("Umounting {} threw the following warning: {}", zipFile.getCanonicalPath(), awe.getMessage());
85      } catch (ArchiveException ae) {
86        logger.error("Unable to umount zip file: {}", zipFile.getCanonicalPath());
87        throw new IOException("Unable to umount zip file: " + zipFile.getCanonicalPath(), ae);
88      }
89    }
90  
91    /***********************************************************************************/
92    /* SERVICE CLASSES - The two following classes are the ones actually doing the job */
93    /***********************************************************************************/
94  
95    /**
96     * Compresses source files into a zip archive
97     *
98     * @param sourceFiles
99     *          A {@link java.io.File} array with the files to include in the root of the archive
100    * @param destination
101    *          A {@link java.io.File} descriptor to the location where the zip file should be created
102    * @param recursive
103    *          Indicate whether or not recursively zipping nested directories
104    * @param level
105    *          The zip algorithm compression level. Ranges between 0 (no compression) and 9 (max. compression)
106    * @return A {@link java.io.File} descriptor of the zip archive file
107    * @throws IOException
108    *           If the zip file can not be created, or the input files names can not be correctly parsed
109    */
110   public static java.io.File zip(java.io.File[] sourceFiles, java.io.File destination, boolean recursive, int level)
111           throws IOException {
112 
113     if (sourceFiles == null) {
114       logger.error("The array with files to zip cannot be null");
115       throw new IllegalArgumentException("The array with files to zip cannot be null");
116     }
117 
118     if (sourceFiles.length <= 0) {
119       logger.error("The array with files to zip cannot be empty");
120       throw new IllegalArgumentException("The array with files to zip cannot be empty");
121     }
122 
123     if (destination == null) {
124       logger.error("The destination file cannot be null");
125       throw new IllegalArgumentException("The destination file cannot be null");
126     }
127 
128     if (destination.exists()) {
129       logger.error("The destination file {} already exists", destination.getCanonicalPath());
130       throw new IllegalArgumentException("The destination file already exists");
131     }
132 
133     if (level < -1) {
134       logger.warn("Compression level cannot be less than 0 (or -1 for default)");
135       logger.warn("Reverting to default...");
136       level = -1;
137     } else if (level > 9) {
138       logger.warn("Compression level cannot be greater than 9");
139       logger.warn("Reverting to default...");
140       level = -1;
141     }
142 
143     // Limits the compression support to ZIP only and sets the compression level
144     ZipDriver zd = new ZipDriver(level);
145     ArchiveDetector ad = new DefaultArchiveDetector(ArchiveDetector.NULL, "zip", zd);
146     File zipFile;
147     try {
148       zipFile = new File(destination.getCanonicalFile(), ad);
149     } catch (IOException ioe) {
150       logger.error("Unable to create the zip file: {}", destination.getAbsolutePath());
151       throw new IOException("Unable to create the zip file: {}" + destination.getAbsolutePath(), ioe);
152     }
153 
154     try {
155       if (!zipFile.isArchive()) {
156         logger.error("The destination file does not represent a valid zip archive (.zip extension is required)");
157         zipFile.deleteAll();
158         throw new IllegalArgumentException(
159                 "The destination file does not represent a valid zip archive (.zip extension is required)");
160       }
161 
162       if (!zipFile.mkdirs()) {
163         throw new IOException("Couldn't create the destination file");
164       }
165 
166       for (java.io.File f : sourceFiles) {
167 
168         if (f == null) {
169           logger.error("Null inputfile in array");
170           zipFile.deleteAll();
171           throw new IllegalArgumentException("Null inputfile in array");
172         }
173 
174         logger.debug("Attempting to zip file {}...", f.getAbsolutePath());
175 
176         // TrueZip manual says that (archiveC|copy)All(From|To) methods work with either directories or regular files
177         // Therefore, one could do zipFile.archiveCopyAllFrom(f), where f is a regular file, and it would work. Well, it
178         // DOESN'T
179         // This is why we have to tell if a file is a regular file or a directory BEFORE copying it with the appropriate
180         // method
181         boolean success = false;
182         if (f.exists()) {
183           if (!f.isDirectory() || recursive) {
184             success = new File(zipFile, f.getName()).copyAllFrom(f);
185             if (success) {
186               logger.debug("File {} zipped successfuly", f.getAbsolutePath());
187             }
188             else {
189               logger.error("File {} not zipped", f.getAbsolutePath());
190               zipFile.deleteAll();
191               throw new IOException("Failed to zip one of the input files: " + f.getAbsolutePath());
192             }
193           }
194         } else {
195           logger.error("Input file {} doesn't exist", f.getAbsolutePath());
196           zipFile.deleteAll();
197           throw new FileNotFoundException("One of the input files does not exist: " + f.getAbsolutePath());
198         }
199       }
200     } catch (IOException e) {
201       throw e;
202     } finally {
203       umount(zipFile);
204     }
205 
206     return destination;
207   }
208 
209   /**
210    * Extracts a zip file to a directory.
211    *
212    * @param zipFile
213    *          A {@link String} with the path to the source zip archive
214    * @param destination
215    *          A {@link String} with the location where the zip archive will be extracted. If this destination directory
216    *          does not exist, it will be created.
217    * @throws IOException
218    *           if the zip file cannot be read, the destination directory cannot be created or the extraction is not
219    *           successful
220    */
221   public static void unzip(java.io.File zipFile, java.io.File destination) throws IOException {
222 
223     boolean success;
224 
225     if (zipFile == null) {
226       logger.error("The zip file cannot be null");
227       throw new IllegalArgumentException("The zip file must be set");
228     }
229 
230     if (!zipFile.exists()) {
231       logger.error("The zip file does not exist: {}", zipFile.getCanonicalPath());
232       throw new FileNotFoundException("The zip file does not exist: " + zipFile.getCanonicalPath());
233     }
234 
235     if (destination == null) {
236       logger.error("The destination file cannot be null");
237       throw new IllegalArgumentException("Destination file cannot be null");
238     }
239 
240     // FIXME Commented out for 3rd party compatibility. See comment in the zip method above -ruben.perez
241     // File f = new File(zipFile.getCanonicalFile(), new DefaultArchiveDetector(ArchiveDetector.NULL, "zip", new
242     // ZipDriver("utf-8")));
243     File f;
244     try {
245       f = new File(zipFile.getCanonicalFile());
246     } catch (IOException ioe) {
247       logger.error("Unable to create the zip file: {}", destination.getAbsolutePath());
248       throw new IOException("Unable to create the zip file: {}" + destination.getAbsolutePath(), ioe);
249     }
250 
251     try {
252       if (f.isArchive() && f.isDirectory()) {
253         if (destination.exists()) {
254           if (!destination.isDirectory()) {
255             logger.error("Destination file must be a directory");
256             throw new IllegalArgumentException("Destination file must be a directory");
257           }
258         }
259 
260         try {
261           destination.mkdirs();
262         } catch (SecurityException e) {
263           logger.error("Cannot create destination directory: {}", e.getMessage());
264           throw new IOException("Cannot create destination directory", e);
265         }
266 
267         success = f.copyAllTo(destination);
268 
269         if (success) {
270           logger.debug("File {} unzipped successfully", zipFile.getCanonicalPath());
271         } else {
272           logger.warn("File {} was not correctly unzipped", zipFile.getCanonicalPath());
273           throw new IOException("File " + zipFile.getCanonicalPath() + " was not correctly unzipped");
274         }
275       } else {
276         logger.error("The input file is not a valid zip file");
277         throw new IllegalArgumentException("The input file is not a valid zip file");
278       }
279     } catch (IOException e) {
280       throw (e);
281     } finally {
282       umount(f);
283     }
284 
285   }
286 
287   /************************************************************************************* */
288   /* "ALIASES" - For different types of input, but actually calling the previous methods */
289   /***************************************************************************************/
290 
291   /**
292    * Compresses source files into a zip archive
293    *
294    * @param sourceFiles
295    *          A {@link String} array with the file names to be included in the root of the archive
296    * @param destination
297    *          A {@link String} with the path name of the resulting zip file
298    * @param recursive
299    *          Indicate whether or not recursively zipping nested directories
300    * @param level
301    *          The zip algorithm compression level. Ranges between 0 (no compression) and 9 (max. compression)
302    * @return A {@link java.io.File} descriptor of the zip archive file
303    * @throws IOException
304    *           If the zip file can not be created, or the input files names can not be correctly parsed
305    */
306   public static java.io.File zip(String[] sourceFiles, String destination, boolean recursive, int level)
307           throws IOException {
308 
309     if (sourceFiles == null) {
310       logger.error("The input String array cannot be null");
311       throw new IllegalArgumentException("The input String array cannot be null");
312     }
313 
314     if (destination == null) {
315       logger.error("Destination file cannot be null");
316       throw new IllegalArgumentException("Destination file cannot be null");
317     }
318 
319     if ("".equals(destination)) {
320       logger.error("Destination file name must be set");
321       throw new IllegalArgumentException("Destination file name must be set");
322     }
323 
324     Vector<java.io.File> files = new Vector<java.io.File>();
325     for (String name : sourceFiles) {
326       if (name == null) {
327         logger.error("One of the input file names is null");
328         throw new IllegalArgumentException("One of the input file names is null");
329       } else if ("".equals(name)) {
330         logger.error("One of the input file names is blank");
331         throw new IllegalArgumentException("One of the input file names is blank");
332       }
333       files.add(new java.io.File(name));
334     }
335 
336     return zip(files.toArray(new java.io.File[files.size()]), new java.io.File(destination), recursive, level);
337 
338   }
339 
340   /**
341    * Compresses source files into a zip archive
342    *
343    * @param sourceFiles
344    *          A {@link String} array with the file names to be included in the root of the archive
345    * @param destination
346    *          A {@link java.io.File} with the path name of the resulting zip file
347    * @param recursive
348    *          Indicate whether or not recursively zipping nested directories
349    * @param level
350    *          The zip algorithm compression level. Ranges between 0 (no compression) and 9 (max. compression)
351    * @return A {@link java.io.File} descriptor of the zip archive file
352    * @throws IOException
353    *           If the zip file can not be created, or the input files names can not be correctly parsed
354    */
355   public static java.io.File zip(String[] sourceFiles, java.io.File destination, boolean recursive, int level)
356           throws IOException {
357 
358     if (sourceFiles == null) {
359       logger.error("The input String array cannot be null");
360       throw new IllegalArgumentException("The input String array cannot be null");
361     }
362 
363     Vector<java.io.File> files = new Vector<java.io.File>();
364     for (String name : sourceFiles) {
365       if (name == null) {
366         logger.error("One of the input file names is null");
367         throw new IllegalArgumentException("One of the input file names is null");
368       } else if ("".equals(name)) {
369         logger.error("One of the input file names is blank");
370         throw new IllegalArgumentException("One of the input file names is blank");
371       }
372       files.add(new java.io.File(name));
373     }
374 
375     return zip(files.toArray(new java.io.File[files.size()]), destination, recursive, level);
376 
377   }
378 
379   /**
380    * Compresses source files into a zip archive
381    *
382    * @param sourceFiles
383    *          A {@link java.io.File} array with the file names to be included in the root of the archive
384    * @param destination
385    *          A {@link String} with the path name of the resulting zip file
386    * @param recursive
387    *          Indicate whether or not recursively zipping nested directories
388    * @param level
389    *          The zip algorithm compression level. Ranges between 0 (no compression) and 9 (max. compression)
390    * @return A {@link java.io.File} descriptor of the zip archive file
391    * @throws IOException
392    *           If the zip file can not be created, or the input files names can not be correctly parsed
393    */
394   public static java.io.File zip(java.io.File[] sourceFiles, String destination, boolean recursive, int level)
395           throws IOException {
396 
397     if (destination == null) {
398       logger.error("Destination file cannot be null");
399       throw new IllegalArgumentException("Destination file cannot be null");
400     }
401 
402     if ("".equals(destination)) {
403       logger.error("Destination file name must be set");
404       throw new IllegalArgumentException("Destination file name must be set");
405     }
406 
407     return zip(sourceFiles, new java.io.File(destination), recursive, level);
408 
409   }
410 
411   /**
412    * Compresses source files into a zip archive (no recursive)
413    *
414    * @param sourceFiles
415    *          A {@link java.io.File} array with the file names to be included in the root of the archive
416    * @param destination
417    *          A {@link java.io.File} with the path name of the resulting zip file
418    * @param level
419    *          The zip algorithm compression level. Ranges between 0 (no compression) and 9 (max. compression)
420    * @return A {@link java.io.File} descriptor of the zip archive file
421    * @throws IOException
422    *           If the zip file can not be created, or the input files names can not be correctly parsed
423    */
424   public static java.io.File zip(java.io.File[] sourceFiles, java.io.File destination, int level) throws IOException {
425     return zip(sourceFiles, destination, false, level);
426   }
427 
428   /**
429    * Extracts a zip file to a directory.
430    *
431    * @param zipFile
432    *          A {@link String} with the path to the source zip archive
433    * @param destination
434    *          A {@link String} with the location where the zip archive will be extracted. If this destination directory
435    *          does not exist, it will be created.
436    * @throws IOException
437    *           if the zip file cannot be read, the destination directory cannot be created or the extraction is not
438    *           successful
439    */
440   public static void unzip(String zipFile, String destination) throws IOException {
441 
442     if (zipFile == null) {
443       logger.error("Input filename cannot be null");
444       throw new IllegalArgumentException("Input filename cannot be null");
445     }
446 
447     if ("".equals(zipFile)) {
448       logger.error("Input filename cannot be empty");
449       throw new IllegalArgumentException("Input filename cannot be empty");
450     }
451 
452     if (destination == null) {
453       logger.error("Output filename cannot be null");
454       throw new IllegalArgumentException("Output filename cannot be null");
455     }
456 
457     if ("".equals(destination)) {
458       logger.error("Output filename cannot be empty");
459       throw new IllegalArgumentException("Output filename cannot be empty");
460     }
461 
462     unzip(new java.io.File(zipFile), new java.io.File(destination));
463 
464   }
465 
466   /**
467    * Extracts a zip file to a directory.
468    *
469    * @param zipFile
470    *          A {@link java.io.File} with the path to the source zip archive
471    * @param destination
472    *          A {@link String} with the location where the zip archive will be extracted.
473    * @throws IOException
474    *           if the zip file cannot be read, the destination directory cannot be created or the extraction is not
475    *           successful
476    */
477   public static void unzip(java.io.File zipFile, String destination) throws IOException {
478 
479     if (destination == null) {
480       logger.error("Output filename cannot be null");
481       throw new IllegalArgumentException("Output filename cannot be null");
482     }
483 
484     if ("".equals(destination)) {
485       logger.error("Output filename cannot be empty");
486       throw new IllegalArgumentException("Output filename cannot be empty");
487     }
488 
489     unzip(zipFile, new java.io.File(destination));
490 
491   }
492 
493   /**
494    * Extracts a zip file to a directory.
495    *
496    * @param zipFile
497    *          A {@link String} with the path to the source zip archive
498    * @param destination
499    *          A {@link java.io.File} with the location where the zip archive will be extracted.
500    * @throws IOException
501    *           if the zip file cannot be read, the destination directory cannot be created or the extraction is not
502    *           successful
503    */
504   public static void unzip(String zipFile, java.io.File destination) throws IOException {
505 
506     if (zipFile == null) {
507       logger.error("Input filename cannot be null");
508       throw new IllegalArgumentException("Input filename cannot be null");
509     }
510 
511     if ("".equals(zipFile)) {
512       logger.error("Input filename cannot be empty");
513       throw new IllegalArgumentException("Input filename cannot be empty");
514     }
515 
516     unzip(new java.io.File(zipFile), destination);
517 
518   }
519 
520 }