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}