View Javadoc
1   package com.atlassian.plugin.util.zip;
2   
3   import com.google.common.annotations.VisibleForTesting;
4   import org.apache.commons.io.FileUtils;
5   import org.apache.commons.io.FilenameUtils;
6   import org.apache.commons.io.IOUtils;
7   import org.apache.commons.lang3.StringUtils;
8   import org.slf4j.Logger;
9   import org.slf4j.LoggerFactory;
10  
11  import java.io.File;
12  import java.io.FileNotFoundException;
13  import java.io.FileOutputStream;
14  import java.io.IOException;
15  import java.io.InputStream;
16  import java.util.ArrayList;
17  import java.util.Arrays;
18  import java.util.Collections;
19  import java.util.HashMap;
20  import java.util.List;
21  import java.util.Map;
22  import java.util.zip.ZipEntry;
23  import java.util.zip.ZipInputStream;
24  
25  import static java.util.stream.Collectors.toMap;
26  
27  public abstract class AbstractUnzipper implements Unzipper {
28      protected static Logger log = LoggerFactory.getLogger(FileUnzipper.class);
29      protected File destDir;
30  
31      protected File saveEntry(InputStream is, ZipEntry entry) throws IOException {
32          final File file = new File(destDir, normaliseAndVerify(entry.getName()));
33  
34          if (entry.isDirectory()) {
35              file.mkdirs();
36          } else {
37              final File dir = new File(file.getParent());
38              dir.mkdirs();
39  
40              FileOutputStream fos = null;
41              try {
42                  fos = new FileOutputStream(file);
43                  IOUtils.copy(is, fos);
44                  fos.flush();
45              } catch (FileNotFoundException fnfe) {
46                  log.error("Error extracting a file to '{}{}{}'. This destination is invalid for writing an extracted " +
47                          "file stream to.", destDir, File.separator, entry.getName());
48                  return null;
49              } finally {
50                  IOUtils.closeQuietly(fos);
51              }
52          }
53          file.setLastModified(entry.getTime());
54  
55          return file;
56      }
57  
58      protected ZipEntry[] entries(ZipInputStream zis) throws IOException {
59          List<ZipEntry> entries = new ArrayList<>();
60          try {
61              ZipEntry zipEntry = zis.getNextEntry();
62              while (zipEntry != null) {
63                  entries.add(zipEntry);
64                  zis.closeEntry();
65                  zipEntry = zis.getNextEntry();
66              }
67          } finally {
68              IOUtils.closeQuietly(zis);
69          }
70  
71          return entries.toArray(new ZipEntry[0]);
72      }
73  
74      public void conditionalUnzip() throws IOException {
75          Map<String, Long> zipContentsAndLastModified = Arrays.stream(entries())
76                  .collect(toMap(ZipEntry::getName, ZipEntry::getTime));
77  
78          // If the jar contents of the directory does not match the contents of the zip
79          // The we will nuke the bundled plugins directory and re-extract.
80          Map<String, Long> targetDirContents = getContentsOfTargetDir(destDir);
81          if (!targetDirContents.equals(zipContentsAndLastModified)) {
82              // Note: clean, not delete, as destdir may be a symlink (PLUG-606).
83              if (destDir.exists()) {
84                  FileUtils.cleanDirectory(destDir);
85              }
86              unzip();
87          } else {
88              log.debug("Target directory contents match zip contents. Do nothing.");
89          }
90      }
91  
92      /**
93       * Normalises the supplied path name, by removing all intermediate {@code ..} constructs
94       * and ensures that resulting the path name is not a relative path.
95       * <p>
96       * Examples are:
97       * <ul>
98       * <li>Passing {@code foo} will return {@code foo}
99       * <li>Passing {@code dir/../foo} will return {@code foo}
100      * <li>Passing {@code dir/../../foo} will throw an IllegalArgumentException
101      * </ul>
102      *
103      * @param name the name to be normalised and verified
104      * @return the normalised name, that is not relative.
105      */
106     @VisibleForTesting
107     static String normaliseAndVerify(String name) {
108         final String normalised = FilenameUtils.normalizeNoEndSeparator(name);
109         if (StringUtils.isBlank(normalised)) {
110             throw new IllegalArgumentException("Path name " + name + " is illegal");
111         }
112         return normalised;
113     }
114 
115     private Map<String, Long> getContentsOfTargetDir(File dir) {
116         if (!dir.isDirectory()) {
117             return Collections.emptyMap();
118         }
119 
120         File[] files = dir.listFiles();
121         if (files == null) {
122             return Collections.emptyMap();
123         }
124 
125         Map<String, Long> targetDirContents = new HashMap<>();
126         for (File child : files) {
127             if (log.isDebugEnabled()) {
128                 log.debug("Examining entry in zip: " + child);
129             }
130             targetDirContents.put(child.getName(), child.lastModified());
131         }
132 
133         return targetDirContents;
134     }
135 }