View Javadoc

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