View Javadoc

1   package com.atlassian.plugin.repositories;
2   
3   import com.atlassian.plugin.PluginArtifact;
4   import com.atlassian.plugin.RevertablePluginInstaller;
5   import com.atlassian.util.concurrent.CopyOnWriteMap;
6   import org.apache.commons.io.FileUtils;
7   import org.apache.commons.io.IOUtils;
8   import org.apache.commons.lang.Validate;
9   import org.slf4j.Logger;
10  import org.slf4j.LoggerFactory;
11  
12  import java.io.*;
13  import java.util.Map;
14  
15  import static com.google.common.base.Preconditions.checkNotNull;
16  
17  /**
18   * File-based implementation of a PluginInstaller which writes plugin artifact
19   * to a specified directory.  Handles reverting installs by keeping track of the first installation for a given
20   * instance, and restores it.  Installation of plugin artifacts with different names will overwrite an existing artifact
21   * of that same name, if it exists, with the only exception being the backup of the first overwritten artifact to
22   * support reverting.
23   *
24   * NOTE: This implementation has a limitation. The issue is that when installing a plugin we are only provided the plugin
25   * key and do not know the name of the artifact that provided the original plugin. So if someone installs a new version
26   * of an existing plugin in an artifact that has a different name we have no way of telling what artifact provided
27   * the original plugin and therefore which artifact to delete. This will result in two of the same plugins, but in
28   * different artifacts being left in the plugins directory. Hopefully the versions will differ so that the plugins
29   * framework can decide which plugin to enable. 
30   *
31   * @see RevertablePluginInstaller
32   */
33  public class FilePluginInstaller implements RevertablePluginInstaller
34  {
35      private File directory;
36      private static final Logger log = LoggerFactory.getLogger(FilePluginInstaller.class);
37  
38      private final Map<String, BackupRepresentation> installedPlugins = CopyOnWriteMap.<String, BackupRepresentation>builder().stableViews().newHashMap();
39  
40      public static final String ORIGINAL_PREFIX = ".original-";
41  
42      /**
43       * @param directory where plugin JARs will be installed.
44       */
45      public FilePluginInstaller(File directory)
46      {
47          Validate.isTrue(directory != null && directory.exists(), "The plugin installation directory must exist");
48          this.directory = directory;
49      }
50  
51      /**
52       * If there is an existing JAR with the same filename, it is replaced.
53       *
54       * @throws RuntimeException if there was an exception reading or writing files.
55       */
56      public void installPlugin(String key, PluginArtifact pluginArtifact)
57      {
58          checkNotNull(key, "The plugin key must be specified");
59          checkNotNull(pluginArtifact, "The plugin artifact must not be null");
60  
61          final File newPluginFile = new File(directory, pluginArtifact.getName());
62          try
63          {
64              backup(key, newPluginFile);
65              if (newPluginFile.exists())
66              {
67                  // would happen if the plugin was installed for a previous instance
68                  newPluginFile.delete();
69              }
70          }
71          catch (IOException e)
72          {
73              log.warn("Unable to backup old file", e);
74          }
75  
76          OutputStream os = null;
77          InputStream in = null;
78          try
79          {
80              os = new FileOutputStream(newPluginFile);
81              in = pluginArtifact.getInputStream();
82              IOUtils.copy(in, os);
83          }
84          catch (IOException e)
85          {
86              throw new RuntimeException("Could not install plugin: " + pluginArtifact, e);
87          }
88          finally
89          {
90              IOUtils.closeQuietly(in);
91              IOUtils.closeQuietly(os);
92          }
93      }
94  
95      /**
96       * Reverts an installed plugin.  Handles plugin file overwrites and different names over time.
97       *
98       * @param pluginKey The plugin key to revert
99       * @since 2.5.0
100      */
101     public void revertInstalledPlugin(String pluginKey)
102     {
103         BackupRepresentation backup = installedPlugins.get(pluginKey);
104         if (backup != null)
105         {
106             File currentFile = new File(backup.getBackupFile().getParent(), backup.getCurrentPluginFilename());
107             if (currentFile.exists())
108             {
109                 currentFile.delete();
110             }
111 
112             // We need to copy the original backed-up file back to the original filename if we overwrote the plugin
113             // when we first installed it.
114             if (backup.isUpgrade())
115             {
116                 try
117                 {
118                     FileUtils.moveFile(backup.getBackupFile(), new File(backup.getBackupFile().getParent(), backup.getOriginalPluginArtifactFilename()));
119                 }
120                 catch (IOException e)
121                 {
122                     log.warn("Unable to restore old plugin for " + pluginKey);
123                 }
124             }
125         }
126     }
127 
128     /**
129      * Deletes all backup files in the plugin directory
130      *
131      * @since 2.5.0
132      */
133     public void clearBackups()
134     {
135         for (File file : directory.listFiles(new BackupNameFilter()))
136         {
137             file.delete();
138         }
139         installedPlugins.clear();
140     }
141 
142     private void backup(String pluginKey, File currentPluginArtifact) throws IOException
143     {
144         BackupRepresentation orig = null;
145         // If this is the first time we have seen the pluginkey then we will create a backup representation that may
146         // refer to the original plugins file, if the artifact is named the same
147         if (!installedPlugins.containsKey(pluginKey))
148         {
149             orig = getBackupRepresentation(pluginKey, currentPluginArtifact);
150         }
151         // There is already a backup, we need to delete the intermediate file representation so that we do not
152         // leave a bunch of files laying around and update the backup with the new current plugin artifact name
153         else
154         {
155             final BackupRepresentation oldBackupFile = installedPlugins.get(pluginKey);
156             // Create a new backup representation that retains the reference to the original backup file but that changes
157             // the current plugin artifact name to be the new plugin file representation
158             orig = new BackupRepresentation(oldBackupFile, currentPluginArtifact.getName());
159 
160             // Delete the previous plugin representation
161             final File previousPluginFile = new File(oldBackupFile.getBackupFile().getParent(), oldBackupFile.getCurrentPluginFilename());
162             if (previousPluginFile.exists())
163             {
164                 previousPluginFile.delete();
165             }
166         }
167 
168         // Lets keep the backup representation for this plugin up-to-date
169         installedPlugins.put(pluginKey, orig);
170     }
171 
172     private BackupRepresentation getBackupRepresentation(final String pluginKey, final File currentPluginArtifact) throws IOException
173     {
174         // If there is already a file of the same name as our current plugin artifact then we should create a backup copy
175         // of the original file before we overwrite the old plugin file
176         if (currentPluginArtifact.exists())
177         {
178             File backupFile = new File(currentPluginArtifact.getParent(), ORIGINAL_PREFIX + currentPluginArtifact.getName());
179             if (backupFile.exists())
180             {
181                 throw new IOException("Existing backup found for plugin " + pluginKey + ".  Cannot install.");
182             }
183 
184             FileUtils.copyFile(currentPluginArtifact, backupFile);
185             return new BackupRepresentation(backupFile, currentPluginArtifact.getName());
186         }
187         // Since there was no original file then there is not really anything we need to store as a backup
188         else
189         {
190             return new BackupRepresentation(currentPluginArtifact, currentPluginArtifact.getName());
191         }
192     }
193 
194     private static class BackupNameFilter implements FilenameFilter
195     {
196         public boolean accept(File dir, String name)
197         {
198             return name.startsWith(ORIGINAL_PREFIX);
199         }
200     }
201 
202     private static class BackupRepresentation
203     {
204         private final File backupFile;
205         private final String originalPluginArtifactFilename;
206         private final String currentPluginFilename;
207         private final boolean isUpgrade;
208 
209         /**
210          * @param backupFile the file reference to the file that we should restore if we need to restore a backup
211          * @param originalPluginArtifactFilename the name of the original plugin artifact.
212          */
213         public BackupRepresentation(File backupFile, String originalPluginArtifactFilename)
214         {
215             this.backupFile = checkNotNull(backupFile, "backupFile");
216             this.originalPluginArtifactFilename = checkNotNull(originalPluginArtifactFilename, "originalPluginArtifactFilename");
217             this.isUpgrade = !backupFile.getName().equals(originalPluginArtifactFilename);
218             this.currentPluginFilename = originalPluginArtifactFilename;
219         }
220 
221         /**
222          * @param oldBackup defines the backup file, original plugin artifact name and if the backup is an "upgrade", non-null.
223          * @param currentPluginFilename the name of the current plugin artifact, not null.
224          */
225         public BackupRepresentation(BackupRepresentation oldBackup, String currentPluginFilename)
226         {
227             this.backupFile = checkNotNull(oldBackup, "oldBackup").backupFile;
228             this.originalPluginArtifactFilename = oldBackup.originalPluginArtifactFilename;
229             this.isUpgrade = oldBackup.isUpgrade;
230             this.currentPluginFilename = checkNotNull(currentPluginFilename, "currentPluginFilename");
231         }
232 
233         public File getBackupFile()
234         {
235             return backupFile;
236         }
237 
238         public String getOriginalPluginArtifactFilename()
239         {
240             return originalPluginArtifactFilename;
241         }
242 
243         public String getCurrentPluginFilename()
244         {
245             return currentPluginFilename;
246         }
247 
248         public boolean isUpgrade()
249         {
250             return isUpgrade;
251         }
252     }
253 }