001/* 002 * Syncany, www.syncany.org 003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com> 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU General Public License as published by 007 * the Free Software Foundation, either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU General Public License for more details. 014 * 015 * You should have received a copy of the GNU General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018package org.syncany.cli; 019 020import static java.util.Arrays.asList; 021 022import java.util.ArrayList; 023import java.util.List; 024 025import org.syncany.cli.util.CliTableUtil; 026import org.syncany.operations.OperationResult; 027import org.syncany.operations.daemon.messages.ConnectToHostExternalEvent; 028import org.syncany.operations.daemon.messages.PluginInstallExternalEvent; 029import org.syncany.operations.plugin.ExtendedPluginInfo; 030import org.syncany.operations.plugin.PluginInfo; 031import org.syncany.operations.plugin.PluginOperation; 032import org.syncany.operations.plugin.PluginOperationAction; 033import org.syncany.operations.plugin.PluginOperationOptions; 034import org.syncany.operations.plugin.PluginOperationOptions.PluginListMode; 035import org.syncany.operations.plugin.PluginOperationResult; 036import org.syncany.operations.plugin.PluginOperationResult.PluginResultCode; 037import org.syncany.util.StringUtil; 038 039import com.google.common.collect.Iterables; 040import com.google.common.eventbus.Subscribe; 041 042import joptsimple.OptionParser; 043import joptsimple.OptionSet; 044import joptsimple.OptionSpec; 045 046public class PluginCommand extends Command { 047 private boolean minimalOutput = false; 048 049 @Override 050 public CommandScope getRequiredCommandScope() { 051 return CommandScope.ANY; 052 } 053 054 @Override 055 public boolean canExecuteInDaemonScope() { 056 return false; // TODO [low] Doesn't have an impact if command scope is ANY 057 } 058 059 @Override 060 public int execute(String[] operationArgs) throws Exception { 061 PluginOperationOptions operationOptions = parseOptions(operationArgs); 062 PluginOperationResult operationResult = new PluginOperation(config, operationOptions).execute(); 063 064 printResults(operationResult); 065 066 return 0; 067 } 068 069 @Override 070 public PluginOperationOptions parseOptions(String[] operationArgs) throws Exception { 071 PluginOperationOptions operationOptions = new PluginOperationOptions(); 072 073 OptionParser parser = new OptionParser(); 074 OptionSpec<Void> optionLocal = parser.acceptsAll(asList("L", "local-only")); 075 OptionSpec<Void> optionRemote = parser.acceptsAll(asList("R", "remote-only")); 076 OptionSpec<Void> optionSnapshots = parser.acceptsAll(asList("s", "snapshot", "snapshots")); 077 OptionSpec<Void> optionMinimalOutput = parser.acceptsAll(asList("m", "minimal-output")); 078 OptionSpec<String> optionApiEndpoint = parser.acceptsAll(asList("a", "api-endpoint")).withRequiredArg(); 079 080 OptionSet options = parser.parse(operationArgs); 081 082 // Files 083 List<?> nonOptionArgs = options.nonOptionArguments(); 084 085 if (nonOptionArgs.size() == 0) { 086 throw new Exception("Invalid syntax, please specify an action (list, install, remove, update)."); 087 } 088 089 // <action> 090 String actionStr = nonOptionArgs.get(0).toString(); 091 PluginOperationAction action = parsePluginAction(actionStr); 092 093 operationOptions.setAction(action); 094 095 // --minimal-output 096 minimalOutput = options.has(optionMinimalOutput); 097 098 // --snapshots 099 operationOptions.setSnapshots(options.has(optionSnapshots)); 100 101 // --api-endpoint 102 if (options.has(optionApiEndpoint)) { 103 operationOptions.setApiEndpoint(options.valueOf(optionApiEndpoint)); 104 } 105 106 // install|remove <plugin-id> 107 if (action == PluginOperationAction.INSTALL || action == PluginOperationAction.REMOVE) { 108 if (nonOptionArgs.size() != 2) { 109 throw new Exception("Invalid syntax, please specify a plugin ID."); 110 } 111 112 // <plugin-id> 113 String pluginId = nonOptionArgs.get(1).toString(); 114 operationOptions.setPluginId(pluginId); 115 } 116 117 // --local-only, --remote-only 118 else if (action == PluginOperationAction.LIST) { 119 if (options.has(optionLocal)) { 120 operationOptions.setListMode(PluginListMode.LOCAL); 121 } 122 else if (options.has(optionRemote)) { 123 operationOptions.setListMode(PluginListMode.REMOTE); 124 } 125 else { 126 operationOptions.setListMode(PluginListMode.ALL); 127 } 128 129 // <plugin-id> (optional in 'list' or 'update') 130 if (nonOptionArgs.size() == 2) { 131 String pluginId = nonOptionArgs.get(1).toString(); 132 operationOptions.setPluginId(pluginId); 133 } 134 } 135 136 else if (action == PluginOperationAction.UPDATE && nonOptionArgs.size() == 2) { 137 String pluginId = nonOptionArgs.get(1).toString(); 138 operationOptions.setPluginId(pluginId); 139 } 140 141 return operationOptions; 142 } 143 144 private PluginOperationAction parsePluginAction(String actionStr) throws Exception { 145 try { 146 return PluginOperationAction.valueOf(actionStr.toUpperCase()); 147 } 148 catch (Exception e) { 149 throw new Exception("Invalid syntax, unknown action '" + actionStr + "'"); 150 } 151 } 152 153 @Override 154 public void printResults(OperationResult operationResult) { 155 PluginOperationResult concreteOperationResult = (PluginOperationResult) operationResult; 156 157 switch (concreteOperationResult.getAction()) { 158 case LIST: 159 printResultList(concreteOperationResult); 160 return; 161 162 case INSTALL: 163 printResultInstall(concreteOperationResult); 164 return; 165 166 case REMOVE: 167 printResultRemove(concreteOperationResult); 168 return; 169 170 case UPDATE: 171 printResultUpdate(concreteOperationResult); 172 return; 173 174 default: 175 out.println("Unknown action: " + concreteOperationResult.getAction()); 176 } 177 } 178 179 private void printResultList(PluginOperationResult operationResult) { 180 if (operationResult.getResultCode() == PluginResultCode.OK) { 181 List<String[]> tableValues = new ArrayList<String[]>(); 182 183 tableValues.add(new String[]{"Id", "Name", "Local Version", "Type", "Remote Version", "Updatable", "Provided By"}); 184 185 int outdatedCount = 0; 186 int updatableCount = 0; 187 int thirdPartyCount = 0; 188 189 for (ExtendedPluginInfo extPluginInfo : operationResult.getPluginList()) { 190 PluginInfo pluginInfo = (extPluginInfo.isInstalled()) ? extPluginInfo.getLocalPluginInfo() : extPluginInfo.getRemotePluginInfo(); 191 192 String localVersionStr = (extPluginInfo.isInstalled()) ? extPluginInfo.getLocalPluginInfo().getPluginVersion() : ""; 193 String installedStr = extPluginInfo.isInstalled() ? (extPluginInfo.canUninstall() ? "User" : "Global") : ""; 194 String remoteVersionStr = (extPluginInfo.isRemoteAvailable()) ? extPluginInfo.getRemotePluginInfo().getPluginVersion() : ""; 195 String thirdPartyStr = (pluginInfo.isPluginThirdParty()) ? "Third Party" : "Syncany Team"; 196 String updatableStr = ""; 197 198 if (extPluginInfo.isInstalled() && extPluginInfo.isOutdated()) { 199 if (extPluginInfo.canUninstall()) { 200 updatableStr = "Auto"; 201 updatableCount++; 202 } 203 else { 204 updatableStr = "Manual"; 205 } 206 207 outdatedCount++; 208 } 209 210 if (pluginInfo.isPluginThirdParty()) { 211 thirdPartyCount++; 212 } 213 214 tableValues.add(new String[]{pluginInfo.getPluginId(), pluginInfo.getPluginName(), localVersionStr, installedStr, remoteVersionStr, updatableStr, thirdPartyStr}); 215 } 216 217 CliTableUtil.printTable(out, tableValues, "No plugins found."); 218 219 if (outdatedCount > 0) { 220 String isAre = (outdatedCount == 1) ? "is" : "are"; 221 String pluginPlugins = (outdatedCount == 1) ? "plugin" : "plugins"; 222 223 out.printf("\nUpdates:\nThere %s %d outdated %s, %d of them %s automatically updatable.\n", isAre, outdatedCount, pluginPlugins, updatableCount, isAre); 224 } 225 226 if (thirdPartyCount > 0) { 227 String pluginPlugins = (thirdPartyCount == 1) ? "plugin" : "plugins"; 228 out.printf("\nThird party plugins:\nPlease note that the Syncany Team does not review or maintain the third-party %s\nlisted above. Please report issues to the corresponding plugin site.\n", pluginPlugins); 229 } 230 } 231 else { 232 out.printf("Listing plugins failed. No connection? Try -d to get more details.\n"); 233 out.println(); 234 } 235 } 236 237 private void printResultInstall(PluginOperationResult operationResult) { 238 // Print minimal result 239 if (minimalOutput) { 240 if (operationResult.getResultCode() == PluginResultCode.OK) { 241 out.println("OK"); 242 } 243 else { 244 out.println("NOK"); 245 } 246 } 247 // Print regular result 248 else { 249 if (operationResult.getResultCode() == PluginResultCode.OK) { 250 out.printf("Plugin successfully installed from %s\n", operationResult.getSourcePluginPath()); 251 out.printf("Install location: %s\n", operationResult.getTargetPluginPath()); 252 out.println(); 253 254 printPluginDetails(operationResult.getAffectedPluginInfo()); 255 printPluginConflictWarning(operationResult); 256 } 257 else { 258 out.println("Plugin installation failed. Try -d to get more details."); 259 out.println(); 260 } 261 } 262 } 263 264 private void printResultUpdate(final PluginOperationResult operationResult) { 265 // Print regular result 266 if (operationResult.getResultCode() == PluginResultCode.OK) { 267 if (operationResult.getUpdatedPluginIds().size() == 0) { 268 out.println("All plugins are up to date."); 269 } 270 else { 271 Iterables.removeAll(operationResult.getUpdatedPluginIds(), operationResult.getErroneousPluginIds()); 272 Iterables.removeAll(operationResult.getUpdatedPluginIds(), operationResult.getDelayedPluginIds()); 273 274 if (operationResult.getDelayedPluginIds().size() > 0) { 275 out.printf("Plugins to be updated: %s\n", StringUtil.join(operationResult.getDelayedPluginIds(), ", ")); 276 } 277 278 if (operationResult.getUpdatedPluginIds().size() > 0) { 279 out.printf("Plugins successfully updated: %s\n", StringUtil.join(operationResult.getUpdatedPluginIds(), ", ")); 280 } 281 282 if (operationResult.getErroneousPluginIds().size() > 0) { 283 out.printf("Failed to update %s. Try -d to get more details\n", StringUtil.join(operationResult.getErroneousPluginIds(), ", ")); 284 } 285 286 out.println(); 287 } 288 } 289 else { 290 out.println("Plugin update failed. Try -d to get more details."); 291 out.println(); 292 } 293 } 294 295 private void printPluginConflictWarning(PluginOperationResult operationResult) { 296 List<String> conflictingPluginIds = operationResult.getConflictingPluginIds(); 297 298 if (conflictingPluginIds != null && conflictingPluginIds.size() > 0) { 299 out.println("---------------------------------------------------------------------------"); 300 out.printf(" WARNING: The installed plugin '%s' conflicts with other installed:\n", operationResult.getAffectedPluginInfo().getPluginId()); 301 out.printf(" plugin(s): %s\n", StringUtil.join(conflictingPluginIds, ", ")); 302 out.println(); 303 out.println(" If you'd like to use these plugins in the daemon, it is VERY likely"); 304 out.println(" that parts of the application WILL CRASH. Data corruption might occur!"); 305 out.println(); 306 out.println(" Using the plugins outside of the daemon (sy <command> ...) might also"); 307 out.println(" be an issue. Details about this in issue #154."); 308 out.println("---------------------------------------------------------------------------"); 309 out.println(); 310 } 311 } 312 313 private void printResultRemove(PluginOperationResult operationResult) { 314 // Print minimal result 315 if (minimalOutput) { 316 if (operationResult.getResultCode() == PluginResultCode.OK) { 317 out.println("OK"); 318 } 319 else { 320 out.println("NOK"); 321 } 322 } 323 // Print regular result 324 else { 325 if (operationResult.getResultCode() == PluginResultCode.OK) { 326 out.printf("Plugin successfully removed.\n"); 327 out.printf("Original local was %s\n", operationResult.getSourcePluginPath()); 328 out.println(); 329 } 330 else { 331 out.println("Plugin removal failed."); 332 out.println(); 333 334 out.println("Note: Plugins shipped with the application or additional packages"); 335 out.println(" cannot be removed. These plugin are marked 'Global' in the list."); 336 out.println(); 337 } 338 } 339 } 340 341 private void printPluginDetails(PluginInfo pluginInfo) { 342 out.println("Plugin details:"); 343 out.println("- ID: " + pluginInfo.getPluginId()); 344 out.println("- Name: " + pluginInfo.getPluginName()); 345 out.println("- Version: " + pluginInfo.getPluginVersion()); 346 out.println(); 347 } 348 349 @Subscribe 350 public void onConnectToHostEventReceived(ConnectToHostExternalEvent event) { 351 if (!minimalOutput) { 352 out.printr("Connecting to " + event.getHost() + " ..."); 353 } 354 } 355 356 @Subscribe 357 public void onPluginInstallEventReceived(PluginInstallExternalEvent event) { 358 if (!minimalOutput) { 359 out.printr("Installing plugin from " + event.getSource() + " ..."); 360 } 361 } 362}