mirror of
https://github.com/plexusorg/Plex.git
synced 2026-06-04 05:26:55 +00:00
Rewrite update system
This commit is contained in:
@@ -2,6 +2,8 @@ import net.minecrell.pluginyml.paper.PaperPluginDescription
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
val paperApiVersion = "26.1.2"
|
||||
|
||||
plugins {
|
||||
java
|
||||
`maven-publish`
|
||||
@@ -29,7 +31,7 @@ dependencies {
|
||||
library("com.zaxxer:HikariCP:7.0.2")
|
||||
library("com.j256.ormlite:ormlite-jdbc:6.1")
|
||||
library("org.jetbrains:annotations:26.1.0")
|
||||
compileOnly("io.papermc.paper:paper-api:26.1.2.build.+")
|
||||
compileOnly("io.papermc.paper:paper-api:${paperApiVersion}.build.+")
|
||||
compileOnly("com.github.MilkBowl:VaultAPI:1.7.1") {
|
||||
exclude("org.bukkit", "bukkit")
|
||||
}
|
||||
@@ -53,7 +55,7 @@ paper {
|
||||
loader = "dev.plex.PlexLibraryManager"
|
||||
website = "https://plex.us.org"
|
||||
authors = listOf("Telesphoreo", "taahanis", "supernt")
|
||||
apiVersion = "26.1.2"
|
||||
apiVersion = paperApiVersion
|
||||
foliaSupported = true
|
||||
generateLibrariesJson = true
|
||||
// Load BukkitTelnet and LibsDisguises before Plex so the modules register properly
|
||||
@@ -131,6 +133,7 @@ tasks {
|
||||
property("buildNumber", if (System.getenv("BUILD_NUMBER") != null) System.getenv("BUILD_NUMBER") else getBuildNumber())
|
||||
property("date", SimpleDateFormat("MM/dd/yyyy '<light_purple>at<gold>' hh:mm:ss a z").format(Date()))
|
||||
property("gitCommit", indraGit.commit().get().name.take(7))
|
||||
property("minecraftVersion", paperApiVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@CommandPermissions(source = RequiredCommandSource.ANY)
|
||||
@CommandParameters(name = "plex", usage = "/<command> [reload | redis | modules [reload]]", description = "Show information about Plex or reload it")
|
||||
@CommandParameters(name = "plex", usage = "/<command> [reload | redis | update | modules [reload | update]]", description = "Show information about Plex or reload it")
|
||||
public class PlexCMD extends ServerCommand
|
||||
{
|
||||
// Don't modify this command
|
||||
@@ -132,11 +132,11 @@ public class PlexCMD extends ServerCommand
|
||||
{
|
||||
if (args.length == 1)
|
||||
{
|
||||
return Arrays.asList("reload", "redis", "modules");
|
||||
return Arrays.asList("reload", "redis", "modules", "update");
|
||||
}
|
||||
else if (args[0].equalsIgnoreCase("modules"))
|
||||
{
|
||||
return List.of("reload");
|
||||
return Arrays.asList("reload", "update");
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
package dev.plex.updater;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class ArtifactMetadata
|
||||
{
|
||||
private static final int CURRENT_SCHEMA_VERSION = 1;
|
||||
private static final Pattern SHA256_PATTERN = Pattern.compile("^[a-fA-F0-9]{64}$");
|
||||
|
||||
private int schemaVersion;
|
||||
private String name;
|
||||
private String version;
|
||||
private String buildNumber;
|
||||
private String commit;
|
||||
private String channel;
|
||||
private List<String> minecraftVersions;
|
||||
private Integer apiCompatibility;
|
||||
private Integer requiredApiCompatibility;
|
||||
private String downloadUrl;
|
||||
private String sha256;
|
||||
private Long size;
|
||||
private String publishedAt;
|
||||
|
||||
public String name()
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
public String version()
|
||||
{
|
||||
return version;
|
||||
}
|
||||
|
||||
public String buildNumber()
|
||||
{
|
||||
return buildNumber;
|
||||
}
|
||||
|
||||
public String commit()
|
||||
{
|
||||
return commit;
|
||||
}
|
||||
|
||||
public String channel()
|
||||
{
|
||||
return channel;
|
||||
}
|
||||
|
||||
public String downloadUrl()
|
||||
{
|
||||
return downloadUrl;
|
||||
}
|
||||
|
||||
public String sha256()
|
||||
{
|
||||
return sha256;
|
||||
}
|
||||
|
||||
public Long size()
|
||||
{
|
||||
return size;
|
||||
}
|
||||
|
||||
public String publishedAt()
|
||||
{
|
||||
return publishedAt;
|
||||
}
|
||||
|
||||
public Optional<String> validatePlex(UpdateChannel requestedChannel, String minecraftVersion)
|
||||
{
|
||||
Optional<String> commonError = validateCommon(requestedChannel);
|
||||
if (commonError.isPresent())
|
||||
{
|
||||
return commonError;
|
||||
}
|
||||
if (!"Plex".equalsIgnoreCase(name))
|
||||
{
|
||||
return Optional.of("Plex metadata has unexpected artifact name " + name);
|
||||
}
|
||||
if (minecraftVersions == null || !minecraftVersions.contains(minecraftVersion))
|
||||
{
|
||||
return Optional.of("metadata does not include Minecraft version " + minecraftVersion);
|
||||
}
|
||||
if (apiCompatibility == null)
|
||||
{
|
||||
return Optional.of("Plex metadata is missing apiCompatibility");
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public Optional<String> validateModule(UpdateChannel requestedChannel, String moduleName, int apiCompatibility)
|
||||
{
|
||||
Optional<String> commonError = validateCommon(requestedChannel);
|
||||
if (commonError.isPresent())
|
||||
{
|
||||
return commonError;
|
||||
}
|
||||
if (!moduleName.equalsIgnoreCase(name))
|
||||
{
|
||||
return Optional.of("module metadata has unexpected artifact name " + name);
|
||||
}
|
||||
if (requiredApiCompatibility == null)
|
||||
{
|
||||
return Optional.of("module metadata is missing requiredApiCompatibility");
|
||||
}
|
||||
if (requiredApiCompatibility != apiCompatibility)
|
||||
{
|
||||
return Optional.of("module metadata requires API compatibility " + requiredApiCompatibility + ", but Plex provides " + apiCompatibility);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public boolean matchesCurrentBuild(String currentVersion, String currentBuildNumber, String currentCommit)
|
||||
{
|
||||
if (!Objects.equals(version, currentVersion))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (isKnown(commit) && isKnown(currentCommit))
|
||||
{
|
||||
return commit.equalsIgnoreCase(currentCommit);
|
||||
}
|
||||
return Objects.equals(buildNumber, currentBuildNumber);
|
||||
}
|
||||
|
||||
public String fileName()
|
||||
{
|
||||
try
|
||||
{
|
||||
String path = URI.create(downloadUrl).getPath();
|
||||
int separatorIndex = path.lastIndexOf('/');
|
||||
String fileName = separatorIndex >= 0 ? path.substring(separatorIndex + 1) : path;
|
||||
if (!fileName.isBlank())
|
||||
{
|
||||
return URLDecoder.decode(fileName, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException ignored)
|
||||
{
|
||||
}
|
||||
return name + "-" + version + ".jar";
|
||||
}
|
||||
|
||||
private Optional<String> validateCommon(UpdateChannel requestedChannel)
|
||||
{
|
||||
if (schemaVersion != CURRENT_SCHEMA_VERSION)
|
||||
{
|
||||
return Optional.of("metadata schemaVersion " + schemaVersion + " is not supported");
|
||||
}
|
||||
if (isBlank(name))
|
||||
{
|
||||
return Optional.of("metadata is missing name");
|
||||
}
|
||||
if (isBlank(version))
|
||||
{
|
||||
return Optional.of("metadata is missing version");
|
||||
}
|
||||
if (isBlank(channel))
|
||||
{
|
||||
return Optional.of("metadata is missing channel");
|
||||
}
|
||||
if (!requestedChannel.id().equalsIgnoreCase(channel))
|
||||
{
|
||||
return Optional.of("metadata channel " + channel + " does not match requested channel " + requestedChannel.id());
|
||||
}
|
||||
if (requestedChannel == UpdateChannel.STABLE && version.toUpperCase(Locale.ROOT).contains("SNAPSHOT"))
|
||||
{
|
||||
return Optional.of("stable metadata must not point to a SNAPSHOT version");
|
||||
}
|
||||
if (isBlank(downloadUrl))
|
||||
{
|
||||
return Optional.of("metadata is missing downloadUrl");
|
||||
}
|
||||
if (isBlank(sha256) || !SHA256_PATTERN.matcher(sha256).matches())
|
||||
{
|
||||
return Optional.of("metadata is missing a valid sha256");
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static boolean isBlank(String value)
|
||||
{
|
||||
return value == null || value.isBlank();
|
||||
}
|
||||
|
||||
private static boolean isKnown(String value)
|
||||
{
|
||||
return !isBlank(value) && !"unknown".equalsIgnoreCase(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package dev.plex.updater;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public enum UpdateChannel
|
||||
{
|
||||
STABLE("stable"),
|
||||
DEV("dev");
|
||||
|
||||
private final String id;
|
||||
|
||||
UpdateChannel(String id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String id()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
public static UpdateChannel fromConfig(String value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return STABLE;
|
||||
}
|
||||
|
||||
return switch (value.trim().toLowerCase(Locale.ROOT))
|
||||
{
|
||||
case "dev" -> DEV;
|
||||
case "stable" -> STABLE;
|
||||
default -> STABLE;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package dev.plex.updater;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class UpdateMetadataClient
|
||||
{
|
||||
private static final List<String> BASE_URLS = List.of("https://updater.plex.us.org", "https://plex-updater.com");
|
||||
|
||||
private final Gson gson = new Gson();
|
||||
private final UpdateChannel channel;
|
||||
|
||||
public UpdateMetadataClient(UpdateChannel channel)
|
||||
{
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
public ArtifactMetadata fetchPlexLatest(String minecraftVersion) throws MetadataException
|
||||
{
|
||||
String path = "/api/v1/projects/Plex/channels/" + channel.id() + "/latest/minecraft/" + encodePathSegment(minecraftVersion) + ".json";
|
||||
ArtifactMetadata metadata = fetch(path);
|
||||
Optional<String> validationError = metadata.validatePlex(channel, minecraftVersion);
|
||||
if (validationError.isPresent())
|
||||
{
|
||||
throw new MetadataException(validationError.get(), false);
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public ArtifactMetadata fetchModuleLatest(String moduleName, int apiCompatibility) throws MetadataException
|
||||
{
|
||||
String path = "/api/v1/projects/" + encodePathSegment(moduleName) + "/channels/" + channel.id() + "/latest/api/" + apiCompatibility + ".json";
|
||||
ArtifactMetadata metadata = fetch(path);
|
||||
Optional<String> validationError = metadata.validateModule(channel, moduleName, apiCompatibility);
|
||||
if (validationError.isPresent())
|
||||
{
|
||||
throw new MetadataException(validationError.get(), false);
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private ArtifactMetadata fetch(String path) throws MetadataException
|
||||
{
|
||||
MetadataException notFound = null;
|
||||
MetadataException failure = null;
|
||||
for (String baseUrl : BASE_URLS)
|
||||
{
|
||||
try
|
||||
{
|
||||
return fetch(baseUrl, path);
|
||||
}
|
||||
catch (MetadataException e)
|
||||
{
|
||||
if (e.notFound())
|
||||
{
|
||||
if (notFound == null)
|
||||
{
|
||||
notFound = e;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
failure = e;
|
||||
}
|
||||
}
|
||||
|
||||
if (failure != null)
|
||||
{
|
||||
throw new MetadataException("all metadata endpoints failed for " + path + "; last error: " + failure.getMessage(), false, failure);
|
||||
}
|
||||
if (notFound != null)
|
||||
{
|
||||
throw notFound;
|
||||
}
|
||||
throw new MetadataException("no updater metadata endpoints are available", false);
|
||||
}
|
||||
|
||||
private ArtifactMetadata fetch(String baseUrl, String path) throws MetadataException
|
||||
{
|
||||
String url = baseUrl + path;
|
||||
try
|
||||
{
|
||||
HttpURLConnection connection = (HttpURLConnection) URI.create(url).toURL().openConnection();
|
||||
connection.setConnectTimeout(10000);
|
||||
connection.setReadTimeout(10000);
|
||||
connection.setRequestProperty("Accept", "application/json");
|
||||
|
||||
int statusCode = connection.getResponseCode();
|
||||
if (statusCode == HttpURLConnection.HTTP_NOT_FOUND)
|
||||
{
|
||||
throw new MetadataException("no compatible update metadata exists at " + path + " on " + baseUrl, true);
|
||||
}
|
||||
if (statusCode != HttpURLConnection.HTTP_OK)
|
||||
{
|
||||
throw new MetadataException("metadata request returned HTTP " + statusCode + " for " + path + " on " + baseUrl, false);
|
||||
}
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)))
|
||||
{
|
||||
ArtifactMetadata metadata = gson.fromJson(reader, ArtifactMetadata.class);
|
||||
if (metadata == null)
|
||||
{
|
||||
throw new MetadataException("metadata response was empty for " + path + " on " + baseUrl, false);
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
catch (JsonSyntaxException e)
|
||||
{
|
||||
throw new MetadataException("metadata response was not valid JSON for " + path + " on " + baseUrl, false, e);
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException e)
|
||||
{
|
||||
throw new MetadataException("metadata URL is invalid: " + url, false, e);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new MetadataException("metadata request failed for " + path + " on " + baseUrl, false, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String encodePathSegment(String value)
|
||||
{
|
||||
return URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", "%20");
|
||||
}
|
||||
|
||||
public static final class MetadataException extends Exception
|
||||
{
|
||||
private final boolean notFound;
|
||||
|
||||
private MetadataException(String message, boolean notFound)
|
||||
{
|
||||
super(message);
|
||||
this.notFound = notFound;
|
||||
}
|
||||
|
||||
private MetadataException(String message, boolean notFound, Throwable cause)
|
||||
{
|
||||
super(message, cause);
|
||||
this.notFound = notFound;
|
||||
}
|
||||
|
||||
public boolean notFound()
|
||||
{
|
||||
return notFound;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ public class BuildInfo
|
||||
public static String date;
|
||||
@Getter
|
||||
public static String number;
|
||||
@Getter
|
||||
public static String minecraftVersion;
|
||||
|
||||
public void load(Plex plugin)
|
||||
{
|
||||
@@ -33,6 +35,7 @@ public class BuildInfo
|
||||
commit = props.getProperty("gitCommit", "unknown");
|
||||
date = props.getProperty("date", "unknown");
|
||||
number = props.getProperty("buildNumber", "unknown");
|
||||
minecraftVersion = props.getProperty("minecraftVersion", "unknown");
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
|
||||
@@ -1,88 +1,41 @@
|
||||
package dev.plex.util;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import dev.plex.Plex;
|
||||
import dev.plex.updater.ArtifactMetadata;
|
||||
import dev.plex.updater.UpdateChannel;
|
||||
import dev.plex.updater.UpdateMetadataClient;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
|
||||
import lombok.NonNull;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.json.JSONException;
|
||||
|
||||
public class UpdateChecker
|
||||
{
|
||||
/*
|
||||
* -4 = Never checked for updates
|
||||
* -3 = Likely rate limited
|
||||
* -2 = Unknown commit
|
||||
* -1 = Error occurred
|
||||
* 0 = Up to date
|
||||
* > 0 = Number of commits behind
|
||||
*/
|
||||
private final Plex plugin;
|
||||
private final String DOWNLOAD_PAGE = "https://ci.plex.us.org/job/";
|
||||
private final String REPO;
|
||||
private String BRANCH;
|
||||
private int distance = -4;
|
||||
private final UpdateChannel channel;
|
||||
private final UpdateMetadataClient metadataClient;
|
||||
private ArtifactMetadata latestPlexMetadata;
|
||||
|
||||
public UpdateChecker(Plex plugin)
|
||||
{
|
||||
this.plugin = plugin;
|
||||
this.REPO = plugin.config.getString("update_repo");
|
||||
this.BRANCH = plugin.config.getString("update_branch");
|
||||
}
|
||||
|
||||
// Adapted from Paper
|
||||
private int fetchDistanceFromGitHub(@NonNull String repo, @NonNull String branch, @NonNull String hash)
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpURLConnection connection = (HttpURLConnection) URI.create("https://api.github.com/repos/" + repo + "/compare/" + branch + "..." + hash).toURL().openConnection();
|
||||
connection.connect();
|
||||
if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND)
|
||||
{
|
||||
return -2; // Unknown commit
|
||||
}
|
||||
if (connection.getResponseCode() == HttpURLConnection.HTTP_FORBIDDEN)
|
||||
{
|
||||
return -3; // Rate limited likely
|
||||
}
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)))
|
||||
{
|
||||
JsonObject obj = new Gson().fromJson(reader, JsonObject.class);
|
||||
String status = obj.get("status").getAsString();
|
||||
return switch (status)
|
||||
{
|
||||
case "identical" -> 0;
|
||||
case "behind" -> obj.get("behind_by").getAsInt();
|
||||
default -> -1;
|
||||
};
|
||||
}
|
||||
catch (JsonSyntaxException | NumberFormatException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
return -1;
|
||||
}
|
||||
this.channel = UpdateChannel.fromConfig(plugin.config.getString("updater.channel"));
|
||||
this.metadataClient = new UpdateMetadataClient(channel);
|
||||
}
|
||||
|
||||
// If verbose is 0, it will display nothing
|
||||
@@ -90,143 +43,183 @@ public class UpdateChecker
|
||||
// If verbose is 2, it will display all messages
|
||||
public boolean getUpdateStatusMessage(CommandSender sender, boolean cached, int verbosity)
|
||||
{
|
||||
if (BRANCH == null)
|
||||
try
|
||||
{
|
||||
PlexLog.error("You did not specify a branch to use for update checking. Defaulting to master.");
|
||||
BRANCH = "master";
|
||||
}
|
||||
// If it's -4, it hasn't checked for updates yet
|
||||
if (distance == -4)
|
||||
{
|
||||
distance = fetchDistanceFromGitHub(REPO, BRANCH, BuildInfo.getCommit());
|
||||
PlexLog.debug("Never checked for updates, checking now...");
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the request isn't asked to be cached, fetch it
|
||||
if (!cached)
|
||||
ArtifactMetadata metadata = fetchLatestPlexMetadata(cached);
|
||||
if (metadata.matchesCurrentBuild(plugin.getPluginMeta().getVersion(), BuildInfo.getNumber(), BuildInfo.getCommit()))
|
||||
{
|
||||
distance = fetchDistanceFromGitHub(REPO, BRANCH, BuildInfo.getCommit());
|
||||
PlexLog.debug("We have checked for updates before, but this request was not asked to be cached.");
|
||||
if (verbosity == 2)
|
||||
{
|
||||
sendMessage(sender, Component.text("Plex is up to date on the " + channel.id() + " channel.").color(NamedTextColor.GREEN));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
PlexLog.debug("We have checked for updates before, using cache.");
|
||||
}
|
||||
}
|
||||
|
||||
switch (distance)
|
||||
if (verbosity >= 1)
|
||||
{
|
||||
sendMessage(sender, Component.text("Plex " + metadata.version() + " is available on the " + channel.id() + " channel.", NamedTextColor.RED));
|
||||
sendMessage(sender, Component.text("Run: /plex update").color(NamedTextColor.RED));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (UpdateMetadataClient.MetadataException e)
|
||||
{
|
||||
case -1 ->
|
||||
if (verbosity == 2 || (verbosity >= 1 && !e.notFound()))
|
||||
{
|
||||
if (verbosity == 2)
|
||||
{
|
||||
sendMessage(sender, Component.text("There was an error checking for updates.").color(NamedTextColor.RED));
|
||||
}
|
||||
return false;
|
||||
sendMessage(sender, Component.text(updateMetadataErrorMessage(e)).color(NamedTextColor.RED));
|
||||
}
|
||||
case 0 ->
|
||||
if (!e.notFound())
|
||||
{
|
||||
if (verbosity == 2)
|
||||
PlexLog.error("Unable to check for updates: {0}", e.getMessage());
|
||||
if (e.getCause() != null)
|
||||
{
|
||||
sendMessage(sender, Component.text("Plex is up to date!").color(NamedTextColor.GREEN));
|
||||
e.getCause().printStackTrace();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case -2 ->
|
||||
{
|
||||
if (verbosity == 2)
|
||||
{
|
||||
sendMessage(sender, Component.text("Unknown version, unable to check for updates.").color(NamedTextColor.RED));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
default ->
|
||||
{
|
||||
if (verbosity >= 1)
|
||||
{
|
||||
sendMessage(sender, Component.text("Plex is not up to date!", NamedTextColor.RED));
|
||||
sendMessage(sender, Component.text("Download a new version at: " + DOWNLOAD_PAGE + "Plex").color(NamedTextColor.RED));
|
||||
sendMessage(sender, Component.text("Or run: /plex update").color(NamedTextColor.RED));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void updateJar(CommandSender sender, String name, boolean module)
|
||||
{
|
||||
AtomicReference<String> url = new AtomicReference<>(DOWNLOAD_PAGE + name);
|
||||
if (!module)
|
||||
{
|
||||
url.set(url.get() + "/job/" + BRANCH);
|
||||
}
|
||||
PlexLog.debug(url.toString());
|
||||
try
|
||||
{
|
||||
HttpURLConnection connection = (HttpURLConnection) URI.create(url + "/lastSuccessfulBuild/api/json").toURL().openConnection();
|
||||
int statusCode = connection.getResponseCode();
|
||||
ArtifactMetadata metadata = module
|
||||
? metadataClient.fetchModuleLatest(name, plugin.getApi().compatibility().version())
|
||||
: fetchLatestPlexMetadata(false);
|
||||
|
||||
if (statusCode == HttpURLConnection.HTTP_OK)
|
||||
if (!module && metadata.matchesCurrentBuild(plugin.getPluginMeta().getVersion(), BuildInfo.getNumber(), BuildInfo.getCommit()))
|
||||
{
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)))
|
||||
sendMessage(sender, PlexUtils.mmDeserialize("<red>Plex is already up to date on the " + channel.id() + " channel."));
|
||||
return;
|
||||
}
|
||||
|
||||
File copyTo = module
|
||||
? new File(plugin.getModulesFolder(), metadata.fileName())
|
||||
: new File(Bukkit.getUpdateFolderFile(), metadata.fileName());
|
||||
|
||||
sendMessage(sender, PlexUtils.mmDeserialize("<green>Downloading latest JAR file: " + metadata.fileName()));
|
||||
plugin.getApi().scheduler().runAsync(() -> downloadAndInstall(sender, metadata, copyTo));
|
||||
}
|
||||
catch (UpdateMetadataClient.MetadataException e)
|
||||
{
|
||||
sendMessage(sender, PlexUtils.mmDeserialize("<red>" + updateMetadataErrorMessage(e)));
|
||||
if (!e.notFound())
|
||||
{
|
||||
PlexLog.error("Unable to update {0}: {1}", name, e.getMessage());
|
||||
if (e.getCause() != null)
|
||||
{
|
||||
JsonObject object = new Gson().fromJson(reader, JsonObject.class);
|
||||
JsonObject artifact = object.getAsJsonArray("artifacts").get(module ? 0 : 1).getAsJsonObject();
|
||||
String jarFile = artifact.get("fileName").getAsString();
|
||||
sendMessage(sender, PlexUtils.mmDeserialize("<green>Downloading latest JAR file: " + jarFile));
|
||||
File copyTo;
|
||||
if (!module)
|
||||
{
|
||||
copyTo = new File(Bukkit.getUpdateFolderFile(), jarFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
copyTo = new File(plugin.getModulesFolder().getPath(), jarFile);
|
||||
}
|
||||
plugin.getApi().scheduler().runAsync(() ->
|
||||
{
|
||||
try
|
||||
{
|
||||
FileUtils.copyURLToFile(
|
||||
URI.create(url + "/lastSuccessfulBuild/artifact/build/libs/" + jarFile).toURL(),
|
||||
copyTo
|
||||
);
|
||||
sendMessage(sender, PlexUtils.mmDeserialize("<green>New JAR file downloaded successfully."));
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (JsonSyntaxException | NumberFormatException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
e.getCause().printStackTrace();
|
||||
}
|
||||
}
|
||||
else if (statusCode == HttpURLConnection.HTTP_NOT_FOUND)
|
||||
{
|
||||
sendMessage(sender, PlexUtils.mmDeserialize("<red>Could not update " + name + " as it can't be found on Jenkins."));
|
||||
}
|
||||
else
|
||||
{
|
||||
sendMessage(sender, PlexUtils.mmDeserialize("<red>Something went wrong while trying to update " + name + ". Please check the log for more information."));
|
||||
PlexLog.error("Unable to update module {0} due to unexpected status code returned from Jenkins - Status Code: {1}", name, statusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ArtifactMetadata fetchLatestPlexMetadata(boolean cached) throws UpdateMetadataClient.MetadataException
|
||||
{
|
||||
if (!cached || latestPlexMetadata == null)
|
||||
{
|
||||
latestPlexMetadata = metadataClient.fetchPlexLatest(BuildInfo.getMinecraftVersion());
|
||||
}
|
||||
return latestPlexMetadata;
|
||||
}
|
||||
|
||||
private void downloadAndInstall(CommandSender sender, ArtifactMetadata metadata, File copyTo)
|
||||
{
|
||||
File parent = copyTo.getParentFile();
|
||||
if (parent != null && !parent.exists() && !parent.mkdirs())
|
||||
{
|
||||
sendMessage(sender, PlexUtils.mmDeserialize("<red>Unable to create update directory: " + parent.getAbsolutePath()));
|
||||
return;
|
||||
}
|
||||
|
||||
File temporaryFile = new File(parent, copyTo.getName() + ".download");
|
||||
try
|
||||
{
|
||||
download(metadata.downloadUrl(), temporaryFile);
|
||||
validateDownloadedFile(metadata, temporaryFile);
|
||||
Files.move(temporaryFile.toPath(), copyTo.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
sendMessage(sender, PlexUtils.mmDeserialize("<green>New JAR file downloaded and verified successfully."));
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
sendMessage(sender, PlexUtils.mmDeserialize("<red>Something went wrong while downloading " + metadata.name() + ". Please check the log for more information."));
|
||||
PlexLog.error("Unable to download update {0}: {1}", metadata.name(), e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
catch (JSONException e)
|
||||
finally
|
||||
{
|
||||
sendMessage(sender, PlexUtils.mmDeserialize("<red>Something went wrong while trying to gather information from Jenkins for " + name + ". Please check the log for more information"));
|
||||
PlexLog.error("Unable to parse JSON information received from Jenkins - see below for more information...");
|
||||
e.printStackTrace();
|
||||
try
|
||||
{
|
||||
Files.deleteIfExists(temporaryFile.toPath());
|
||||
}
|
||||
catch (IOException ignored)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void download(String downloadUrl, File destination) throws IOException
|
||||
{
|
||||
HttpURLConnection connection = (HttpURLConnection) URI.create(downloadUrl).toURL().openConnection();
|
||||
connection.setConnectTimeout(15000);
|
||||
connection.setReadTimeout(30000);
|
||||
|
||||
int statusCode = connection.getResponseCode();
|
||||
if (statusCode != HttpURLConnection.HTTP_OK)
|
||||
{
|
||||
throw new IOException("download request returned HTTP " + statusCode);
|
||||
}
|
||||
|
||||
try (InputStream inputStream = connection.getInputStream())
|
||||
{
|
||||
Files.copy(inputStream, destination.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDownloadedFile(ArtifactMetadata metadata, File file) throws IOException
|
||||
{
|
||||
if (metadata.size() != null && metadata.size() >= 0 && Files.size(file.toPath()) != metadata.size())
|
||||
{
|
||||
throw new IOException("downloaded file size did not match metadata size");
|
||||
}
|
||||
|
||||
String actualSha256 = sha256(file);
|
||||
if (!metadata.sha256().equalsIgnoreCase(actualSha256))
|
||||
{
|
||||
throw new IOException("downloaded file SHA-256 did not match metadata SHA-256");
|
||||
}
|
||||
}
|
||||
|
||||
private String sha256(File file) throws IOException
|
||||
{
|
||||
MessageDigest digest;
|
||||
try
|
||||
{
|
||||
digest = MessageDigest.getInstance("SHA-256");
|
||||
}
|
||||
catch (NoSuchAlgorithmException e)
|
||||
{
|
||||
throw new IllegalStateException("SHA-256 is not available", e);
|
||||
}
|
||||
|
||||
try (InputStream inputStream = Files.newInputStream(file.toPath());
|
||||
DigestInputStream digestInputStream = new DigestInputStream(inputStream, digest))
|
||||
{
|
||||
digestInputStream.transferTo(OutputStream.nullOutputStream());
|
||||
}
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
}
|
||||
|
||||
private String updateMetadataErrorMessage(UpdateMetadataClient.MetadataException e)
|
||||
{
|
||||
if (e.notFound())
|
||||
{
|
||||
return "No compatible update is available on the " + channel.id() + " channel.";
|
||||
}
|
||||
return "There was an error checking update metadata: " + e.getMessage();
|
||||
}
|
||||
|
||||
private void sendMessage(CommandSender sender, Component message)
|
||||
{
|
||||
if (sender instanceof Player player)
|
||||
|
||||
@@ -5,3 +5,5 @@ buildNumber={{ buildNumber | default("unknown") }}
|
||||
date={{ date }}
|
||||
|
||||
gitCommit={{ gitCommit | default("unknown") }}
|
||||
|
||||
minecraftVersion={{ minecraftVersion | default("unknown") }}
|
||||
|
||||
@@ -258,11 +258,10 @@ worlds:
|
||||
stone: 16
|
||||
bedrock: 1
|
||||
|
||||
# If you are running a custom fork of Plex, you may wish to check for updates from a different repository.
|
||||
update_repo: "plexusorg/Plex"
|
||||
|
||||
# What branch should Plex fetch updates from?
|
||||
update_branch: "master"
|
||||
# Static updater metadata. The metadata can be hosted as plain JSON on Cloudflare Pages.
|
||||
updater:
|
||||
# Update channel to use. stable only serves non-SNAPSHOT releases; dev serves development builds.
|
||||
channel: "stable"
|
||||
|
||||
# Additional logging for debugging
|
||||
debug: false
|
||||
|
||||
Reference in New Issue
Block a user