/** * BetonQuest - advanced quests for Bukkit * Copyright (C) 2016 Jakub "Co0sh" Sapalski * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package pl.betoncraft.betonquest.utils; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.util.HashMap; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import org.bukkit.scheduler.BukkitRunnable; import org.json.JSONArray; import org.json.JSONObject; import pl.betoncraft.betonquest.BetonQuest; /** * Updates the plugin to the newest version and displays notifications about new * releases. * * @author Jakub Sapalski */ public class Updater { private static final String RELEASE_API_URL = "https://api.github.com/repos/Co0sh/BetonQuest/releases"; private static final String DEV_API_URL = "http://betonquest.betoncraft.pl/latest.txt"; private BetonQuest plugin; private String fileName; // name of the plugin file private boolean enabled = true; // configuration settings private boolean updateBugFixes; private boolean notifyDevBuild; private boolean notifyNewRelease; private boolean isOfficial; private boolean isDevBuild; private int devBuildNumber; // if these are null it means there is no update of this type private String releaseAddress; // this can be the same as bugfixAddress private String bugfixAddress; private String devBuildAddress; // version strings private String remoteRelease; private String remoteBugfix; private String remoteDevBuild; /** * Initializes the updater. Does not do anything if updater is disabled in * the config. * * @param file * the file to which the update will be saved */ public Updater(File file) { fileName = file.getName(); plugin = BetonQuest.getInstance(); // do nothing if the autoupdate is disabled if (!plugin.getConfig().getBoolean("update.enabled")) { enabled = false; return; } String version = plugin.getDescription().getVersion(); final Version locVer = new Version(); isDevBuild = version.contains("dev"); isOfficial = isDevBuild && version.contains("#"); // parse current version string to get all required information // correct version string is in one of those formats: // "1.2", "1.2.3", "1.2-dev", "1.2.3-dev", "1.2-dev#3" or "1.2.3-dev#4" try { // just keep parsing, if there are any errors it means that version // string is incorrect and should not be parsed String[] numbers = version.split("-")[0].split("\\."); locVer.coreVersion = Integer.parseInt(numbers[0]); locVer.majorVersion = Integer.parseInt(numbers[1]); if (numbers.length > 2) locVer.bugfixVersion = Integer.parseInt(numbers[2]); if (isDevBuild && isOfficial) { String raw = version.split("#")[1]; devBuildNumber = Integer.parseInt(raw); } } catch (Exception e) { Debug.broadcast("Could not parse version string: '" + version + "'. Autoupdater disabled."); return; } if (isDevBuild && !isOfficial) { // this is unofficial dev build, compiled by the developer Debug.broadcast("Detected unofficial development version. Autoupdater disabled."); return; } // read updater settings from configuration load(); Debug.broadcast("Autoupdater enabled!"); // check for updates new BukkitRunnable() { @Override public void run() { // handle checking dev build server try { int remoteDevBuildNumber = Integer.parseInt(readFromURL(DEV_API_URL)); remoteDevBuild = "#" + remoteDevBuildNumber; if (devBuildNumber < remoteDevBuildNumber) { devBuildAddress = "http://betonquest.betoncraft.pl/" + remoteDevBuildNumber + "/BetonQuest.jar"; } } catch (IOException | NumberFormatException e) { Debug.error("Could not get the latest dev build number"); return; } // handle checking github releases try { HashMap<Version, String> remoteVersions = new HashMap<>(); Version highestRelease = new Version(locVer.coreVersion, locVer.majorVersion, locVer.bugfixVersion); Version highestBugfix = new Version(locVer.coreVersion, locVer.majorVersion, locVer.bugfixVersion); JSONArray json = new JSONArray(readFromURL(RELEASE_API_URL)); for (int i = 0; i < json.length(); i++) { // read all info from each release and put it into the // hashmap JSONObject release = json.getJSONObject(i); Version version = parseTagVersion(release.getString("tag_name")); String url = release.getJSONArray("assets").getJSONObject(0).getString("browser_download_url"); remoteVersions.put(version, url); // check if this release is an update target if (version.coreVersion == locVer.coreVersion) { if (version.majorVersion >= highestRelease.majorVersion) { // if the version is higher than what we found // already, make sure bugfix version is also // updated if (version.majorVersion > highestRelease.majorVersion) { highestRelease.bugfixVersion = version.bugfixVersion; } highestRelease.majorVersion = version.majorVersion; if (version.bugfixVersion >= highestRelease.bugfixVersion) { highestRelease.bugfixVersion = version.bugfixVersion; } else { } } if (version.majorVersion == locVer.majorVersion) { if (version.bugfixVersion >= highestBugfix.bugfixVersion) { highestBugfix.bugfixVersion = version.bugfixVersion; } } } } // if the update targets are different that current version, // get their urls if (!isDevBuild && !highestBugfix.equals(locVer)) { for (Version v : remoteVersions.keySet()) { if (highestBugfix.equals(v)) { bugfixAddress = remoteVersions.get(v); break; } } remoteBugfix = highestBugfix.toString(); } if (!highestRelease.equals(locVer) || isDevBuild) { // if it's dev build, it will try to get the address of // the update. if there is no such // address in the map it means that there is no update // yet for (Version v : remoteVersions.keySet()) { if (highestRelease.equals(v)) { releaseAddress = remoteVersions.get(v); break; } } remoteRelease = highestRelease.toString(); } } catch (Exception e) { Debug.error("Could not get the latest release"); } // display notifications new BukkitRunnable() { @Override public void run() { if (updateBugFixes && bugfixAddress != null) { Debug.broadcast("Found bugfix version: " + remoteBugfix + ", it will be downloaded on next restart/reload."); } if (notifyNewRelease && releaseAddress != null && !remoteRelease.equals(remoteBugfix)) { Debug.broadcast( "Found new release: " + remoteRelease + ", use '/q update' to download it."); } if (notifyDevBuild && devBuildAddress != null) { Debug.broadcast("Found new development build: " + remoteDevBuild + ", use '/q update --dev' to download it."); } } }.runTask(plugin); } }.runTaskAsynchronously(plugin); } private void load() { updateBugFixes = plugin.getConfig().getBoolean("update.download_bugfixes"); notifyNewRelease = plugin.getConfig().getBoolean("update.notify_new_release"); // if it's an official dev build and there is no dev option, try to add // it if (isDevBuild && isOfficial) { if (plugin.getConfig().isSet("update.notify_dev_build")) { notifyDevBuild = plugin.getConfig().getBoolean("update.notify_dev_build"); } else { notifyDevBuild = true; plugin.getConfig().set("update.notify_dev_build", true); plugin.saveConfig(); } } else { if (plugin.getConfig().isSet("update.notify_dev_build")) { plugin.getConfig().set("update.notify_dev_build", null); plugin.saveConfig(); } notifyDevBuild = false; } } private String readFromURL(String url) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(new URL(url).openStream())); StringBuilder sb = new StringBuilder(); int cp; while ((cp = br.read()) != -1) { sb.append((char) cp); } return sb.toString(); } private Version parseTagVersion(String tag) throws Exception { tag = tag.replace("v", ""); String[] numbers = tag.split("\\."); Version version = new Version(); version.coreVersion = Integer.parseInt(numbers[0]); version.majorVersion = Integer.parseInt(numbers[1]); if (numbers.length > 2) version.bugfixVersion = Integer.parseInt(numbers[2]); return version; } private void downloadUpdate(final String address, final CommandSender sender) { try { URL remoteFile = new URL(address); ReadableByteChannel rbc = Channels.newChannel(remoteFile.openStream()); File folder = Bukkit.getUpdateFolderFile(); if (!folder.exists()) { folder.mkdirs(); } File file = new File(folder, fileName); if (!file.exists()) { file.createNewFile(); } FileOutputStream fos = new FileOutputStream(file); fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); fos.close(); if (sender != null) { sender.sendMessage("§2Download finished. Restart/reload the server to update the plugin."); } else { Debug.broadcast("Download finished."); } } catch (IOException e) { if (sender != null) { sender.sendMessage("§cCould not download the file. Try again or update manually."); } else { Debug.broadcast("Could not download the file."); } } } public boolean updateBugfixes() { if (enabled && updateBugFixes && bugfixAddress != null) { Debug.broadcast("Downloading bugfix version " + remoteBugfix); downloadUpdate(bugfixAddress, null); return true; } return false; } public boolean updateNewRelease(final CommandSender sender) { if (enabled && releaseAddress != null) { new BukkitRunnable() { @Override public void run() { downloadUpdate(releaseAddress, sender); bugfixAddress = null; // this will prevent bugfixes from // overwriting the release } }.runTaskAsynchronously(plugin); return true; } return false; } public boolean updateDevBuild(final CommandSender sender) { if (enabled && devBuildAddress != null) { new BukkitRunnable() { @Override public void run() { downloadUpdate(devBuildAddress, sender); bugfixAddress = null; } }.runTaskAsynchronously(plugin); return true; } return false; } public boolean isEnabled() { return enabled; } public String getRemoteRelease() { return remoteRelease; } public String getRemoteBugfix() { return remoteBugfix; } public String getRemoteDevBuild() { return remoteDevBuild; } public void reload() { enabled = plugin.getConfig().getBoolean("update.enabled"); load(); } private class Version { int coreVersion = 0; int majorVersion = 0; int bugfixVersion = 0; public Version() { } public Version(int a, int b, int c) { coreVersion = a; majorVersion = b; bugfixVersion = c; } @Override public String toString() { return "v" + coreVersion + "." + majorVersion + "." + bugfixVersion; } @Override public boolean equals(Object o) { if (o instanceof Version) { Version v = (Version) o; return coreVersion == v.coreVersion && majorVersion == v.majorVersion && bugfixVersion == v.bugfixVersion; } return false; } } }