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