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.operations.update; 019 020import java.io.BufferedReader; 021import java.io.InputStreamReader; 022import java.net.URL; 023import java.net.URLConnection; 024import java.util.ArrayList; 025import java.util.logging.Level; 026import java.util.logging.Logger; 027 028import org.simpleframework.xml.core.Persister; 029import org.syncany.Client; 030import org.syncany.config.Config; 031import org.syncany.config.LocalEventBus; 032import org.syncany.operations.Operation; 033import org.syncany.operations.daemon.messages.ConnectToHostExternalEvent; 034import org.syncany.operations.update.UpdateOperationResult.UpdateResultCode; 035import org.syncany.plugins.Plugins; 036import org.syncany.util.EnvironmentUtil; 037 038import com.github.zafarkhaja.semver.Version; 039 040/** 041 * This operation manages updates of the application. It currently only 042 * performs update checks, but will likely be extended to automatically 043 * update the application. The following actions exist: 044 * 045 * <p>The 'check' action checks if a new application version is available. 046 * It queries the Syncany API and outputs whether the local copy of the 047 * application is up-to-date. If it is not, it outputs the newest version 048 * and a download URL. 049 * 050 * @see <a href="https://github.com/syncany/syncany-website">Syncany Website/API</a> 051 * @author Philipp C. Heckel (philipp.heckel@gmail.com) 052 */ 053public class UpdateOperation extends Operation { 054 private static final Logger logger = Logger.getLogger(UpdateOperation.class.getSimpleName()); 055 056 private static final String GUI_PLUGIN_ID = "gui"; 057 private static final String API_DEFAULT_ENDPOINT_URL = "https://api.syncany.org/v3"; 058 private static final String API_APP_LIST_REQUEST_FORMAT = "%s/app?dist=%s&type=%s&snapshots=%s&os=%s&arch=%s"; 059 060 private UpdateOperationOptions options; 061 private UpdateOperationResult result; 062 063 private LocalEventBus eventBus; 064 065 public UpdateOperation(Config config, UpdateOperationOptions options) { 066 super(config); 067 068 this.options = options; 069 this.result = new UpdateOperationResult(); 070 071 this.eventBus = LocalEventBus.getInstance(); 072 } 073 074 @Override 075 public UpdateOperationResult execute() throws Exception { 076 result.setAction(options.getAction()); 077 078 switch (options.getAction()) { 079 case CHECK: 080 return executeCheck(); 081 082 default: 083 throw new Exception("Unknown action: " + options.getAction()); 084 } 085 } 086 087 private UpdateOperationResult executeCheck() throws Exception { 088 Version localAppVersion = Version.valueOf(Client.getApplicationVersion()); 089 090 String appInfoResponseStr = getAppInfoResponseStr(); 091 AppInfoResponse appInfoResponse = new Persister().read(AppInfoResponse.class, appInfoResponseStr); 092 093 ArrayList<AppInfo> appInfoList = appInfoResponse.getAppInfoList(); 094 095 if (appInfoList.size() > 0) { 096 AppInfo remoteAppInfo = appInfoList.get(0); 097 Version remoteAppVersion = Version.valueOf(remoteAppInfo.getAppVersion()); 098 099 boolean newVersionAvailable = remoteAppVersion.greaterThan(localAppVersion); 100 101 result.setResultCode(UpdateResultCode.OK); 102 result.setAppInfo(remoteAppInfo); 103 result.setNewVersionAvailable(newVersionAvailable); 104 105 return result; 106 } 107 else { 108 result.setResultCode(UpdateResultCode.NOK); 109 return result; 110 } 111 } 112 113 private String getAppInfoResponseStr() throws Exception { 114 boolean hasGuiPlugin = Plugins.get(GUI_PLUGIN_ID) != null; 115 116 String typeStr = determineType(hasGuiPlugin); 117 String distStr = determineDist(hasGuiPlugin, typeStr); 118 String snapshotsEnabled = (options.isSnapshots()) ? "true" : "false"; 119 String osStr = EnvironmentUtil.getOperatingSystemDescription(); 120 String archStr = EnvironmentUtil.getArchDescription(); 121 122 String apiEndpointUrl = (options.getApiEndpoint() != null) ? options.getApiEndpoint() : API_DEFAULT_ENDPOINT_URL; 123 URL appListUrl = new URL(String.format(API_APP_LIST_REQUEST_FORMAT, apiEndpointUrl, distStr, typeStr, snapshotsEnabled, 124 osStr, archStr)); 125 126 logger.log(Level.INFO, "Querying " + appListUrl + " ..."); 127 eventBus.post(new ConnectToHostExternalEvent(appListUrl.getHost())); 128 129 URLConnection urlConnection = appListUrl.openConnection(); 130 urlConnection.setConnectTimeout(2000); 131 urlConnection.setReadTimeout(2000); 132 133 BufferedReader urlStreamReader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); 134 StringBuilder responseStringBuilder = new StringBuilder(); 135 136 String line; 137 while ((line = urlStreamReader.readLine()) != null) { 138 responseStringBuilder.append(line); 139 } 140 141 String responseStr = responseStringBuilder.toString(); 142 logger.log(Level.INFO, "Response from api.syncany.org: " + responseStr); 143 144 return responseStr; 145 } 146 147 private String determineType(boolean hasGuiPlugin) { 148 if (EnvironmentUtil.isWindows()) { 149 return "exe"; 150 } 151 else if (EnvironmentUtil.isMacOSX()) { 152 return (hasGuiPlugin) ? "app.zip" : "zip"; 153 } 154 else if (EnvironmentUtil.isUnixLikeOperatingSystem()) { 155 return (EnvironmentUtil.isDebianBased()) ? "deb" : "tar.gz"; 156 } 157 158 return "zip"; 159 } 160 161 private String determineDist(boolean hasGuiPlugin, String type) { 162 boolean packageWithGuiExists = type.equals("exe") || type.equals("app.zip"); 163 return (hasGuiPlugin && packageWithGuiExists) ? "gui" : "cli"; 164 } 165}