1 /*
2 * #%L
3 * Nuiton Utils
4 * %%
5 * Copyright (C) 2004 - 2010 CodeLutin
6 * %%
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Lesser General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Lesser Public License for more details.
16 *
17 * You should have received a copy of the GNU General Lesser Public
18 * License along with this program. If not, see
19 * <http://www.gnu.org/licenses/lgpl-3.0.html>.
20 * #L%
21 */
22
23 package org.nuiton.util;
24
25 import org.apache.commons.io.FileUtils;
26 import org.apache.commons.logging.Log;
27 import org.apache.commons.logging.LogFactory;
28
29 import java.io.BufferedInputStream;
30 import java.io.BufferedOutputStream;
31 import java.io.File;
32 import java.io.FileFilter;
33 import java.io.FileInputStream;
34 import java.io.FileOutputStream;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.io.OutputStream;
38 import java.nio.charset.Charset;
39 import java.util.ArrayList;
40 import java.util.Collection;
41 import java.util.Enumeration;
42 import java.util.List;
43 import java.util.zip.ZipEntry;
44 import java.util.zip.ZipFile;
45 import java.util.zip.ZipInputStream;
46 import java.util.zip.ZipOutputStream;
47
48 /**
49 * Opérations sur des fichiers Zip. Compression et décompression avec ou
50 * sans filtres, scan des fichiers créés ou écrasés lors de la décompression...
51 *
52 * Created: 24 août 2006 10:13:35
53 *
54 * @author Benjamin Poussin - poussin@codelutin.com
55 *
56 */
57 public class ZipUtil {
58
59 /** Class logger. */
60 private static final Log log = LogFactory.getLog(ZipUtil.class);
61
62 /** Taille du buffer pour les lectures/écritures. */
63 private static final int BUFFER_SIZE = 8 * 1024;
64
65 /** Le séparateur de fichier en local. */
66 private static final String LOCAL_SEP = File.separator;
67
68 private static final String LOCAL_SEP_PATTERN = "\\".equals(LOCAL_SEP) ?
69 LOCAL_SEP + LOCAL_SEP : LOCAL_SEP;
70
71 /** Le séparateur zip. */
72 private static final String ZIP_SEP = "/";
73
74 private static final String ZIP_SEP_PATTERN = "/";
75
76 /** Accept all file pattern. */
77 protected static FileFilter ALL_FILE_FILTER = new FileFilter() {
78 public boolean accept(File pathname) {
79 return true;
80 }
81 };
82
83 /**
84 * Uncompress zipped file in targetDir.
85 *
86 * @param file the zip source file
87 * @param targetDir the destination directory
88 * @return return last entry name
89 * @throws IOException if any problem while uncompressing
90 */
91 public static String uncompress(File file, File targetDir) throws IOException {
92 String result;
93 result = uncompressAndRename(file, targetDir, null, null);
94 return result;
95 }
96
97 /**
98 * Uncompress zipped stream in targetDir.
99 *
100 *
101 * @param stream the zip source stream, stream is closed before return
102 * @param targetDir the destination directory
103 * @return return last entry name
104 * @throws IOException if any problem while uncompressing
105 * @since 2.6.6
106 */
107 public static String uncompress(InputStream stream, File targetDir) throws IOException {
108 String result = uncompressAndRename(stream, targetDir, null, null);
109 return result;
110 }
111
112 /**
113 * Uncompress zipped file in targetDir, and rename uncompressed file if
114 * necessary. If renameFrom or renameTo is null no renaming is done
115 *
116 * file in zip use / to separate directory and not begin with /
117 * each directory ended with /
118 *
119 * @param file the zip source file
120 * @param targetDir the destination directory
121 * @param renameFrom pattern to permit rename file before uncompress it
122 * @param renameTo new name for file if renameFrom is applicable to it
123 * you can use $1, $2, ... if you have '(' ')' in renameFrom
124 * @return return last entry name
125 * @throws IOException if any problem while uncompressing
126 */
127 public static String uncompressAndRename(File file,
128 File targetDir,
129 String renameFrom,
130 String renameTo) throws IOException {
131 return uncompressAndRename(new FileInputStream(file), targetDir, renameFrom, renameTo);
132 }
133
134
135 /**
136 * Uncompress zipped stream in targetDir, and rename uncompressed file if
137 * necessary. If renameFrom or renameTo is null no renaming is done
138 *
139 * file in zip use / to separate directory and not begin with /
140 * each directory ended with /
141 *
142 * @param stream the zip source stream, stream is closed before return
143 * @param targetDir the destination directory
144 * @param renameFrom pattern to permit rename file before uncompress it
145 * @param renameTo new name for file if renameFrom is applicable to it
146 * you can use $1, $2, ... if you have '(' ')' in renameFrom
147 * @return return last entry name
148 * @throws IOException if any problem while uncompressing
149 * @since 2.6.6
150 */
151 public static String uncompressAndRename(InputStream stream,
152 File targetDir,
153 String renameFrom,
154 String renameTo) throws IOException {
155 String result = "";
156 ZipInputStream in = new ZipInputStream(new BufferedInputStream(stream));
157 try {
158 ZipEntry entry;
159 while ((entry = in.getNextEntry()) != null) {
160 String name = entry.getName();
161 if (renameFrom != null && renameTo != null) {
162 name = name.replaceAll(renameFrom, renameTo);
163 if (log.isDebugEnabled()) {
164 log.debug("rename " + entry.getName() + " → " + name);
165 }
166 }
167 result = name;
168 File target = new File(targetDir, name);
169 if (entry.isDirectory()) {
170 FileUtil.createDirectoryIfNecessary(target);
171 } else {
172 FileUtil.createDirectoryIfNecessary(target.getParentFile());
173 OutputStream out = new BufferedOutputStream(new FileOutputStream(target));
174 try {
175 byte[] buffer = new byte[BUFFER_SIZE];
176 int len;
177 while ((len = in.read(buffer, 0, BUFFER_SIZE)) != -1) {
178 out.write(buffer, 0, len);
179 }
180 } finally {
181 out.close();
182 }
183 }
184 }
185 } finally {
186 in.close();
187 }
188 return result;
189 }
190
191 /**
192 * Compress 'includes' files in zipFile. If file in includes is directory
193 * only the directory is put in zipFile, not the file contained in directory
194 *
195 * @param zipFile the destination zip file
196 * @param root for all file in includes that is in this directory, then we
197 * remove this directory in zip entry name (aka -C for tar), can be null;
198 * @param includes the files to include in zip
199 * @throws IOException if any problem while compressing
200 */
201 public static void compressFiles(File zipFile,
202 File root,
203 Collection<File> includes) throws
204 IOException {
205 compressFiles(zipFile, root, includes, false);
206 }
207
208 /**
209 * Compress 'includes' files in zipFile. If file in includes is directory
210 * only the directory is put in zipFile, not the file contained in directory
211 *
212 * @param zipFile the destination zip file
213 * @param root for all file in includes that is in this directory, then we
214 * remove this directory in zip entry name (aka -C for tar), can be null;
215 * @param includes the files to include in zip
216 * @param createMD5 also create a MD5 file (zip name + .md5). MD5 file is created after zip.
217 * @throws IOException if any problem while compressing
218 */
219 public static void compressFiles(File zipFile,
220 File root,
221 Collection<File> includes,
222 boolean createMD5) throws IOException {
223 OutputStream oStream = new FileOutputStream(zipFile);
224
225 // if md5 creation flag
226 if (createMD5) {
227 oStream = new MD5OutputStream(oStream);
228 }
229 try {
230 ZipOutputStream zipOStream = new ZipOutputStream(oStream);
231
232 for (File file : includes) {
233 String entryName = toZipEntryName(root, file);
234
235 // Création d'une nouvelle entrée dans le zip
236 ZipEntry entry = new ZipEntry(entryName);
237 entry.setTime(file.lastModified());
238 zipOStream.putNextEntry(entry);
239
240 if (file.isFile() && file.canRead()) {
241 byte[] readBuffer = new byte[BUFFER_SIZE];
242 int bytesIn;
243 BufferedInputStream bis = new BufferedInputStream(
244 new FileInputStream(file), BUFFER_SIZE);
245 try {
246 while ((bytesIn =
247 bis.read(readBuffer, 0, BUFFER_SIZE)) != -1) {
248 zipOStream.write(readBuffer, 0, bytesIn);
249 }
250 } finally {
251 bis.close();
252 }
253 }
254 zipOStream.closeEntry();
255 }
256 zipOStream.close();
257
258 // if md5 creation flag
259 if (createMD5) {
260 String md5hash = StringUtil.asHex(((MD5OutputStream) oStream).hash());
261 File md5File = new File(zipFile.getAbsoluteFile() + ".md5");
262 FileUtils.write(md5File, md5hash, Charset.defaultCharset());
263 }
264 } finally {
265 oStream.close();
266 }
267 }
268
269 /**
270 * If fileOrDirectory is directory Compress recursively all file in this
271 * directory, else if is just file compress one file.
272 *
273 * Entry result name in zip start at fileOrDirectory.
274 * example: if we compress /etc/apache, entry will be apache/http.conf, ...
275 *
276 * @param zipFile the target zip file
277 * @param fileOrDirectory the file or directory to compress
278 * @throws IOException if any problem while compressing
279 */
280 public static void compress(File zipFile,
281 File fileOrDirectory) throws IOException {
282 compress(zipFile, fileOrDirectory, null, false);
283 }
284
285 /**
286 * If fileOrDirectory is directory Compress recursively all file in this
287 * directory, else if is just file compress one file.
288 *
289 * Entry result name in zip start at fileOrDirectory.
290 * example: if we compress /etc/apache, entry will be apache/http.conf, ...
291 *
292 * @param zipFile the target zip file
293 * @param fileOrDirectory the file or directory to compress
294 * @param filter used to accept file, if null, all file is accepted
295 * @throws IOException if any problem while compressing
296 */
297 public static void compress(File zipFile,
298 File fileOrDirectory,
299 FileFilter filter) throws IOException {
300 compress(zipFile, fileOrDirectory, filter, false);
301 }
302
303 /**
304 * If fileOrDirectory is directory Compress recursively all file in this
305 * directory, else if is just file compress one file.
306 *
307 * Entry result name in zip start at fileOrDirectory.
308 * example: if we compress /etc/apache, entry will be apache/http.conf, ...
309 *
310 * @param zipFile the target zip file
311 * @param fileOrDirectory the file or directory to compress
312 * @param filter used to accept file, if null, all file is accepted
313 * @param createMD5 also create a MD5 file (zip name + .md5). MD5 file is created after zip.
314 * @throws IOException if any problem while compressing
315 */
316 public static void compress(File zipFile,
317 File fileOrDirectory,
318 FileFilter filter,
319 boolean createMD5) throws IOException {
320 if (filter == null) {
321 filter = ALL_FILE_FILTER;
322 }
323 List<File> files = new ArrayList<File>();
324 if (fileOrDirectory.isDirectory()) {
325 files = FileUtil.getFilteredElements(fileOrDirectory, filter, true);
326 } else if (filter.accept(fileOrDirectory)) {
327 files.add(fileOrDirectory);
328 }
329
330 compressFiles(zipFile, fileOrDirectory.getParentFile(), files,
331 createMD5);
332 }
333
334 /**
335 * <li> supprime le root du fichier
336 * <li> Converti les '\' en '/' car les zip entry utilise des '/'
337 * <li> ajoute un '/' a la fin pour les repertoires
338 * <li> supprime le premier '/' si la chaine commence par un '/'
339 *
340 * @param root the root directory
341 * @param file the file to treate
342 * @return the zip entry name corresponding to the given {@code file}
343 * from {@code root} dir.
344 */
345 private static String toZipEntryName(File root, File file) {
346 String result = file.getPath();
347
348 if (root != null) {
349 String rootPath = root.getPath();
350 if (result.startsWith(rootPath)) {
351 result = result.substring(rootPath.length());
352 }
353 }
354
355 result = result.replace('\\', '/');
356 if (file.isDirectory()) {
357 result += '/';
358 }
359 while (result.startsWith("/")) {
360 result = result.substring(1);
361 }
362 return result;
363 }
364
365 /**
366 * Scan a zipFile, and fill two lists of relative paths corresponding of
367 * zip entries.
368 * First list contains all entries to be added while a uncompress operation
369 * on the destination directory {@code targetDir}.
370 * Second list contains all entries to be overwritten while a uncompress
371 * operation on the destination directory {@code targetDir}.
372 *
373 * If {@code targetDir} is {@code null} we don't fill {@code existingFiles} list.
374 *
375 * @param zipFile location of the zip to scanZip
376 * @param targetDir location of destination for a uncompress operation.
377 * If {@code null} we don't test to
378 * find overwritten files.
379 * @param newFiles list of files to be added while a uncompress
380 * @param existingFiles list of files to be overwritten while a uncompress
381 * if the {@code targetDir},
382 * (only use if {@code targetDir} is not
383 * {@code null})
384 * @param excludeFilter used to exclude some files
385 * @param renameFrom {@link #uncompressAndRename(File, File, String, String)}
386 * @param renameTo {@link #uncompressAndRename(File, File, String, String)}
387 * @throws IOException if any exception while dealing with zipfile
388 */
389 public static void scan(File zipFile,
390 File targetDir,
391 List<String> newFiles,
392 List<String> existingFiles,
393 FileFilter excludeFilter,
394 String renameFrom,
395 String renameTo) throws IOException {
396 ZipFile zip = null;
397 try {
398 zip = new ZipFile(zipFile);
399 boolean findExisting = targetDir != null && targetDir.exists();
400 boolean filter = findExisting && excludeFilter != null;
401 boolean rename = renameFrom != null && renameTo != null;
402 Enumeration<? extends ZipEntry> entries = zip.entries();
403 while (entries.hasMoreElements()) {
404 String entryName = entries.nextElement().getName();
405 if (rename) {
406 entryName = entryName.replaceAll(renameFrom, renameTo);
407 }
408 String name = convertToLocalEntryName(entryName);
409 if (findExisting || filter) {
410 File file = new File(targetDir, name);
411 if (filter && excludeFilter.accept(file)) continue;
412 if (file.exists()) {
413 existingFiles.add(name);
414 continue;
415 }
416 }
417 newFiles.add(name);
418 }
419 } finally {
420 if (zip != null) {
421 zip.close();
422 }
423 }
424 }
425
426 @SuppressWarnings({"unchecked"})
427 public static List<String>[] scanAndExplodeZip(File source,
428 File root,
429 FileFilter excludeFilter)
430 throws IOException {
431
432 List<String> overwrittenFiles = new ArrayList<String>();
433 List<String> newFiles = new ArrayList<String>();
434
435 // obtain list of relative paths (to add or overwrite)
436 scan(source, root, newFiles, overwrittenFiles, excludeFilter, null,
437 null);
438
439 return new List[]{newFiles, overwrittenFiles};
440 }
441
442 /**
443 * uncompress zipped file in targetDir.
444 *
445 * If {@code toTreate} if not null nor empty, we use it to filter
446 * entries to uncompress : it contains a list of relative local path of
447 * files to uncompress.
448 * Otherwise just delegate to {@link ZipUtil#uncompress(File, File)}.
449 *
450 * @param file location of zip file
451 * @param targetDir destination directory
452 * @param toTreate list of relative local path of entries to treate
453 * @param renameFrom {@link #uncompressAndRename(File, File, String, String)}
454 * @param renameTo {@link #uncompressAndRename(File, File, String, String)}
455 * @return return last entry name
456 * @throws IOException if nay exception while operation
457 */
458 public static String uncompress(File file,
459 File targetDir,
460 List<String> toTreate,
461 String renameFrom,
462 String renameTo) throws IOException {
463 if (toTreate == null || toTreate.isEmpty()) {
464 return uncompressAndRename(file, targetDir, renameFrom, renameTo);
465 }
466
467 boolean rename = renameFrom != null && renameTo != null;
468
469 String result = "";
470 ZipEntry entry;
471 ZipInputStream in = new ZipInputStream(new FileInputStream(file));
472 try {
473 while ((entry = in.getNextEntry()) != null) {
474 String name = entry.getName();
475 if (rename) {
476 result = convertToLocalEntryName(name.replaceAll(renameFrom,
477 renameTo));
478 } else {
479 result = convertToLocalEntryName(name);
480 }
481
482 if (log.isDebugEnabled()) {
483 log.debug("open [" + name + "] : " + result);
484 }
485 if (!toTreate.contains(result)) {
486 continue;
487 }
488
489 if (log.isDebugEnabled()) {
490 log.debug("copy [" + name + "] : " + result);
491 }
492 File target = new File(targetDir, result);
493 if (entry.isDirectory()) {
494 FileUtil.createDirectoryIfNecessary(target);
495 } else {
496 FileUtil.createDirectoryIfNecessary(target.getParentFile());
497 OutputStream out = new BufferedOutputStream(
498 new FileOutputStream(target));
499 try {
500 byte[] buffer = new byte[BUFFER_SIZE];
501 int len;
502 while ((len = in.read(buffer, 0, BUFFER_SIZE)) != -1) {
503 out.write(buffer, 0, len);
504 }
505 } finally {
506 out.close();
507 }
508 }
509 }
510 } finally {
511 in.close();
512 }
513 return result;
514 }
515
516 /**
517 * Unzip compressed archive and keep non excluded patterns.
518 *
519 * @param file archive file
520 * @param targetDir destination file
521 * @param excludes excludes pattern (pattern must match complete entry name including root folder)
522 * @throws IOException FIXME
523 */
524 public static void uncompressFiltred(File file,
525 File targetDir,
526 String... excludes) throws IOException {
527
528 ZipFile zipFile = new ZipFile(file);
529
530 Enumeration<? extends ZipEntry> entries = zipFile.entries();
531
532 while (entries.hasMoreElements()) {
533 ZipEntry entry = entries.nextElement();
534
535 String name = entry.getName();
536 // add continue to break loop
537 boolean excludeEntry = false;
538 if (excludes != null) {
539 for (String exclude : excludes) {
540 if (name.matches(exclude)) {
541 excludeEntry = true;
542 }
543 }
544 }
545
546 if (!excludeEntry) {
547 File target = new File(targetDir, name);
548 if (entry.isDirectory()) {
549 FileUtil.createDirectoryIfNecessary(target);
550 } else {
551 // get inputstream only here
552 FileUtil.createDirectoryIfNecessary(target.getParentFile());
553 InputStream in = zipFile.getInputStream(entry);
554 try {
555 OutputStream out = new BufferedOutputStream(
556 new FileOutputStream(target));
557 try {
558 byte[] buffer = new byte[8 * 1024];
559 int len;
560
561 while ((len = in.read(buffer, 0, 8 * 1024)) != -1) {
562 out.write(buffer, 0, len);
563 }
564 } finally {
565 out.close();
566 }
567 } finally {
568 in.close();
569 }
570 }
571 }
572 }
573 }
574
575 /**
576 * Tests if the given file is a zip file.
577 *
578 * @param file the file to test
579 * @return {@code true} if the file is a valid zip file,
580 * {@code false} otherwise.
581 * @since 2.4.9
582 */
583 public static boolean isZipFile(File file) {
584
585 boolean result = false;
586 try {
587 ZipFile zipFile = new ZipFile(file);
588 zipFile.close();
589 result = true;
590 } catch (IOException e) {
591 // silent test
592 }
593 return result;
594 }
595
596 protected static String convertToLocalEntryName(String txt) {
597 String s = txt.replaceAll(ZIP_SEP_PATTERN, LOCAL_SEP_PATTERN);
598 if (s.endsWith(ZIP_SEP)) {
599 s = s.substring(0, s.length() - 1);
600 }
601 return s;
602 }
603 }