diff --git a/pom.xml b/pom.xml
index fe85372..e5464d3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
4.0.0
mc.alk
BattlePluginUpdater
- 2.0
+ 2.1
jar
BattlePluginUpdater
https://github.com/BattlePlugins/PluginUpdater
@@ -50,7 +50,7 @@
org.bukkit
bukkit
- 1.7.2-R0.1-SNAPSHOT
+ 1.9-R0.1-SNAPSHOT
provided
true
diff --git a/src/main/java/mc/alk/battlepluginupdater/PluginUpdater.java b/src/main/java/mc/alk/battlepluginupdater/PluginUpdater.java
index 9c51057..6ae3096 100644
--- a/src/main/java/mc/alk/battlepluginupdater/PluginUpdater.java
+++ b/src/main/java/mc/alk/battlepluginupdater/PluginUpdater.java
@@ -2,20 +2,21 @@
import java.io.File;
import java.util.HashSet;
+import java.util.logging.Level;
import mc.euro.version.Version;
import mc.euro.version.VersionFactory;
import org.bukkit.Bukkit;
+import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.plugin.Plugin;
/**
- * Originally a class that downloaded and updated bukkit plugins. Since the new
- * bukkit rules this class has been converted into a wrapper around Gravity's
- * Updater class
+ * @deprecated this class is known for causing issues with plugin downloads.
+ * Instead, use {@link mc.alk.battlepluginupdater.SpigotUpdater}
*/
public class PluginUpdater {
@@ -104,7 +105,10 @@ public static AnnounceUpdateOption fromString(String name) {
* @param updateOption when should we update the plugin
* @param announceOption who should recieve announcements about a newer
* version
+ *
+ * @deprecated use
*/
+ @Deprecated
public static void update(final Plugin plugin, final int bukkitId, final File file,
final UpdateOption updateOption,
final AnnounceUpdateOption announceOption) {
diff --git a/src/main/java/mc/alk/battlepluginupdater/SpigotUpdater.java b/src/main/java/mc/alk/battlepluginupdater/SpigotUpdater.java
new file mode 100644
index 0000000..55a8f0f
--- /dev/null
+++ b/src/main/java/mc/alk/battlepluginupdater/SpigotUpdater.java
@@ -0,0 +1,254 @@
+package mc.alk.battlepluginupdater;
+
+import mc.alk.battlepluginupdater.checker.UpdateChecker;
+
+import org.bukkit.ChatColor;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.plugin.Plugin;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.logging.Level;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+public class SpigotUpdater {
+
+ private Plugin plugin;
+ private int pluginId;
+ private String downloadLink;
+
+ private String updateFolder;
+
+ public SpigotUpdater(Plugin plugin, int pluginId, String downloadLink) {
+ this.plugin = plugin;
+ this.pluginId = pluginId;
+ this.downloadLink = downloadLink;
+
+ this.updateFolder = plugin.getServer().getUpdateFolder();
+ }
+
+ public void update() {
+ File pluginFile = plugin.getDataFolder().getParentFile();
+ File updaterFile = new File(pluginFile, "Updater");
+ File updaterConfigFile = new File(updaterFile, "config.yml");
+
+ YamlConfiguration config = new YamlConfiguration(); // Config file
+ config.options().header("This configuration file affects all plugins using the Updater system (version 2+ - http://forums.bukkit.org/threads/96681/ )" + '\n'
+ + "If you wish to use your API key, read http://wiki.bukkit.org/ServerMods_API and place it below." + '\n'
+ + "Some updating systems will not adhere to the disabled value, but these may be turned off in their plugin's configuration.");
+ config.addDefault("api-key", "PUT_API_KEY_HERE");
+ config.addDefault("disable", false);
+
+ if (!updaterFile.exists()) {
+ updaterFile.mkdir();
+ }
+
+ boolean createFile = !updaterConfigFile.exists();
+ try {
+ if (createFile) {
+ updaterConfigFile.createNewFile();
+ config.options().copyDefaults(true);
+ config.save(updaterConfigFile);
+ } else {
+ config.load(updaterConfigFile);
+ }
+ } catch (final Exception e) {
+ if (createFile) {
+ plugin.getLogger().severe("The updater could not create configuration at " + updaterFile.getAbsolutePath());
+ } else {
+ plugin.getLogger().severe("The updater could not load configuration at " + updaterFile.getAbsolutePath());
+ }
+ plugin.getLogger().log(Level.SEVERE, null, e);
+ }
+
+ boolean disabled = false;
+ if (config.contains("disable"))
+ disabled = config.getBoolean("disable", false);
+
+ if (disabled) {
+ plugin.getLogger().warning("You have opted-out of auto updating, so you won't know for certain if you're running the latest version.");
+ return;
+ }
+
+ UpdateChecker.init(plugin, pluginId).requestUpdateCheck().whenComplete((result, exception) -> {
+ plugin.getLogger().info(ChatColor.GOLD + "Running " + plugin.getDescription().getName() + " v" + plugin.getDescription().getVersion() + ".");
+ switch (result.getReason()) {
+ case UP_TO_DATE:
+ plugin.getLogger().info(ChatColor.GREEN + "You are currently running the latest version.");
+ break;
+ case UNRELEASED_VERSION:
+ plugin.getLogger().info(ChatColor.DARK_GREEN + "You are currently running a version slightly ahead from release (development build?)");
+ break;
+ case INVALID_JSON:
+ case UNKNOWN_ERROR:
+ case COULD_NOT_CONNECT:
+ case UNAUTHORIZED_QUERY:
+ plugin.getLogger().warning("An error occurred when trying to update the plugin. If this persists, please contact the BattlePlugins team!");
+ break;
+ case UNSUPPORTED_VERSION_SCHEME:
+ plugin.getLogger().warning("An error occurred with the plugin version scheme, please contact the BatlePlugins team!");
+ case NEW_UPDATE:
+ plugin.getLogger().info(ChatColor.AQUA + "A new update was found: " + plugin.getDescription().getName() + " " + result.getNewestVersion());
+
+ File folder = new File(plugin.getDataFolder().getParent(), updateFolder);
+ downloadFile(folder, plugin.getDescription().getName() + ".jar", result.getNewestVersion(), String.format(downloadLink, result.getNewestVersion()));
+ }
+ });
+ }
+
+ /**
+ * Downloads a file from the specified URL into the server's update folder.
+ *
+ * @param folder the updates folder location.
+ * @param file the name of the file to save it as.
+ * @param link the url of the file.
+ * @param version the version of the plugin.
+ */
+ private void downloadFile(File folder, String file, String version, String link) {
+ if (!folder.exists()) {
+ folder.mkdir();
+ }
+ BufferedInputStream in = null;
+ FileOutputStream fout = null;
+ try {
+ // Download the file
+ final URL url = new URL(link);
+ final int fileLength = url.openConnection().getContentLength();
+ in = new BufferedInputStream(url.openStream());
+ fout = new FileOutputStream(folder.getAbsolutePath() + File.separator + file);
+
+ final byte[] data = new byte[1024];
+ int count;
+ this.plugin.getLogger().info("About to download a new update: " + version);
+ long downloaded = 0;
+ while ((count = in.read(data, 0, 1024)) != -1) {
+ downloaded += count;
+ fout.write(data, 0, count);
+ }
+ //Just a quick check to make sure we didn't leave any files from last time...
+ for (final File xFile : new File(this.plugin.getDataFolder().getParent(), this.updateFolder).listFiles()) {
+ if (xFile.getName().endsWith(".zip")) {
+ xFile.delete();
+ }
+ }
+ // Check to see if it's a zip file, if it is, unzip it.
+ final File dFile = new File(folder.getAbsolutePath() + File.separator + file);
+ if (dFile.getName().endsWith(".zip")) {
+ // Unzip
+ this.unzip(dFile.getCanonicalPath());
+ }
+ this.plugin.getLogger().info("Finished updating.");
+ } catch (final Exception ex) {
+ this.plugin.getLogger().warning("The auto-updater tried to download a new update, but was unsuccessful.");
+ } finally {
+ try {
+ if (in != null) {
+ in.close();
+ }
+ if (fout != null) {
+ fout.close();
+ }
+ } catch (final Exception ex) {
+ }
+ }
+ }
+
+ /**
+ * Part of Zip-File-Extractor, modified by Gravity for use with Updater.
+ *
+ * @param file the location of the file to extract.
+ */
+ private void unzip(String file) {
+ try {
+ final File fSourceZip = new File(file);
+ final String zipPath = file.substring(0, file.length() - 4);
+ ZipFile zipFile = new ZipFile(fSourceZip);
+ Enumeration extends ZipEntry> e = zipFile.entries();
+ while (e.hasMoreElements()) {
+ ZipEntry entry = e.nextElement();
+ File destinationFilePath = new File(zipPath, entry.getName());
+ destinationFilePath.getParentFile().mkdirs();
+ if (entry.isDirectory()) {
+ continue;
+ } else {
+ final BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(entry));
+ int b;
+ final byte buffer[] = new byte[1024];
+ final FileOutputStream fos = new FileOutputStream(destinationFilePath);
+ final BufferedOutputStream bos = new BufferedOutputStream(fos, 1024);
+ while ((b = bis.read(buffer, 0, 1024)) != -1) {
+ bos.write(buffer, 0, b);
+ }
+ bos.flush();
+ bos.close();
+ bis.close();
+ final String name = destinationFilePath.getName();
+ if (name.endsWith(".jar") && this.pluginFile(name)) {
+ destinationFilePath.renameTo(new File(this.plugin.getDataFolder().getParent(), this.updateFolder + File.separator + 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 (final File dFile : new File(zipPath).listFiles()) {
+ if (dFile.isDirectory()) {
+ if (this.pluginFile(dFile.getName())) {
+ final File oFile = new File(this.plugin.getDataFolder().getParent(), dFile.getName()); // Get current dir
+ final File[] contents = oFile.listFiles(); // List of existing files in the current dir
+ for (final File cFile : dFile.listFiles()) // Loop through all the files in the new dir
+ {
+ boolean found = false;
+ for (final 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() + File.separator + 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 (final IOException e) {
+ this.plugin.getLogger().log(Level.SEVERE, "The auto-updater tried to unzip a new update file, but was unsuccessful.", e);
+ }
+ 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.
+ *
+ * @param name a name to check for inside the plugins folder.
+ * @return true if a file inside the plugins folder is named this.
+ */
+ private boolean pluginFile(String name) {
+ for (final File file : new File("plugins").listFiles()) {
+ if (file.getName().equals(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/mc/alk/battlepluginupdater/Updater.java b/src/main/java/mc/alk/battlepluginupdater/Updater.java
index 1bdcd11..80fc48e 100644
--- a/src/main/java/mc/alk/battlepluginupdater/Updater.java
+++ b/src/main/java/mc/alk/battlepluginupdater/Updater.java
@@ -1,5 +1,6 @@
package mc.alk.battlepluginupdater;
+import mc.alk.battlepluginupdater.checker.UpdateChecker;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.Plugin;
import org.json.simple.JSONArray;
@@ -44,6 +45,9 @@
*
* @author Gravity
* @version 2.1
+ *
+ * @deprecated
+ * Instead, use {@link mc.alk.battlepluginupdater.checker.UpdateChecker}
*/
public class Updater {
diff --git a/src/main/java/mc/alk/battlepluginupdater/checker/UpdateChecker.java b/src/main/java/mc/alk/battlepluginupdater/checker/UpdateChecker.java
new file mode 100644
index 0000000..45bc859
--- /dev/null
+++ b/src/main/java/mc/alk/battlepluginupdater/checker/UpdateChecker.java
@@ -0,0 +1,314 @@
+package mc.alk.battlepluginupdater.checker;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.concurrent.CompletableFuture;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.google.common.base.Preconditions;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonSyntaxException;
+
+import org.apache.commons.lang.math.NumberUtils;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.java.JavaPlugin;
+
+/**
+ * A utility class to assist in checking for updates for plugins uploaded to
+ * SpigotMC. Before any members of this
+ * class are accessed, {@link #init(Plugin, int)} must be invoked by the plugin,
+ * preferrably in its {@link JavaPlugin#onEnable()} method, though that is not a
+ * requirement.
+ *
+ * This class performs asynchronous queries to SpiGet,
+ * an REST server which is updated periodically. If the results of {@link #requestUpdateCheck()}
+ * are inconsistent with what is published on SpigotMC, it may be due to SpiGet's cache.
+ * Results will be updated in due time.
+ *
+ * @author Parker Hawke - 2008Choco
+ */
+public final class UpdateChecker {
+
+ public static final VersionScheme VERSION_SCHEME_DECIMAL = (first, second) -> {
+ String[] firstSplit = splitVersionInfo(first), secondSplit = splitVersionInfo(second);
+ if (firstSplit == null || secondSplit == null) return null;
+
+ for (int i = 0; i < Math.min(firstSplit.length, secondSplit.length); i++) {
+ int currentValue = NumberUtils.toInt(firstSplit[i]), newestValue = NumberUtils.toInt(secondSplit[i]);
+
+ if (newestValue > currentValue) {
+ return second;
+ } else if (newestValue < currentValue) {
+ return first;
+ }
+ }
+
+ return (secondSplit.length > firstSplit.length) ? second : first;
+ };
+
+ private static final String USER_AGENT = "CHOCO-update-checker";
+ private static final String UPDATE_URL = "https://api.spiget.org/v2/resources/%d/versions?size=1&sort=-releaseDate";
+ private static final Pattern DECIMAL_SCHEME_PATTERN = Pattern.compile("\\d+(?:\\.\\d+)*");
+
+ private static UpdateChecker instance;
+
+ private UpdateResult lastResult = null;
+
+ private final Plugin plugin;
+ private final int pluginID;
+ private final VersionScheme versionScheme;
+
+ private UpdateChecker(Plugin plugin, int pluginID, VersionScheme versionScheme) {
+ this.plugin = plugin;
+ this.pluginID = pluginID;
+ this.versionScheme = versionScheme;
+ }
+
+ /**
+ * Request an update check to SpiGet. This request is asynchronous and may not complete
+ * immediately as an HTTP GET request is published to the SpiGet API.
+ *
+ * @return a future update result
+ */
+ public CompletableFuture requestUpdateCheck() {
+ return CompletableFuture.supplyAsync(() -> {
+ int responseCode = -1;
+ try {
+ URL url = new URL(String.format(UPDATE_URL, pluginID));
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.addRequestProperty("User-Agent", USER_AGENT);
+
+ InputStreamReader reader = new InputStreamReader(connection.getInputStream());
+ responseCode = connection.getResponseCode();
+
+ JsonElement element = new JsonParser().parse(reader);
+ if (!element.isJsonArray()) {
+ return new UpdateResult(UpdateReason.INVALID_JSON);
+ }
+
+ reader.close();
+
+ JsonObject versionObject = element.getAsJsonArray().get(0).getAsJsonObject();
+ String current = plugin.getDescription().getVersion(), newest = versionObject.get("name").getAsString();
+ String latest = versionScheme.compareVersions(current, newest);
+
+ if (latest == null) {
+ return new UpdateResult(UpdateReason.UNSUPPORTED_VERSION_SCHEME);
+ } else if (latest.equals(current)) {
+ return new UpdateResult(current.equals(newest) ? UpdateReason.UP_TO_DATE : UpdateReason.UNRELEASED_VERSION);
+ } else if (latest.equals(newest)) {
+ return new UpdateResult(UpdateReason.NEW_UPDATE, latest);
+ }
+ } catch (IOException e) {
+ return new UpdateResult(UpdateReason.COULD_NOT_CONNECT);
+ } catch (JsonSyntaxException e) {
+ return new UpdateResult(UpdateReason.INVALID_JSON);
+ }
+
+ return new UpdateResult(responseCode == 401 ? UpdateReason.UNAUTHORIZED_QUERY : UpdateReason.UNKNOWN_ERROR);
+ });
+ }
+
+ /**
+ * Get the last update result that was queried by {@link #requestUpdateCheck()}. If no update
+ * check was performed since this class' initialization, this method will return null.
+ *
+ * @return the last update check result. null if none.
+ */
+ public UpdateResult getLastResult() {
+ return lastResult;
+ }
+
+ private static String[] splitVersionInfo(String version) {
+ Matcher matcher = DECIMAL_SCHEME_PATTERN.matcher(version);
+ if (!matcher.find()) return null;
+
+ return matcher.group().split("\\.");
+ }
+
+ /**
+ * Initialize this update checker with the specified values and return its instance. If an instance
+ * of UpdateChecker has already been initialized, this method will act similarly to {@link #get()}
+ * (which is recommended after initialization).
+ *
+ * @param plugin the plugin for which to check updates. Cannot be null
+ * @param pluginID the ID of the plugin as identified in the SpigotMC resource link. For example,
+ * "https://www.spigotmc.org/resources/veinminer.12038/" would expect "12038" as a value. The
+ * value must be greater than 0
+ * @param versionScheme a custom version scheme parser. Cannot be null
+ *
+ * @return the UpdateChecker instance
+ */
+ public static UpdateChecker init(Plugin plugin, int pluginID, VersionScheme versionScheme) {
+ Preconditions.checkArgument(plugin != null, "Plugin cannot be null");
+ Preconditions.checkArgument(pluginID > 0, "Plugin ID must be greater than 0");
+ Preconditions.checkArgument(versionScheme != null, "null version schemes are unsupported");
+
+ return (instance == null) ? instance = new UpdateChecker(plugin, pluginID, versionScheme) : instance;
+ }
+
+ /**
+ * Initialize this update checker with the specified values and return its instance. If an instance
+ * of UpdateChecker has already been initialized, this method will act similarly to {@link #get()}
+ * (which is recommended after initialization).
+ *
+ * @param plugin the plugin for which to check updates. Cannot be null
+ * @param pluginID the ID of the plugin as identified in the SpigotMC resource link. For example,
+ * "https://www.spigotmc.org/resources/veinminer.12038/" would expect "12038" as a value. The
+ * value must be greater than 0
+ *
+ * @return the UpdateChecker instance
+ */
+ public static UpdateChecker init(Plugin plugin, int pluginID) {
+ return init(plugin, pluginID, VERSION_SCHEME_DECIMAL);
+ }
+
+ /**
+ * Get the initialized instance of UpdateChecker. If {@link #init(Plugin, int)} has not yet been
+ * invoked, this method will throw an exception.
+ *
+ * @return the UpdateChecker instance
+ */
+ public static UpdateChecker get() {
+ Preconditions.checkState(instance != null, "Instance has not yet been initialized. Be sure #init() has been invoked");
+ return instance;
+ }
+
+ /**
+ * Check whether the UpdateChecker has been initialized or not (if {@link #init(Plugin, int)}
+ * has been invoked) and {@link #get()} is safe to use.
+ *
+ * @return true if initialized, false otherwise
+ */
+ public static boolean isInitialized() {
+ return instance != null;
+ }
+
+
+ /**
+ * A functional interface to compare two version Strings with similar version schemes.
+ */
+ @FunctionalInterface
+ public static interface VersionScheme {
+
+ /**
+ * Compare two versions and return the higher of the two. If null is returned, it is assumed
+ * that at least one of the two versions are unsupported by this version scheme parser.
+ *
+ * @param first the first version to check
+ * @param second the second version to check
+ *
+ * @return the greater of the two versions. null if unsupported version schemes
+ */
+ public String compareVersions(String first, String second);
+
+ }
+
+ /**
+ * A constant reason for the result of {@link UpdateResult}.
+ */
+ public static enum UpdateReason {
+
+ /**
+ * A new update is available for download on SpigotMC.
+ */
+ NEW_UPDATE, // The only reason that requires an update
+
+ /**
+ * A successful connection to the SpiGet API could not be established.
+ */
+ COULD_NOT_CONNECT,
+
+ /**
+ * The JSON retrieved from SpiGet was invalid or malformed.
+ */
+ INVALID_JSON,
+
+ /**
+ * A 401 error was returned by the SpiGet API.
+ */
+ UNAUTHORIZED_QUERY,
+
+ /**
+ * The version of the plugin installed on the server is greater than the one uploaded
+ * to SpigotMC's resources section.
+ */
+ UNRELEASED_VERSION,
+
+ /**
+ * An unknown error occurred.
+ */
+ UNKNOWN_ERROR,
+
+ /**
+ * The plugin uses an unsupported version scheme, therefore a proper comparison between
+ * versions could not be made.
+ */
+ UNSUPPORTED_VERSION_SCHEME,
+
+ /**
+ * The plugin is up to date with the version released on SpigotMC's resources section.
+ */
+ UP_TO_DATE;
+
+ }
+
+ /**
+ * Represents a result for an update query performed by {@link UpdateChecker#requestUpdateCheck()}.
+ */
+ public final class UpdateResult {
+
+ private final UpdateReason reason;
+ private final String newestVersion;
+
+ { // An actual use for initializer blocks. This is madness!
+ UpdateChecker.this.lastResult = this;
+ }
+
+ private UpdateResult(UpdateReason reason, String newestVersion) {
+ this.reason = reason;
+ this.newestVersion = newestVersion;
+ }
+
+ private UpdateResult(UpdateReason reason) {
+ Preconditions.checkArgument(reason != UpdateReason.NEW_UPDATE, "Reasons that require updates must also provide the latest version String");
+ this.reason = reason;
+ this.newestVersion = plugin.getDescription().getVersion();
+ }
+
+ /**
+ * Get the constant reason of this result.
+ *
+ * @return the reason
+ */
+ public UpdateReason getReason() {
+ return reason;
+ }
+
+ /**
+ * Check whether or not this result requires the user to update.
+ *
+ * @return true if requires update, false otherwise
+ */
+ public boolean requiresUpdate() {
+ return reason == UpdateReason.NEW_UPDATE;
+ }
+
+ /**
+ * Get the latest version of the plugin. This may be the currently installed version, it
+ * may not be. This depends entirely on the result of the update.
+ *
+ * @return the newest version of the plugin
+ */
+ public String getNewestVersion() {
+ return newestVersion;
+ }
+
+ }
+
+}
\ No newline at end of file