|
|
|
@ -6,6 +6,8 @@
|
|
|
|
|
|
|
|
|
|
package com.Acrobot.ChestShop.Updater;
|
|
|
|
|
|
|
|
|
|
import com.google.common.hash.Hashing;
|
|
|
|
|
import com.google.common.io.Files;
|
|
|
|
|
import org.bukkit.configuration.file.YamlConfiguration;
|
|
|
|
|
import org.bukkit.plugin.Plugin;
|
|
|
|
|
import org.json.simple.JSONArray;
|
|
|
|
@ -19,6 +21,8 @@ import java.net.URLConnection;
|
|
|
|
|
import java.util.Enumeration;
|
|
|
|
|
import java.util.Locale;
|
|
|
|
|
import java.util.logging.Level;
|
|
|
|
|
import java.util.regex.Matcher;
|
|
|
|
|
import java.util.regex.Pattern;
|
|
|
|
|
import java.util.zip.ZipEntry;
|
|
|
|
|
import java.util.zip.ZipFile;
|
|
|
|
|
|
|
|
|
@ -45,6 +49,7 @@ public final class Updater {
|
|
|
|
|
private UpdateType type;
|
|
|
|
|
private String versionName;
|
|
|
|
|
private String versionLink;
|
|
|
|
|
private String versionHash;
|
|
|
|
|
private String versionType;
|
|
|
|
|
private String versionGameVersion;
|
|
|
|
|
|
|
|
|
@ -54,21 +59,21 @@ public final class Updater {
|
|
|
|
|
private File file; // The plugin's file
|
|
|
|
|
private Thread thread; // Updater thread
|
|
|
|
|
|
|
|
|
|
private int id = -1; // Project's Curse ID
|
|
|
|
|
private String apiKey = null; // BukkitDev ServerMods API key
|
|
|
|
|
private static final String TITLE_VALUE = "name"; // Gets remote file's title
|
|
|
|
|
private static final String LINK_VALUE = "downloadUrl"; // Gets remote file's download link
|
|
|
|
|
private static final String TYPE_VALUE = "releaseType"; // Gets remote file's release type
|
|
|
|
|
private static final String VERSION_VALUE = "gameVersion"; // Gets remote file's build version
|
|
|
|
|
private static final String QUERY = "/servermods/files?projectIds="; // Path to GET
|
|
|
|
|
private static final String HOST = "https://api.curseforge.com"; // Slugs will be appended to this to get to the project's RSS feed
|
|
|
|
|
private String id; // Project's Curse ID
|
|
|
|
|
private String apiKey = null; // Modrinth API key
|
|
|
|
|
private static final String TITLE_VALUE = "version_number"; // Gets remote version
|
|
|
|
|
private static final String FILES_VALUE = "files"; // Gets all files associated with that version
|
|
|
|
|
private static final String LINK_VALUE = "url"; // Gets remote file's download link
|
|
|
|
|
private static final String TYPE_VALUE = "version_type"; // Gets remote file's release type
|
|
|
|
|
private static final String VERSION_VALUE = "game_versions"; // Gets remote file's build version
|
|
|
|
|
private static final String QUERY = "/v2/project/%projectid%/version"; // Path to GET
|
|
|
|
|
private static final String HOST = "https://api.modrinth.com"; // Slugs will be appended to this to get to the project's versions
|
|
|
|
|
|
|
|
|
|
private static final String USER_AGENT = "Updater (by Gravity)";
|
|
|
|
|
private static final String delimiter = "^v|[\\s_-]v"; // Used for locating version numbers in file names
|
|
|
|
|
private static final String USER_AGENT = "Updater v2.1 (by Gravity) - Modified by Phoenix616 for Modrinth";
|
|
|
|
|
private static final Pattern VERSION_PATTERN = Pattern.compile("(\\d+\\.\\d+(?>\\.\\d+)?)"); // Used for locating version numbers in file names
|
|
|
|
|
private static final String[] NO_UPDATE_TAG = { "-DEV", "-PRE", "-SNAPSHOT" }; // If the version number contains one of these, don't update.
|
|
|
|
|
private static final int BYTE_SIZE = 1024; // Used for downloading files
|
|
|
|
|
private final YamlConfiguration config = new YamlConfiguration(); // Config file
|
|
|
|
|
private String updateFolder;// The folder that downloads will be placed in
|
|
|
|
|
private Updater.UpdateResult result = Updater.UpdateResult.SUCCESS; // Used for determining the outcome of the update process
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -110,7 +115,11 @@ public final class Updater {
|
|
|
|
|
/**
|
|
|
|
|
* The updater found an update, but because of the UpdateType being set to NO_DOWNLOAD, it wasn't downloaded.
|
|
|
|
|
*/
|
|
|
|
|
UPDATE_AVAILABLE
|
|
|
|
|
UPDATE_AVAILABLE,
|
|
|
|
|
/**
|
|
|
|
|
* The downloaded file does not match the SHA1 hash sum provided by the api.
|
|
|
|
|
*/
|
|
|
|
|
FAIL_HASH,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -153,27 +162,26 @@ public final class Updater {
|
|
|
|
|
* Initialize the updater.
|
|
|
|
|
*
|
|
|
|
|
* @param plugin The plugin that is checking for an update.
|
|
|
|
|
* @param id The dev.bukkit.org id of the project.
|
|
|
|
|
* @param id The id of the project.
|
|
|
|
|
* @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, int id, File file, UpdateType type, boolean announce) {
|
|
|
|
|
public Updater(Plugin plugin, String id, File file, UpdateType type, boolean announce) {
|
|
|
|
|
this.plugin = plugin;
|
|
|
|
|
this.type = type;
|
|
|
|
|
this.announce = announce;
|
|
|
|
|
this.file = file;
|
|
|
|
|
this.id = id;
|
|
|
|
|
this.updateFolder = plugin.getServer().getUpdateFolder();
|
|
|
|
|
|
|
|
|
|
final File pluginFile = plugin.getDataFolder().getParentFile();
|
|
|
|
|
final File updaterFile = new File(pluginFile, "Updater");
|
|
|
|
|
final File updaterConfigFile = new File(updaterFile, "config.yml");
|
|
|
|
|
|
|
|
|
|
this.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'
|
|
|
|
|
this.config.options().header("This configuration file affects all plugins using the Updater system (version 2+ )" + '\n'
|
|
|
|
|
+ "If you wish to use your API key, then you can get it from https://modrinth.com/settings/pats 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.");
|
|
|
|
|
this.config.addDefault("api-key", "PUT_API_KEY_HERE");
|
|
|
|
|
this.config.addDefault("modrinth-key", "PUT_PAT_HERE");
|
|
|
|
|
this.config.addDefault("disable", false);
|
|
|
|
|
|
|
|
|
|
if (!updaterFile.exists()) {
|
|
|
|
@ -211,7 +219,7 @@ public final class Updater {
|
|
|
|
|
this.apiKey = key;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
this.url = new URL(Updater.HOST + Updater.QUERY + id);
|
|
|
|
|
this.url = new URL(Updater.HOST + Updater.QUERY.replace("%projectid%", id));
|
|
|
|
|
} catch (final MalformedURLException e) {
|
|
|
|
|
plugin.getLogger().log(Level.SEVERE, "The project ID provided for updating, " + id + " is invalid.", e);
|
|
|
|
|
this.result = UpdateResult.FAIL_BADID;
|
|
|
|
@ -240,6 +248,16 @@ public final class Updater {
|
|
|
|
|
*/
|
|
|
|
|
public ReleaseType getLatestType() {
|
|
|
|
|
this.waitForThread();
|
|
|
|
|
return getLatestTypeInternal();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the latest version's release type without waiting for the thread to finish.
|
|
|
|
|
*
|
|
|
|
|
* @return latest version's release type.
|
|
|
|
|
* @see ReleaseType
|
|
|
|
|
*/
|
|
|
|
|
private ReleaseType getLatestTypeInternal() {
|
|
|
|
|
if (this.versionType != null) {
|
|
|
|
|
for (ReleaseType type : ReleaseType.values()) {
|
|
|
|
|
if (this.versionType.equals(type.name().toLowerCase(Locale.ROOT))) {
|
|
|
|
@ -309,8 +327,9 @@ public final class Updater {
|
|
|
|
|
// Download the file
|
|
|
|
|
final URL url = new URL(link);
|
|
|
|
|
final int fileLength = url.openConnection().getContentLength();
|
|
|
|
|
final File targetFile = new File(folder, file);
|
|
|
|
|
try (BufferedInputStream in = new BufferedInputStream(url.openStream());
|
|
|
|
|
FileOutputStream fout = new FileOutputStream(folder.getAbsolutePath() + File.separator + file)) {
|
|
|
|
|
FileOutputStream fout = new FileOutputStream(targetFile)) {
|
|
|
|
|
|
|
|
|
|
final byte[] data = new byte[Updater.BYTE_SIZE];
|
|
|
|
|
int count;
|
|
|
|
@ -318,16 +337,27 @@ public final class Updater {
|
|
|
|
|
this.plugin.getLogger().info("About to download a new update: " + this.versionName);
|
|
|
|
|
}
|
|
|
|
|
long downloaded = 0;
|
|
|
|
|
int lastAnnouncePercent = 0;
|
|
|
|
|
while ((count = in.read(data, 0, Updater.BYTE_SIZE)) != -1) {
|
|
|
|
|
downloaded += count;
|
|
|
|
|
fout.write(data, 0, count);
|
|
|
|
|
final int percent = (int) ((downloaded * 100) / fileLength);
|
|
|
|
|
if (this.announce && ((percent % 10) == 0)) {
|
|
|
|
|
if (this.announce && lastAnnouncePercent != percent && ((percent % 10) == 0)) {
|
|
|
|
|
lastAnnouncePercent = percent;
|
|
|
|
|
this.plugin.getLogger().info("Downloading update: " + percent + "% of " + fileLength + " bytes.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Check sha1 sum of the downloaded file
|
|
|
|
|
if (this.versionHash != null) {
|
|
|
|
|
final String fileHash = Files.asByteSource(targetFile).hash(Hashing.sha512()).toString();
|
|
|
|
|
if (!this.versionHash.equalsIgnoreCase(fileHash)) {
|
|
|
|
|
this.plugin.getLogger().warning("Downloaded file " + file + " does not match the remote file's SHA-1 hash");
|
|
|
|
|
this.result = UpdateResult.FAIL_HASH;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//Just a quick check to make sure we didn't leave any files from last time...
|
|
|
|
|
File[] files = new File(this.plugin.getDataFolder().getParent(), this.updateFolder).listFiles();
|
|
|
|
|
File[] files = this.plugin.getServer().getUpdateFolderFile().listFiles();
|
|
|
|
|
if (files != null) {
|
|
|
|
|
for (final File xFile : files) {
|
|
|
|
|
if (xFile.getName().endsWith(".zip")) {
|
|
|
|
@ -336,7 +366,7 @@ public final class Updater {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Check to see if it's a zip file, if it is, unzip it.
|
|
|
|
|
final File dFile = new File(folder.getAbsolutePath() + File.separator + file);
|
|
|
|
|
final File dFile = new File(folder.getAbsolutePath(), file);
|
|
|
|
|
if (dFile.getName().endsWith(".zip")) {
|
|
|
|
|
// Unzip
|
|
|
|
|
this.unzip(dFile.getCanonicalPath());
|
|
|
|
@ -379,7 +409,7 @@ public final class Updater {
|
|
|
|
|
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));
|
|
|
|
|
destinationFilePath.renameTo(new File(this.plugin.getServer().getUpdateFolderFile(),name));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -409,7 +439,7 @@ public final class Updater {
|
|
|
|
|
}
|
|
|
|
|
if (!found) {
|
|
|
|
|
// Move the new file into the current dir
|
|
|
|
|
cFile.renameTo(new File(oFile.getCanonicalFile() + File.separator + cFile.getName()));
|
|
|
|
|
cFile.renameTo(new File(oFile.getCanonicalFile(), cFile.getName()));
|
|
|
|
|
} else {
|
|
|
|
|
// This file already exists, so we don't need it anymore.
|
|
|
|
|
cFile.delete();
|
|
|
|
@ -456,11 +486,14 @@ public final class Updater {
|
|
|
|
|
*/
|
|
|
|
|
private boolean versionCheck(String title) {
|
|
|
|
|
if (this.type != UpdateType.NO_VERSION_CHECK) {
|
|
|
|
|
final String localVersion = this.plugin.getDescription().getVersion();
|
|
|
|
|
if (title.split(delimiter).length == 2) {
|
|
|
|
|
final String remoteVersion = title.split(delimiter)[1].split(" ")[0]; // Get the newest file's version number
|
|
|
|
|
final String rawLocalVersion = this.plugin.getDescription().getVersion();
|
|
|
|
|
final Matcher localMatcher = Updater.VERSION_PATTERN.matcher(rawLocalVersion);
|
|
|
|
|
final Matcher titleMatcher = Updater.VERSION_PATTERN.matcher(title);
|
|
|
|
|
if (titleMatcher.find() && localMatcher.find()) {
|
|
|
|
|
final String localVersion = localMatcher.group(1); // Get the plugins version number
|
|
|
|
|
final String remoteVersion = titleMatcher.group(1); // Get the newest file's version number
|
|
|
|
|
|
|
|
|
|
if (this.hasTag(localVersion) || !this.shouldUpdate(localVersion, remoteVersion)) {
|
|
|
|
|
if (this.hasTag(rawLocalVersion) || !this.shouldUpdate(localVersion, remoteVersion)) {
|
|
|
|
|
// We already have the latest version, or this build is tagged for no-update
|
|
|
|
|
this.result = Updater.UpdateResult.NO_UPDATE;
|
|
|
|
|
return false;
|
|
|
|
@ -502,11 +535,49 @@ public final class Updater {
|
|
|
|
|
* @return true if Updater should consider the remote version an update, false if not.
|
|
|
|
|
*/
|
|
|
|
|
public boolean shouldUpdate(String localVersion, String remoteVersion) {
|
|
|
|
|
if (this.type != Updater.UpdateType.NO_DOWNLOAD && localVersion.contains("DEV") || getLatestType() != ReleaseType.RELEASE) {
|
|
|
|
|
if (this.type != Updater.UpdateType.NO_DOWNLOAD && localVersion.contains("DEV") || getLatestTypeInternal() != ReleaseType.RELEASE) {
|
|
|
|
|
return false; //Do not download alphas or betas
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return !localVersion.equalsIgnoreCase(remoteVersion);
|
|
|
|
|
if (localVersion.equalsIgnoreCase(remoteVersion)) {
|
|
|
|
|
return true; //Already the same version
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
int[] localSemanticVersion = parseVersion(localVersion);
|
|
|
|
|
int[] remoteSemanticVersion = parseVersion(remoteVersion);
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < localSemanticVersion.length; i++) {
|
|
|
|
|
if (remoteSemanticVersion.length < i + 1) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (localSemanticVersion[i] < remoteSemanticVersion[i]) {
|
|
|
|
|
return true;
|
|
|
|
|
} else if (localSemanticVersion[i] > remoteSemanticVersion[i]) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
} catch (NumberFormatException e) {
|
|
|
|
|
this.plugin.getLogger().warning("Invalid version number found: " + localVersion + " or " + remoteVersion);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Parse the version number from a string. This expects the version number to be consisting of numbers separated by dots.
|
|
|
|
|
*
|
|
|
|
|
* @param version the version string to parse
|
|
|
|
|
* @return the parsed version number
|
|
|
|
|
* @throws NumberFormatException if the version number is not in the expected format
|
|
|
|
|
*/
|
|
|
|
|
private int[] parseVersion(String version) {
|
|
|
|
|
final String[] split = version.split("\\.");
|
|
|
|
|
final int[] semanticVersion = new int[split.length];
|
|
|
|
|
for (int i = 0; i < split.length; i++) {
|
|
|
|
|
semanticVersion[i] = Integer.parseInt(split[i]);
|
|
|
|
|
}
|
|
|
|
|
return semanticVersion;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -535,7 +606,7 @@ public final class Updater {
|
|
|
|
|
conn.setConnectTimeout(5000);
|
|
|
|
|
|
|
|
|
|
if (this.apiKey != null) {
|
|
|
|
|
conn.addRequestProperty("X-API-Key", this.apiKey);
|
|
|
|
|
conn.addRequestProperty("Authorization", this.apiKey);
|
|
|
|
|
}
|
|
|
|
|
conn.addRequestProperty("User-Agent", Updater.USER_AGENT);
|
|
|
|
|
|
|
|
|
@ -553,18 +624,26 @@ public final class Updater {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.versionName = (String) ((JSONObject) array.get(array.size() - 1)).get(Updater.TITLE_VALUE);
|
|
|
|
|
this.versionLink = (String) ((JSONObject) array.get(array.size() - 1)).get(Updater.LINK_VALUE);
|
|
|
|
|
JSONArray versionFiles = (JSONArray) ((JSONObject) array.get(array.size() - 1)).get(Updater.FILES_VALUE);
|
|
|
|
|
for (Object versionFile : versionFiles) {
|
|
|
|
|
JSONObject file = (JSONObject) versionFile;
|
|
|
|
|
if (file.get("primary").equals(true)) {
|
|
|
|
|
versionLink = (String) file.get(Updater.LINK_VALUE);
|
|
|
|
|
versionHash = ((JSONObject) file.get("hashes")).get("sha512").toString();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.versionType = (String) ((JSONObject) array.get(array.size() - 1)).get(Updater.TYPE_VALUE);
|
|
|
|
|
this.versionGameVersion = (String) ((JSONObject) array.get(array.size() - 1)).get(Updater.VERSION_VALUE);
|
|
|
|
|
|
|
|
|
|
JSONArray gameVersions = (JSONArray) ((JSONObject) array.get(array.size() - 1)).get(Updater.VERSION_VALUE);
|
|
|
|
|
this.versionGameVersion = gameVersions.get(gameVersions.size() - 1).toString();
|
|
|
|
|
return true;
|
|
|
|
|
} catch (final IOException e) {
|
|
|
|
|
if (e.getMessage().contains("HTTP response code: 403")) {
|
|
|
|
|
this.plugin.getLogger().severe("dev.bukkit.org rejected the API key provided in plugins/Updater/config.yml");
|
|
|
|
|
this.plugin.getLogger().severe("The Modrinth API server rejected the API key provided in plugins/Updater/config.yml");
|
|
|
|
|
this.plugin.getLogger().severe("Please double-check your configuration to ensure it is correct.");
|
|
|
|
|
this.result = UpdateResult.FAIL_APIKEY;
|
|
|
|
|
} else {
|
|
|
|
|
this.plugin.getLogger().severe("The updater could not contact dev.bukkit.org for updating.");
|
|
|
|
|
this.plugin.getLogger().severe("The updater could not contact the api.modrinth.com server for updating.");
|
|
|
|
|
this.plugin.getLogger().severe("If you have not recently modified your configuration and this is the first time you are seeing this message, the site may be experiencing temporary downtime.");
|
|
|
|
|
this.result = UpdateResult.FAIL_DBO;
|
|
|
|
|
}
|
|
|
|
@ -588,7 +667,7 @@ public final class Updater {
|
|
|
|
|
final String[] split = Updater.this.versionLink.split("/");
|
|
|
|
|
name = split[split.length - 1];
|
|
|
|
|
}
|
|
|
|
|
Updater.this.saveFile(new File(Updater.this.plugin.getDataFolder().getParent(), Updater.this.updateFolder), name, Updater.this.versionLink);
|
|
|
|
|
Updater.this.saveFile(Updater.this.plugin.getServer().getUpdateFolderFile(), name, Updater.this.versionLink);
|
|
|
|
|
} else {
|
|
|
|
|
Updater.this.result = UpdateResult.UPDATE_AVAILABLE;
|
|
|
|
|
}
|
|
|
|
|