View Javadoc
1   package com.atlassian.plugin.repositories;
2   
3   import com.atlassian.plugin.PluginArtifact;
4   import com.atlassian.plugin.RevertablePluginInstaller;
5   import io.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      private static final Logger log = LoggerFactory.getLogger(FilePluginInstaller.class);
40  
41      public static final String ORIGINAL_PREFIX = ".original-";
42  
43      private final File directory;
44      private final Map<String, BackupRepresentation> installedPlugins = CopyOnWriteMap.<String, BackupRepresentation>builder().stableViews().newHashMap();
45  
46      /**
47       * @param directory where plugin JARs will be installed.
48       */
49      public FilePluginInstaller(File directory) {
50          this.directory = checkNotNull(directory);
51          checkState(directory.exists(), "The plugin installation directory must exist, %s", directory.getAbsolutePath());
52      }
53  
54      /**
55       * If there is an existing JAR with the same filename, it is replaced.
56       *
57       * @throws RuntimeException if there was an exception reading or writing files.
58       */
59      public void installPlugin(String key, PluginArtifact pluginArtifact) {
60          checkNotNull(key, "The plugin key must be specified");
61          checkNotNull(pluginArtifact, "The plugin artifact must not be null");
62  
63          final File newPluginFile = new File(directory, pluginArtifact.getName());
64          try {
65              backup(key, newPluginFile);
66              if (newPluginFile.exists()) {
67                  // would happen if the plugin was installed for a previous instance
68                  newPluginFile.delete();
69              }
70          } catch (IOException e) {
71              log.warn("Unable to backup old file", e);
72          }
73  
74          OutputStream os = null;
75          InputStream in = null;
76          try {
77              os = new FileOutputStream(newPluginFile);
78              in = pluginArtifact.getInputStream();
79              IOUtils.copy(in, os);
80          } catch (IOException e) {
81              throw new RuntimeException("Could not install plugin: " + pluginArtifact, e);
82          } finally {
83              IOUtils.closeQuietly(in);
84              IOUtils.closeQuietly(os);
85          }
86      }
87  
88      /**
89       * Reverts an installed plugin. Handles plugin file overwrites and different names over time.
90       *
91       * @param pluginKey The plugin key to revert
92       * @since 2.5.0
93       */
94      public void revertInstalledPlugin(String pluginKey) {
95          BackupRepresentation backup = installedPlugins.get(pluginKey);
96          if (backup != null) {
97              File currentFile = new File(backup.getBackupFile().getParent(), backup.getCurrentPluginFilename());
98              if (currentFile.exists()) {
99                  currentFile.delete();
100             }
101 
102             // We need to copy the original backed-up file back to the original filename if we overwrote the plugin
103             // when we first installed it.
104             if (backup.isUpgrade()) {
105                 try {
106                     FileUtils.moveFile(backup.getBackupFile(), new File(backup.getBackupFile().getParent(), backup.getOriginalPluginArtifactFilename()));
107                 } catch (IOException e) {
108                     log.warn("Unable to restore old plugin for " + pluginKey);
109                 }
110             }
111         }
112     }
113 
114     /**
115      * Deletes all backup files in the plugin directory
116      *
117      * @since 2.5.0
118      */
119     public void clearBackups() {
120         File[] files = directory.listFiles(new BackupNameFilter());
121         if (files != null) {
122             for (File file : files) {
123                 file.delete();
124             }
125         }
126         installedPlugins.clear();
127     }
128 
129     private void backup(String pluginKey, File currentPluginArtifact) throws IOException {
130         BackupRepresentation orig = null;
131         // If this is the first time we have seen the pluginkey then we will create a backup representation that may
132         // refer to the original plugins file, if the artifact is named the same
133         if (!installedPlugins.containsKey(pluginKey)) {
134             orig = getBackupRepresentation(pluginKey, currentPluginArtifact);
135         }
136         // There is already a backup, we need to delete the intermediate file representation so that we do not
137         // leave a bunch of files laying around and update the backup with the new current plugin artifact name
138         else {
139             final BackupRepresentation oldBackupFile = installedPlugins.get(pluginKey);
140             // Create a new backup representation that retains the reference to the original backup file but that changes
141             // the current plugin artifact name to be the new plugin file representation
142             orig = new BackupRepresentation(oldBackupFile, currentPluginArtifact.getName());
143 
144             // Delete the previous plugin representation
145             final File previousPluginFile = new File(oldBackupFile.getBackupFile().getParent(), oldBackupFile.getCurrentPluginFilename());
146             if (previousPluginFile.exists()) {
147                 previousPluginFile.delete();
148             }
149         }
150 
151         // Lets keep the backup representation for this plugin up-to-date
152         installedPlugins.put(pluginKey, orig);
153     }
154 
155     private BackupRepresentation getBackupRepresentation(final String pluginKey, final File currentPluginArtifact) throws IOException {
156         // If there is already a file of the same name as our current plugin artifact then we should create a backup copy
157         // of the original file before we overwrite the old plugin file
158         if (currentPluginArtifact.exists()) {
159             File backupFile = new File(currentPluginArtifact.getParent(), ORIGINAL_PREFIX + currentPluginArtifact.getName());
160             if (backupFile.exists()) {
161                 throw new IOException("Existing backup found for plugin " + pluginKey + ". Cannot install.");
162             }
163 
164             FileUtils.copyFile(currentPluginArtifact, backupFile);
165             return new BackupRepresentation(backupFile, currentPluginArtifact.getName());
166         }
167         // Since there was no original file then there is not really anything we need to store as a backup
168         else {
169             return new BackupRepresentation(currentPluginArtifact, currentPluginArtifact.getName());
170         }
171     }
172 
173     private static class BackupNameFilter implements FilenameFilter {
174         public boolean accept(File dir, String name) {
175             return name.startsWith(ORIGINAL_PREFIX);
176         }
177     }
178 
179     private static class BackupRepresentation {
180         private final File backupFile;
181         private final String originalPluginArtifactFilename;
182         private final String currentPluginFilename;
183         private final boolean isUpgrade;
184 
185         /**
186          * @param backupFile                     the file reference to the file that we should restore if we need to restore a backup
187          * @param originalPluginArtifactFilename the name of the original plugin artifact.
188          */
189         public BackupRepresentation(File backupFile, String originalPluginArtifactFilename) {
190             this.backupFile = checkNotNull(backupFile, "backupFile");
191             this.originalPluginArtifactFilename = checkNotNull(originalPluginArtifactFilename, "originalPluginArtifactFilename");
192             this.isUpgrade = !backupFile.getName().equals(originalPluginArtifactFilename);
193             this.currentPluginFilename = originalPluginArtifactFilename;
194         }
195 
196         /**
197          * @param oldBackup             defines the backup file, original plugin artifact name and if the backup is an "upgrade", non-null.
198          * @param currentPluginFilename the name of the current plugin artifact, not null.
199          */
200         public BackupRepresentation(BackupRepresentation oldBackup, String currentPluginFilename) {
201             this.backupFile = checkNotNull(oldBackup, "oldBackup").backupFile;
202             this.originalPluginArtifactFilename = oldBackup.originalPluginArtifactFilename;
203             this.isUpgrade = oldBackup.isUpgrade;
204             this.currentPluginFilename = checkNotNull(currentPluginFilename, "currentPluginFilename");
205         }
206 
207         public File getBackupFile() {
208             return backupFile;
209         }
210 
211         public String getOriginalPluginArtifactFilename() {
212             return originalPluginArtifactFilename;
213         }
214 
215         public String getCurrentPluginFilename() {
216             return currentPluginFilename;
217         }
218 
219         public boolean isUpgrade() {
220             return isUpgrade;
221         }
222     }
223 }