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