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