9
0
mirror of https://github.com/LeavesMC/Leaves.git synced 2025-12-19 14:59:32 +00:00
Files
LeavesMC/patches/server/0095-Leaves-Updater.patch
2024-07-12 14:34:28 +08:00

323 lines
14 KiB
Diff

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: violetc <58360096+s-yh-china@users.noreply.github.com>
Date: Mon, 31 Jul 2023 17:42:25 +0800
Subject: [PATCH] Leaves Updater
---------
Co-authored-by: MC_XiaoHei <xiaohei.xor7studio@foxmail.com>
diff --git a/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java b/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java
index ebd62b1d3a60d3e22e3849047293e1f62533eae7..e964f05c5c0f2d1d9b4c0a3459076e4b95f0f4e8 100644
--- a/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java
+++ b/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java
@@ -33,6 +33,7 @@ public final class LeavesCommand extends Command {
private static final Map<String, LeavesSubcommand> SUBCOMMANDS = Util.make(() -> {
final Map<Set<String>, LeavesSubcommand> commands = new HashMap<>();
commands.put(Set.of("config"), new ConfigCommand());
+ commands.put(Set.of("update"), new UpdateCommand());
return commands.entrySet().stream()
.flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue())))
diff --git a/src/main/java/org/leavesmc/leaves/command/subcommands/UpdateCommand.java b/src/main/java/org/leavesmc/leaves/command/subcommands/UpdateCommand.java
new file mode 100644
index 0000000000000000000000000000000000000000..7f94df607e8ffd48ab2cb7c90d520c2b4a906cdc
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/command/subcommands/UpdateCommand.java
@@ -0,0 +1,21 @@
+package org.leavesmc.leaves.command.subcommands;
+
+import org.bukkit.ChatColor;
+import org.bukkit.command.CommandSender;
+import org.leavesmc.leaves.command.LeavesSubcommand;
+import org.leavesmc.leaves.util.LeavesUpdateHelper;
+
+public class UpdateCommand implements LeavesSubcommand {
+
+ @Override
+ public boolean execute(CommandSender sender, String subCommand, String[] args) {
+ sender.sendMessage(ChatColor.GRAY + "Trying to update Leaves, see the console for more info.");
+ LeavesUpdateHelper.tryUpdateLeaves();
+ return true;
+ }
+
+ @Override
+ public boolean tabCompletes() {
+ return false;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/util/LeavesUpdateHelper.java b/src/main/java/org/leavesmc/leaves/util/LeavesUpdateHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..8620305f3330a856ad7445259f565bf8e281ea52
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/util/LeavesUpdateHelper.java
@@ -0,0 +1,268 @@
+package org.leavesmc.leaves.util;
+
+import com.google.common.base.Charsets;
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSyntaxException;
+import net.minecraft.Util;
+import org.bukkit.Bukkit;
+import org.jetbrains.annotations.NotNull;
+import org.leavesmc.leaves.LeavesConfig;
+import org.leavesmc.leaves.LeavesLogger;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.channels.Channels;
+import java.nio.channels.FileChannel;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.MessageDigest;
+import java.time.Duration;
+import java.time.LocalTime;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantLock;
+
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
+import static java.nio.file.StandardOpenOption.WRITE;
+
+public class LeavesUpdateHelper {
+
+ private final static String autoUpdateDir = "auto_update";
+ private final static String corePathFileName = autoUpdateDir + File.separator + "core.path";
+
+ private final static ReentrantLock updateLock = new ReentrantLock();
+ private static boolean updateTaskStarted = false;
+
+ private static final ScheduledExecutorService autoUpdateExecutor = Executors.newScheduledThreadPool(1);
+
+ public static void init() {
+ File workingDirFile = new File(autoUpdateDir);
+ if (!workingDirFile.exists()) {
+ if (!workingDirFile.mkdir()) {
+ LeavesLogger.LOGGER.warning("Failed to create working directory: " + autoUpdateDir);
+ }
+ }
+
+ File corePathFile = new File(corePathFileName);
+ if (!corePathFile.exists()) {
+ try {
+ if (!corePathFile.createNewFile()) {
+ throw new IOException();
+ }
+ } catch (IOException e) {
+ LeavesLogger.LOGGER.severe("Failed to create core path file: " + corePathFileName, e);
+ }
+ }
+
+ File leavesUpdateDir = new File(autoUpdateDir + File.separator + "leaves");
+ if (!leavesUpdateDir.exists()) {
+ if (!leavesUpdateDir.mkdir()) {
+ LeavesLogger.LOGGER.warning("Failed to create leaves update directory: " + leavesUpdateDir);
+ }
+ }
+
+ if (LeavesConfig.autoUpdate) {
+ LocalTime currentTime = LocalTime.now();
+ long dailyTaskPeriod = 24 * 60 * 60 * 1000;
+
+ for (String time : LeavesConfig.autoUpdateTime) {
+ try {
+ LocalTime taskTime = LocalTime.of(Integer.parseInt(time.split(":")[0]), Integer.parseInt(time.split(":")[1]));
+ Duration task = Duration.between(currentTime, taskTime);
+ if (task.isNegative()) {
+ task = task.plusDays(1);
+ }
+ autoUpdateExecutor.scheduleAtFixedRate(LeavesUpdateHelper::tryUpdateLeaves, task.toMillis(), dailyTaskPeriod, TimeUnit.MILLISECONDS);
+ } catch (Exception ignored) {
+ LeavesLogger.LOGGER.warning("Illegal auto-update time ignored: " + time);
+ }
+ }
+ }
+ }
+
+ public static void tryUpdateLeaves() {
+ updateLock.lock();
+ try {
+ if (!updateTaskStarted) {
+ updateTaskStarted = true;
+ new Thread(LeavesUpdateHelper::downloadLeaves).start();
+ }
+ } finally {
+ updateLock.unlock();
+ }
+ }
+
+ private static void downloadLeaves() {
+ String minecraftVersion = Bukkit.getMinecraftVersion();
+ String version = Bukkit.getVersion();
+
+ if (version.startsWith("null")) {
+ LeavesLogger.LOGGER.info("IDE? Can not update!");
+ updateTaskStarted = false;
+ return;
+ }
+
+ String gitHash = version.substring("git-Leaves-".length()).split("[-\\s]")[0].replaceAll("\"", "");
+
+ LeavesLogger.LOGGER.info("Trying to get latest build info.");
+ LeavesBuildInfo buildInfo = getLatestBuildInfo(minecraftVersion, gitHash);
+
+ if (buildInfo != LeavesBuildInfo.NULL) {
+ if (!buildInfo.needUpdate) {
+ LeavesLogger.LOGGER.warning("You are running the latest version, stopping update.");
+ updateTaskStarted = false;
+ return;
+ }
+
+ LeavesLogger.LOGGER.info("Got build info, trying to download " + buildInfo.fileName);
+ try {
+ Path outFile = Path.of(autoUpdateDir, "leaves", buildInfo.fileName + ".cache");
+ Files.deleteIfExists(outFile);
+
+ boolean downloadFlag = false;
+ URL cdnUrl = new URI(buildInfo.cdnUrl).toURL();
+ try (
+ final ReadableByteChannel source = Channels.newChannel(cdnUrl.openStream());
+ final FileChannel fileChannel = FileChannel.open(outFile, CREATE, WRITE, TRUNCATE_EXISTING)
+ ) {
+ fileChannel.transferFrom(source, 0, Long.MAX_VALUE);
+ downloadFlag = true;
+ } catch (final IOException e) {
+ Files.deleteIfExists(outFile);
+ }
+
+ if (!downloadFlag) {
+ URL githubUrl = new URI(buildInfo.githubUrl).toURL();
+ try (
+ final ReadableByteChannel source = Channels.newChannel(githubUrl.openStream());
+ final FileChannel fileChannel = FileChannel.open(outFile, CREATE, WRITE, TRUNCATE_EXISTING)
+ ) {
+ fileChannel.transferFrom(source, 0, Long.MAX_VALUE);
+ } catch (final IOException e) {
+ LeavesLogger.LOGGER.warning("Download " + buildInfo.fileName + " failed.", e);
+ Files.deleteIfExists(outFile);
+ updateTaskStarted = false;
+ return;
+ }
+ }
+
+ LeavesLogger.LOGGER.info("Download " + buildInfo.fileName + " completed.");
+ if (!isFileValid(outFile, buildInfo.sha256)) {
+ LeavesLogger.LOGGER.warning("Hash check failed for downloaded file " + buildInfo.fileName);
+ Files.deleteIfExists(outFile);
+ updateTaskStarted = false;
+ return;
+ }
+
+ File nowServerCore = new File(autoUpdateDir + File.separator + "leaves" + File.separator + buildInfo.fileName);
+ File backupServerCore = new File(autoUpdateDir + File.separator + "leaves" + File.separator + buildInfo.fileName + ".old");
+ Util.safeReplaceFile(nowServerCore.toPath(), outFile, backupServerCore.toPath());
+
+ try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(corePathFileName))) {
+ bufferedWriter.write(autoUpdateDir + File.separator + "leaves" + File.separator + buildInfo.fileName);
+ } catch (IOException e) {
+ LeavesLogger.LOGGER.warning("Fail to download leaves core", e);
+ updateTaskStarted = false;
+ return;
+ }
+
+ LeavesLogger.LOGGER.info("Leaves update completed, please restart your server.");
+ } catch (Exception e) {
+ LeavesLogger.LOGGER.severe("Leaves update failed", e);
+ }
+ } else {
+ LeavesLogger.LOGGER.warning("Can't get build info, stopping update.");
+ }
+ updateTaskStarted = false;
+ }
+
+ private static boolean isFileValid(Path file, String hash) {
+ try (FileInputStream inputStream = new FileInputStream(file.toFile())) {
+ byte[] buffer = new byte[1024];
+ MessageDigest md5 = MessageDigest.getInstance("SHA-256");
+
+ for (int numRead; (numRead = inputStream.read(buffer)) > 0; ) {
+ md5.update(buffer, 0, numRead);
+ }
+
+ return toHexString(md5.digest()).equals(hash);
+ } catch (Exception e) {
+ LeavesLogger.LOGGER.warning("Fail to validate file " + file, e);
+ }
+ return false;
+ }
+
+ @NotNull
+ private static String toHexString(byte @NotNull [] bytes) {
+ StringBuilder builder = new StringBuilder();
+ for (byte b : bytes) {
+ builder.append(String.format("%02x", b));
+ }
+ return builder.toString();
+ }
+
+ private static LeavesBuildInfo getLatestBuildInfo(String mcVersion, String gitHash) {
+ try {
+ HttpURLConnection connection = (HttpURLConnection) new URI("https://api.leavesmc.org/projects/leaves/versions/" + mcVersion + "/builds/latest").toURL().openConnection();
+ connection.connect();
+ boolean useApiV2 = false;
+ if (connection.getResponseCode() / 100 != 2) {
+ connection = (HttpURLConnection) new URI("https://api.leavesmc.org/v2/projects/leaves/versions/" + mcVersion + "/builds/latest").toURL().openConnection();
+ connection.connect();
+ useApiV2 = true;
+ }
+ if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) return LeavesBuildInfo.NULL;
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8))) {
+ JsonObject obj = new Gson().fromJson(reader, JsonObject.class);
+ String channel = obj.get("channel").getAsString();
+
+ if (channel.equals("default")) {
+ int build = obj.get("build").getAsInt();
+
+ JsonArray changes = obj.get("changes").getAsJsonArray();
+ boolean needUpdate = true;
+ for (JsonElement change : changes) {
+ if (change.getAsJsonObject().get("commit").getAsString().startsWith(gitHash)) {
+ needUpdate = false;
+ break;
+ }
+ }
+
+ JsonObject downloadInfo = obj.get("downloads").getAsJsonObject().get("application").getAsJsonObject();
+ String name = downloadInfo.get("name").getAsString();
+ String sha256 = downloadInfo.get("sha256").getAsString();
+ String githubUrl = useApiV2 ? "https://api.leavesmc.org/v2/projects/leaves/versions/" + mcVersion + "/builds/" + build + "/downloads/" + name : downloadInfo.get("url").getAsString();
+ String cdnUrl = useApiV2 ? "https://cdn.leavesmc.z0z0r4.top/cache/" + name : downloadInfo.get("cdn_url").getAsString();
+ return new LeavesBuildInfo(build, name, sha256, needUpdate, cdnUrl, githubUrl);
+ } else {
+ return LeavesBuildInfo.NULL;
+ }
+ } catch (JsonSyntaxException | NumberFormatException e) {
+ LeavesLogger.LOGGER.warning("Fail to get latest build info", e);
+ return LeavesBuildInfo.NULL;
+ }
+ } catch (IOException | URISyntaxException e) {
+ LeavesLogger.LOGGER.warning("Fail to get latest build info", e);
+ return LeavesBuildInfo.NULL;
+ }
+ }
+
+ private record LeavesBuildInfo(int build, String fileName, String sha256, boolean needUpdate, String cdnUrl, String githubUrl) {
+ public static LeavesBuildInfo NULL = new LeavesBuildInfo(-1, null, null, false, null, null);
+ }
+}