diff --git a/src/main/java/com/gmail/nossr50/mcMMO.java b/src/main/java/com/gmail/nossr50/mcMMO.java index 510c7f91a..3591cb88d 100644 --- a/src/main/java/com/gmail/nossr50/mcMMO.java +++ b/src/main/java/com/gmail/nossr50/mcMMO.java @@ -33,7 +33,6 @@ import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.metrics.MetricsManager; import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.runnables.SaveTimerTask; -import com.gmail.nossr50.runnables.UpdateCheckerTask; import com.gmail.nossr50.runnables.database.UserPurgeTask; import com.gmail.nossr50.runnables.party.PartyAutoKickTask; import com.gmail.nossr50.runnables.skills.BleedTimerTask; @@ -53,6 +52,10 @@ import com.gmail.nossr50.util.experience.FormulaManager; import com.gmail.nossr50.util.player.UserManager; import com.gmail.nossr50.util.spout.SpoutUtils; +import net.h31ix.updater.Updater; +import net.h31ix.updater.Updater.UpdateResult; +import net.h31ix.updater.Updater.UpdateType; + import net.shatteredlands.shatt.backup.ZipLibrary; public class mcMMO extends JavaPlugin { @@ -283,7 +286,17 @@ public class mcMMO extends JavaPlugin { return; } - getServer().getScheduler().runTaskAsynchronously(this, new UpdateCheckerTask()); + Updater updater = new Updater(this, "mcmmo", mcmmo, UpdateType.NO_DOWNLOAD, false); + + if (updater.getResult() != UpdateResult.UPDATE_AVAILABLE) { + return; + } + + if (updater.getLatestVersionString().contains("-beta") && !Config.getInstance().getPreferBeta()) { + return; + } + + updateCheckerCallback(true); } public void updateCheckerCallback(boolean updateAvailable) { diff --git a/src/main/java/com/gmail/nossr50/runnables/UpdateCheckerTask.java b/src/main/java/com/gmail/nossr50/runnables/UpdateCheckerTask.java deleted file mode 100644 index 20d88d498..000000000 --- a/src/main/java/com/gmail/nossr50/runnables/UpdateCheckerTask.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.gmail.nossr50.runnables; - -import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.util.UpdateChecker; - -/** - * Async task - */ -public class UpdateCheckerTask implements Runnable { - @Override - public void run() { - try { - mcMMO.p.updateCheckerCallback(UpdateChecker.updateAvailable()); - } - catch (Exception e) { - mcMMO.p.updateCheckerCallback(false); - } - } -} diff --git a/src/main/java/com/gmail/nossr50/util/UpdateChecker.java b/src/main/java/com/gmail/nossr50/util/UpdateChecker.java deleted file mode 100644 index 87f874dfa..000000000 --- a/src/main/java/com/gmail/nossr50/util/UpdateChecker.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.gmail.nossr50.util; - -import java.io.InputStreamReader; -import java.net.URL; -import java.net.UnknownHostException; - -import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.config.Config; - -import org.json.simple.JSONObject; -import org.json.simple.parser.JSONParser; -import org.json.simple.parser.ParseException; - -public class UpdateChecker { - private UpdateChecker() {} - - public static boolean updateAvailable() throws Exception { - String checkType = Config.getInstance().getPreferBeta() ? "latest" : "release"; - String version = mcMMO.p.getDescription().getVersion(); - InputStreamReader isr; - - try { - isr = new InputStreamReader(new URL("http://api.bukget.org/api2/bukkit/plugin/mcmmo/" + checkType).openStream()); - } - catch (UnknownHostException e) { - return false; - } - - try { - Object o = new JSONParser().parse(isr); - - if (!(o instanceof JSONObject)) { - return false; - } - - JSONObject versions = (JSONObject) ((JSONObject) o).get("versions"); - String newVersion = (String) versions.get("version"); - - String[] oldTokens = version.replaceAll("(?i)(-)(.+?)(-)", "-").split("[.]|-b"); - String[] newTokens = newVersion.replaceAll("(?i)(-)(.+?)(-)", "-").split("[.]|-b"); - - for (int i = 0; i < 4; i++) { - Integer newVer = Integer.parseInt(newTokens[i]); - Integer oldVer; - - try { - oldVer = Integer.parseInt(oldTokens[i]); - } - catch (NumberFormatException e) { - mcMMO.p.getLogger().warning("Could not get information about this mcMMO version; perhaps you are running a custom one?"); - return false; - } - - if (oldVer < newVer) { - return true; - } - } - - return false; - } - catch (ParseException e) { - return false; - } - finally { - isr.close(); - } - } -} diff --git a/src/main/java/net/h31ix/updater/Updater.java b/src/main/java/net/h31ix/updater/Updater.java new file mode 100644 index 000000000..5838da645 --- /dev/null +++ b/src/main/java/net/h31ix/updater/Updater.java @@ -0,0 +1,613 @@ +/* + * Updater for Bukkit. + * + * This class provides the means to safely and easily update a plugin, or check to see if it is updated using dev.bukkit.org + */ + +package net.h31ix.updater; + +import java.io.*; +import java.lang.IllegalThreadStateException; +import java.lang.Runnable; +import java.lang.Thread; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Enumeration; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.XMLEvent; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; + +/** + * Check dev.bukkit.org to find updates for a given plugin, and download the updates if needed. + *

+ * VERY, VERY IMPORTANT: Because there are no standards for adding auto-update toggles in your plugin's config, this system provides NO CHECK WITH YOUR CONFIG to make sure the user has allowed auto-updating. + *
+ * It is a BUKKIT POLICY that you include a boolean value in your config that prevents the auto-updater from running AT ALL. + *
+ * If you fail to include this option in your config, your plugin will be REJECTED when you attempt to submit it to dev.bukkit.org. + *

+ * An example of a good configuration option would be something similar to 'auto-update: true' - if this value is set to false you may NOT run the auto-updater. + *
+ * If you are unsure about these rules, please read the plugin submission guidelines: http://goo.gl/8iU5l + * + * @author H31IX + */ + +public class Updater +{ + private Plugin plugin; + private UpdateType type; + private String versionTitle; + private String versionLink; + private long totalSize; // Holds the total size of the file + //private double downloadedSize; TODO: Holds the number of bytes downloaded + private int sizeLine; // Used for detecting file size + private int multiplier; // Used for determining when to broadcast download updates + private boolean announce; // Whether to announce file downloads + private URL url; // Connecting to RSS + private File file; // The plugin's file + private Thread thread; // Updater thread + private static final String DBOUrl = "http://dev.bukkit.org/server-mods/"; // Slugs will be appended to this to get to the project's RSS feed + private String [] noUpdateTag = {}; // If the version number contains one of these, don't update. + private static final int BYTE_SIZE = 1024; // Used for downloading files + private String updateFolder = YamlConfiguration.loadConfiguration(new File("bukkit.yml")).getString("settings.update-folder"); // The folder that downloads will be placed in + private Updater.UpdateResult result = Updater.UpdateResult.SUCCESS; // Used for determining the outcome of the update process + // Strings for reading RSS + private static final String TITLE = "title"; + private static final String LINK = "link"; + private static final String ITEM = "item"; + + /** + * Gives the dev the result of the update process. Can be obtained by called getResult(). + */ + public enum UpdateResult + { + /** + * The updater found an update, and has readied it to be loaded the next time the server restarts/reloads. + */ + SUCCESS, + /** + * The updater did not find an update, and nothing was downloaded. + */ + NO_UPDATE, + /** + * The updater found an update, but was unable to download it. + */ + FAIL_DOWNLOAD, + /** + * For some reason, the updater was unable to contact dev.bukkit.org to download the file. + */ + FAIL_DBO, + /** + * When running the version check, the file on DBO did not contain the a version in the format 'vVersion' such as 'v1.0'. + */ + FAIL_NOVERSION, + /** + * The slug provided by the plugin running the updater was invalid and doesn't exist on DBO. + */ + FAIL_BADSLUG, + /** + * The updater found an update, but because of the UpdateType being set to NO_DOWNLOAD, it wasn't downloaded. + */ + UPDATE_AVAILABLE + } + + /** + * Allows the dev to specify the type of update that will be run. + */ + public enum UpdateType + { + /** + * Run a version check, and then if the file is out of date, download the newest version. + */ + DEFAULT, + /** + * Don't run a version check, just find the latest update and download it. + */ + NO_VERSION_CHECK, + /** + * Get information about the version and the download size, but don't actually download anything. + */ + NO_DOWNLOAD + } + + /** + * Initialize the updater + * + * @param plugin + * The plugin that is checking for an update. + * @param slug + * The dev.bukkit.org slug of the project (http://dev.bukkit.org/server-mods/SLUG_IS_HERE) + * @param file + * The file that the plugin is running from, get this by doing this.getFile() from within your main class. + * @param type + * Specify the type of update this will be. See {@link UpdateType} + * @param announce + * True if the program should announce the progress of new updates in console + */ + public Updater(Plugin plugin, String slug, File file, UpdateType type, boolean announce) + { + this.plugin = plugin; + this.type = type; + this.announce = announce; + this.file = file; + try + { + // Obtain the results of the project's file feed + url = new URL(DBOUrl + slug + "/files.rss"); + } + catch (MalformedURLException ex) + { + // Invalid slug + plugin.getLogger().warning("The author of this plugin (" + plugin.getDescription().getAuthors().get(0) + ") has misconfigured their Auto Update system"); + plugin.getLogger().warning("The project slug given ('" + slug + "') is invalid. Please nag the author about this."); + result = Updater.UpdateResult.FAIL_BADSLUG; // Bad slug! Bad! + } + thread = new Thread(new UpdateRunnable()); + thread.start(); + } + + /** + * Get the result of the update process. + */ + public Updater.UpdateResult getResult() + { + waitForThread(); + return result; + } + + /** + * Get the total bytes of the file (can only be used after running a version check or a normal run). + */ + public long getFileSize() + { + waitForThread(); + return totalSize; + } + + /** + * Get the version string latest file avaliable online. + */ + public String getLatestVersionString() + { + waitForThread(); + return versionTitle; + } + + /** + * As the result of Updater output depends on the thread's completion, it is necessary to wait for the thread to finish + * before alloowing anyone to check the result. + */ + public void waitForThread() { + if(thread.isAlive()) { + try { + thread.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + /** + * Save an update from dev.bukkit.org into the server's update folder. + */ + private void saveFile(File folder, String file, String u) + { + if(!folder.exists()) + { + folder.mkdir(); + } + BufferedInputStream in = null; + FileOutputStream fout = null; + try + { + // Download the file + URL url = new URL(u); + int fileLength = url.openConnection().getContentLength(); + in = new BufferedInputStream(url.openStream()); + fout = new FileOutputStream(folder.getAbsolutePath() + "/" + file); + + byte[] data = new byte[BYTE_SIZE]; + int count; + if(announce) plugin.getLogger().info("About to download a new update: " + versionTitle); + long downloaded = 0; + while ((count = in.read(data, 0, BYTE_SIZE)) != -1) + { + downloaded += count; + fout.write(data, 0, count); + int percent = (int) (downloaded * 100 / fileLength); + if(announce & (percent % 10 == 0)) + { + plugin.getLogger().info("Downloading update: " + percent + "% of " + fileLength + " bytes."); + } + } + //Just a quick check to make sure we didn't leave any files from last time... + for(File xFile : new File("plugins/" + updateFolder).listFiles()) + { + if(xFile.getName().endsWith(".zip")) + { + xFile.delete(); + } + } + // Check to see if it's a zip file, if it is, unzip it. + File dFile = new File(folder.getAbsolutePath() + "/" + file); + if(dFile.getName().endsWith(".zip")) + { + // Unzip + unzip(dFile.getCanonicalPath()); + } + if(announce) plugin.getLogger().info("Finished updating."); + } + catch (Exception ex) + { + plugin.getLogger().warning("The auto-updater tried to download a new update, but was unsuccessful."); + result = Updater.UpdateResult.FAIL_DOWNLOAD; + } + finally + { + try + { + if (in != null) + { + in.close(); + } + if (fout != null) + { + fout.close(); + } + } + catch (Exception ex) + { + } + } + } + + /** + * Part of Zip-File-Extractor, modified by H31IX for use with Bukkit + */ + private void unzip(String file) + { + try + { + File fSourceZip = new File(file); + String zipPath = file.substring(0, file.length()-4); + ZipFile zipFile = new ZipFile(fSourceZip); + Enumeration e = zipFile.entries(); + while(e.hasMoreElements()) + { + ZipEntry entry = (ZipEntry)e.nextElement(); + File destinationFilePath = new File(zipPath,entry.getName()); + destinationFilePath.getParentFile().mkdirs(); + if(entry.isDirectory()) + { + continue; + } + else + { + BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(entry)); + int b; + byte buffer[] = new byte[BYTE_SIZE]; + FileOutputStream fos = new FileOutputStream(destinationFilePath); + BufferedOutputStream bos = new BufferedOutputStream(fos, BYTE_SIZE); + while((b = bis.read(buffer, 0, BYTE_SIZE)) != -1) + { + bos.write(buffer, 0, b); + } + bos.flush(); + bos.close(); + bis.close(); + String name = destinationFilePath.getName(); + if(name.endsWith(".jar") && pluginFile(name)) + { + destinationFilePath.renameTo(new File("plugins/" + updateFolder + "/" + name)); + } + } + entry = null; + destinationFilePath = null; + } + e = null; + zipFile.close(); + zipFile = null; + // Move any plugin data folders that were included to the right place, Bukkit won't do this for us. + for(File dFile : new File(zipPath).listFiles()) + { + if(dFile.isDirectory()) + { + if(pluginFile(dFile.getName())) + { + File oFile = new File("plugins/" + dFile.getName()); // Get current dir + File [] contents = oFile.listFiles(); // List of existing files in the current dir + for(File cFile : dFile.listFiles()) // Loop through all the files in the new dir + { + boolean found = false; + for(File xFile : contents) // Loop through contents to see if it exists + { + if(xFile.getName().equals(cFile.getName())) + { + found = true; + break; + } + } + if(!found) + { + // Move the new file into the current dir + cFile.renameTo(new File(oFile.getCanonicalFile() + "/" + cFile.getName())); + } + else + { + // This file already exists, so we don't need it anymore. + cFile.delete(); + } + } + } + } + dFile.delete(); + } + new File(zipPath).delete(); + fSourceZip.delete(); + } + catch(IOException ex) + { + ex.printStackTrace(); + plugin.getLogger().warning("The auto-updater tried to unzip a new update file, but was unsuccessful."); + result = Updater.UpdateResult.FAIL_DOWNLOAD; + } + new File(file).delete(); + } + + /** + * Check if the name of a jar is one of the plugins currently installed, used for extracting the correct files out of a zip. + */ + public boolean pluginFile(String name) + { + for(File file : new File("plugins").listFiles()) + { + if(file.getName().equals(name)) + { + return true; + } + } + return false; + } + + /** + * Obtain the direct download file url from the file's page. + */ + private String getFile(String link) + { + String download = null; + try + { + // Open a connection to the page + URL url = new URL(link); + URLConnection urlConn = url.openConnection(); + InputStreamReader inStream = new InputStreamReader(urlConn.getInputStream()); + BufferedReader buff = new BufferedReader(inStream); + + int counter = 0; + String line; + while((line = buff.readLine()) != null) + { + counter++; + // Search for the download link + if(line.contains("

  • ")) + { + // Get the raw link + download = line.split("Download")[0]; + } + // Search for size + else if (line.contains("
    Size
    ")) + { + sizeLine = counter+1; + } + else if(counter == sizeLine) + { + String size = line.replaceAll("
    ", "").replaceAll("
    ", ""); + multiplier = size.contains("MiB") ? 1048576 : 1024; + size = size.replace(" KiB", "").replace(" MiB", ""); + totalSize = (long)(Double.parseDouble(size)*multiplier); + } + } + urlConn = null; + inStream = null; + buff.close(); + buff = null; + } + catch (Exception ex) + { + ex.printStackTrace(); + plugin.getLogger().warning("The auto-updater tried to contact dev.bukkit.org, but was unsuccessful."); + result = Updater.UpdateResult.FAIL_DBO; + return null; + } + return download; + } + + /** + * Check to see if the program should continue by evaluation whether the plugin is already updated, or shouldn't be updated + */ + private boolean versionCheck(String title) + { + if(type != UpdateType.NO_VERSION_CHECK) + { + String version = plugin.getDescription().getVersion(); + + String[] oldTokens = version.replaceAll("(?i)(-)(.+?)(-)", "-").split("[.]|-b"); + String[] newTokens = title.replaceAll("(?i)(-)(.+?)(-)", "-").split("[.]|-b"); + + for (int i = 0; i < 4; i++) { + Integer newVer = Integer.parseInt(newTokens[i]); + Integer oldVer; + + try { + oldVer = Integer.parseInt(oldTokens[i]); + } + catch (NumberFormatException e) { + plugin.getLogger().warning("Could not get information about this mcMMO version; perhaps you are running a custom one?"); + result = UpdateResult.FAIL_NOVERSION; + return false; + } + + if (oldVer < newVer) { + return true; + } + } + + result = Updater.UpdateResult.NO_UPDATE; + return false; + } + + return true; + } + + /** + * Used to calculate the version string as an Integer + */ + private Integer calVer(String s) throws NumberFormatException + { + if(s.contains(".")) + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i