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 update command diff --git a/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java b/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java index b9e2bdb034dcd5119680ddea44a1eda11a0189b2..facd180da9b7025cf7ae192cbe91317ff4b70472 100644 --- a/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java +++ b/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java @@ -13,6 +13,7 @@ import org.bukkit.plugin.PluginManager; import org.checkerframework.checker.nullness.qual.Nullable; import org.jetbrains.annotations.NotNull; import org.leavesmc.leaves.command.subcommands.ConfigCommand; +import org.leavesmc.leaves.command.subcommands.UpdateCommand; import java.util.ArrayList; import java.util.Arrays; @@ -33,6 +34,7 @@ public final class LeavesCommand extends Command { private static final Map SUBCOMMANDS = Util.make(() -> { final Map, 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..06ca57f0cb319a254ca1511e60f05e5ca151beea --- /dev/null +++ b/src/main/java/org/leavesmc/leaves/util/LeavesUpdateHelper.java @@ -0,0 +1,240 @@ +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.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.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.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; + + public static void initAutoUpdate() { + File workingDirFile = new File(autoUpdateDir); + if (!workingDirFile.exists()) { + workingDirFile.mkdir(); + } + + File corePathFile = new File(corePathFileName); + if (!corePathFile.exists()) { + try { + corePathFile.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + File leavesUpdateDir = new File(autoUpdateDir + File.separator + "leaves"); + if (!leavesUpdateDir.exists()) { + leavesUpdateDir.mkdir(); + } + } + + public static void tryUpdateLeaves() { + updateLock.lock(); + try { + if (!updateTaskStarted) { + updateTaskStarted = true; + new Thread(new Runnable() { + @Override + public void run() { + 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?"); + 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 URL(buildInfo.cdnUrl); + 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 URL(buildInfo.githubUrl); + 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."); + Files.deleteIfExists(outFile); + updateTaskStarted = false; + e.printStackTrace(); + 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) { + e.printStackTrace(); + updateTaskStarted = false; + return; + } + + LeavesLogger.LOGGER.info("Leaves update completed, please restart your server."); + } catch (Exception e) { + e.printStackTrace(); + } + } 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) { + e.printStackTrace(); + } + 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 URL("https://api.leavesmc.org/projects/leaves/versions/" + mcVersion + "/builds/latest").openConnection(); + connection.connect(); + boolean useApiV2 = false; + if (connection.getResponseCode() / 100 != 2){ + connection = (HttpURLConnection) new URL("https://api.leavesmc.org/v2/projects/leaves/versions/" + mcVersion + "/builds/latest").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) { + e.printStackTrace(); + return LeavesBuildInfo.NULL; + } + } catch (IOException e) { + e.printStackTrace(); + 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); + } +}