From 720e8ec8eeb69d24afbb6b14068563cde2d470f0 Mon Sep 17 00:00:00 2001 From: Keuin Date: Thu, 23 Apr 2020 14:51:29 +0800 Subject: Version 1.1.0-dev: - Optimized backup lag (using async I/O). - Added twice confirmation /kb confirm and cancellation /kb cancel, to avoid mistake. - Added countdown before restoring. - Adjusted some text. - Code optimization. --- README.md | 7 +- gradle.properties | 2 +- .../com/keuin/kbackupfabric/KBCommandHandler.java | 141 +++++++-------------- .../com/keuin/kbackupfabric/KBCommandRegister.java | 18 ++- src/main/java/com/keuin/kbackupfabric/KBMain.java | 2 +- .../keuin/kbackupfabric/KBPluginInitializer.java | 2 +- .../kbackupfabric/util/BackupFilesystemUtil.java | 53 ++++++++ src/main/java/com/keuin/kbackupfabric/util/IO.java | 54 -------- .../kbackupfabric/util/PermissionValidator.java | 9 ++ .../util/PostProgressRestoreThread.java | 71 ----------- .../com/keuin/kbackupfabric/util/PrintUtil.java | 57 +++++++++ .../keuin/kbackupfabric/util/ReflectionUtils.java | 4 +- .../com/keuin/kbackupfabric/util/WorldUtil.java | 15 --- .../java/com/keuin/kbackupfabric/util/ZipUtil.java | 2 +- .../keuin/kbackupfabric/worker/BackupWorker.java | 92 ++++++++++++++ .../keuin/kbackupfabric/worker/RestoreWorker.java | 90 +++++++++++++ 16 files changed, 371 insertions(+), 248 deletions(-) create mode 100644 src/main/java/com/keuin/kbackupfabric/util/BackupFilesystemUtil.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/IO.java create mode 100644 src/main/java/com/keuin/kbackupfabric/util/PermissionValidator.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/PostProgressRestoreThread.java create mode 100644 src/main/java/com/keuin/kbackupfabric/util/PrintUtil.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/WorldUtil.java create mode 100644 src/main/java/com/keuin/kbackupfabric/worker/BackupWorker.java create mode 100644 src/main/java/com/keuin/kbackupfabric/worker/RestoreWorker.java diff --git a/README.md b/README.md index 50107ab..a774cbc 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ commands: To-Do List: -- Optimize lag during the backup process (use async I/O) -- Optimize output format -- Implement incremental backup +- Optimize output format. +- Add backup name completion. +- Optimize the backup name output. (add datetime output automatically, name is essential and is separated from the datetime) +- Implement incremental backup. \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 7b5f0c9..e6e62df 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ minecraft_version=1.14.4 yarn_mappings=1.14.4+build.16 loader_version=0.8.2+build.194 # Mod Properties -mod_version=1.0.0 +mod_version=1.1.0-dev maven_group=com.keuin.kbackupfabric archives_base_name=kbackup-fabric # Dependencies diff --git a/src/main/java/com/keuin/kbackupfabric/KBCommandHandler.java b/src/main/java/com/keuin/kbackupfabric/KBCommandHandler.java index 33d0f43..943fb68 100644 --- a/src/main/java/com/keuin/kbackupfabric/KBCommandHandler.java +++ b/src/main/java/com/keuin/kbackupfabric/KBCommandHandler.java @@ -1,32 +1,28 @@ package com.keuin.kbackupfabric; -import com.keuin.kbackupfabric.util.PostProgressRestoreThread; -import com.keuin.kbackupfabric.util.ZipUtil; -import com.keuin.kbackupfabric.util.ZipUtilException; +import com.keuin.kbackupfabric.worker.BackupWorker; +import com.keuin.kbackupfabric.worker.RestoreWorker; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; import net.minecraft.server.MinecraftServer; import net.minecraft.server.command.ServerCommandSource; -import net.minecraft.world.World; import java.io.File; -import java.io.IOException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.HashMap; -import java.util.Map; -import static com.keuin.kbackupfabric.util.IO.debug; -import static com.keuin.kbackupfabric.util.IO.message; +import static com.keuin.kbackupfabric.util.BackupFilesystemUtil.*; +import static com.keuin.kbackupfabric.util.PrintUtil.debug; +import static com.keuin.kbackupfabric.util.PrintUtil.message; -public class KBCommandHandler { +public final class KBCommandHandler { private static final int SUCCESS = 1; private static final int FAILED = -1; - private static final String backupSaveDirectoryName = "backups"; - private static final String backupFileNamePrefix = "kbackup-"; + private static final HashMap backupIndexNameMapper = new HashMap<>(); // index -> backupName private static String restoreBackupNameToBeConfirmed = null; @@ -37,11 +33,13 @@ public class KBCommandHandler { * @return stat code. */ public static int help(CommandContext context) { - message(context, "KBackup Manual"); - message(context, "/kb | /kb help Print help menu."); + message(context, "==== KBackup Manual ===="); + message(context, "/kb /kb help Print help menu."); message(context, "/kb list Show all backups."); message(context, "/kb backup [backup_name] Backup world, nether, end to backup_name. By default, the name is current system time."); message(context, "/kb restore Delete current three worlds, restore the older version from given backup. By default, this command is identical with /kb list."); + message(context, "/kb confirm Confirm and start restoring."); + message(context, "/kb cancel Cancel the restoration to be confirmed. If cancelled, /kb confirm will not effect without another valid /kb restore command."); return SUCCESS; } @@ -49,7 +47,7 @@ public class KBCommandHandler { message(context, "Available backups: (file is not checked, manipulation may affect this plugin)"); MinecraftServer server = context.getSource().getMinecraftServer(); File[] files = getBackupSaveDirectory(server).listFiles( - (dir, name) -> dir.isDirectory() && name.toLowerCase().endsWith(".zip") && name.toLowerCase().startsWith(backupFileNamePrefix) + (dir, name) -> dir.isDirectory() && name.toLowerCase().endsWith(".zip") && name.toLowerCase().startsWith(getBackupFileNamePrefix()) ); backupIndexNameMapper.clear(); if (files != null) { @@ -90,6 +88,7 @@ public class KBCommandHandler { */ public static int restore(CommandContext context) { //KBMain.restore("name") + MinecraftServer server = context.getSource().getMinecraftServer(); String backupName = StringArgumentType.getString(context, "backupName"); if (backupName.matches("[0-9]*")) { @@ -102,6 +101,13 @@ public class KBCommandHandler { backupName = realBackupName; // Replace input number with real backup name. } + // Validate backupName + if (!isBackupNameValid(backupName, server)) { + // Invalid backupName + message(context, "Invalid backup name! Please check your input. The list index number is also valid.", false); + return FAILED; + } + // Update confirm pending variable restoreBackupNameToBeConfirmed = backupName; message(context, String.format("WARNING: You will LOST YOUR CURRENT WORLD COMPLETELY! It will be replaced with the backup %s . Please use /kb confirm to proceed executing.", restoreBackupNameToBeConfirmed), true); @@ -123,74 +129,8 @@ public class KBCommandHandler { } private static int doBackup(CommandContext context, String backupName) { - String destPathFolderName = ""; - try { - message(context, String.format("Making backup %s, please wait ...", backupName), true); - Map oldWorldsSavingDisabled = new HashMap<>(); // old switch stat - - // Get server - MinecraftServer server = context.getSource().getMinecraftServer(); - - // Save old autosave switch stat temporally - server.getWorlds().forEach(world -> { - oldWorldsSavingDisabled.put(world, world.savingDisabled); - world.savingDisabled = true; - }); - - // Force to save all player data and worlds - debug("Saving players ..."); - server.getPlayerManager().saveAllPlayerData(); - debug("Saving worlds ..."); - server.save(true, true, true); - - //// Do our main backup logic - - // Create backup saving directory - File destPathFile = getBackupSaveDirectory(server); - destPathFolderName = destPathFile.getName(); - if (!destPathFile.mkdir() && !destPathFile.isDirectory()) { - message(context, String.format("Failed to create backup saving directory: %s. Failed to backup.", destPathFolderName)); - return FAILED; - } - - // Make zip - String levelPath = getLevelPath(server); - debug(String.format("zip(srcPath=%s, destPath=%s)", levelPath, destPathFile.toString())); - ZipUtil.zip(levelPath, destPathFile.toString(), getBackupFileName(backupName)); - - // Restore old autosave switch stat - server.getWorlds().forEach(world -> world.savingDisabled = oldWorldsSavingDisabled.getOrDefault(world, true)); - - message(context, "Done.", true); - return SUCCESS; - } catch (SecurityException e) { - message(context, String.format("Failed to create backup saving directory: %s. Failed to backup.", destPathFolderName)); - return FAILED; - } catch (IOException | ZipUtilException e) { - message(context, "Failed to make zip: " + e.getMessage()); - return FAILED; - } - } - - private static String getBackupFileName(String backupName) { - return backupFileNamePrefix + backupName + ".zip"; - } - - private static String getBackupName(String backupFileName) { - try { - if (backupFileName.matches(backupFileNamePrefix + ".+\\.zip")) - return backupFileName.substring(backupFileNamePrefix.length(), backupFileName.length() - 4); - } catch (IndexOutOfBoundsException ignored) { - } - return backupFileName; - } - - private static File getBackupSaveDirectory(MinecraftServer server) { - return new File(server.getRunDirectory(), backupSaveDirectoryName); - } - - private static String getLevelPath(MinecraftServer server) { - return (new File(server.getRunDirectory(), server.getLevelName())).getAbsolutePath(); + BackupWorker.invoke(context, backupName); + return SUCCESS; } /** @@ -214,18 +154,35 @@ public class KBCommandHandler { String backupFileName = getBackupFileName(backupName); debug("Backup file name: " + backupFileName); File backupFile = new File(getBackupSaveDirectory(server), backupFileName); - PostProgressRestoreThread postProgressRestoreThread = new PostProgressRestoreThread(server.getThread(), backupFile.getPath(), getLevelPath(server)); - Thread postThread = new Thread(postProgressRestoreThread, "PostProgressRestoreThread"); - postThread.start(); - server.stop(false); - message(context, "Decompressing archive data. Server will shutdown to replace level data. Please do not restart the server.", true); + message(context, "Server will shutdown in a few seconds, depended on your world size and the disk speed, the restore progress may take seconds or minutes.", true); + message(context, "Please do not force the server stop, or the level would be broken.", true); + message(context, "After it shuts down, please restart the server manually.", true); + final int WAIT_SECONDS = 10; + for (int i = 0; i < WAIT_SECONDS; ++i) { + try { + Thread.sleep(1000); + } catch (InterruptedException ignored) { + } + } + message(context, "Shutting down ...", true); + RestoreWorker.invoke(server, backupFile.getPath(), getLevelPath(server)); return SUCCESS; } - static boolean opPermissionValidator(ServerCommandSource commandSource) { - return commandSource.hasPermissionLevel(4); + /** + * Cancel the execution to be confirmed. + * + * @param context the context. + * @return stat code. + */ + public static int cancel(CommandContext context) { + if (restoreBackupNameToBeConfirmed != null) { + restoreBackupNameToBeConfirmed = null; + message(context, "The restoration is cancelled.", true); + return SUCCESS; + } else { + message(context, "Nothing to cancel."); + return FAILED; + } } - - - } diff --git a/src/main/java/com/keuin/kbackupfabric/KBCommandRegister.java b/src/main/java/com/keuin/kbackupfabric/KBCommandRegister.java index d4823c4..0407520 100644 --- a/src/main/java/com/keuin/kbackupfabric/KBCommandRegister.java +++ b/src/main/java/com/keuin/kbackupfabric/KBCommandRegister.java @@ -1,11 +1,12 @@ package com.keuin.kbackupfabric; +import com.keuin.kbackupfabric.util.PermissionValidator; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; import net.minecraft.server.command.CommandManager; import net.minecraft.server.command.ServerCommandSource; -public class KBCommandRegister { +public final class KBCommandRegister { // First make method to register public static void register(CommandDispatcher dispatcher) { @@ -13,17 +14,20 @@ public class KBCommandRegister { dispatcher.register(CommandManager.literal("kb").executes(KBCommandHandler::help)); dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("help").executes(KBCommandHandler::help))); - // register /kb list for showing the backup list - dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("list").executes(KBCommandHandler::list))); + // register /kb list for showing the backup list. OP is required. + dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("list").requires(PermissionValidator::op).executes(KBCommandHandler::list))); // register /kb backup [name] for performing backup. OP is required. - dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("backup").then(CommandManager.argument("backupName", StringArgumentType.string()).requires(KBCommandHandler::opPermissionValidator).executes(KBCommandHandler::backup)).requires(KBCommandHandler::opPermissionValidator).executes(KBCommandHandler::backupWithDefaultName))); + dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("backup").then(CommandManager.argument("backupName", StringArgumentType.string()).requires(PermissionValidator::op).executes(KBCommandHandler::backup)).requires(PermissionValidator::op).executes(KBCommandHandler::backupWithDefaultName))); // register /kb restore for performing restore. OP is required. - dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("restore").then(CommandManager.argument("backupName", StringArgumentType.string()).requires(KBCommandHandler::opPermissionValidator).executes(KBCommandHandler::restore)).executes(KBCommandHandler::list))); + dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("restore").then(CommandManager.argument("backupName", StringArgumentType.string()).requires(PermissionValidator::op).executes(KBCommandHandler::restore)).executes(KBCommandHandler::list))); - // register /kb confirm for confirming the execution - dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("confirm").executes(KBCommandHandler::confirm))); + // register /kb confirm for confirming the execution. OP is required. + dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("confirm").requires(PermissionValidator::op).executes(KBCommandHandler::confirm))); + + // register /kb cancel for cancelling the execution to be confirmed. OP is required. + dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("cancel").requires(PermissionValidator::op).executes(KBCommandHandler::cancel))); // LiteralCommandNode basenode = dispatcher.register(literal("findBiome") // .then(argument("biome_identifier", identifier()).suggests(BiomeCompletionProvider.BIOMES) // We use Biome suggestions for identifier argument diff --git a/src/main/java/com/keuin/kbackupfabric/KBMain.java b/src/main/java/com/keuin/kbackupfabric/KBMain.java index f2a3789..f8a61f1 100644 --- a/src/main/java/com/keuin/kbackupfabric/KBMain.java +++ b/src/main/java/com/keuin/kbackupfabric/KBMain.java @@ -1,6 +1,6 @@ package com.keuin.kbackupfabric; -public class KBMain { +public final class KBMain { /** * Perform real backup process. * diff --git a/src/main/java/com/keuin/kbackupfabric/KBPluginInitializer.java b/src/main/java/com/keuin/kbackupfabric/KBPluginInitializer.java index 684ec07..c3582c7 100644 --- a/src/main/java/com/keuin/kbackupfabric/KBPluginInitializer.java +++ b/src/main/java/com/keuin/kbackupfabric/KBPluginInitializer.java @@ -3,7 +3,7 @@ package com.keuin.kbackupfabric; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.registry.CommandRegistry; -public class KBPluginInitializer implements ModInitializer { +public final class KBPluginInitializer implements ModInitializer { @Override public void onInitialize() { System.out.println("Initializing KBackup..."); diff --git a/src/main/java/com/keuin/kbackupfabric/util/BackupFilesystemUtil.java b/src/main/java/com/keuin/kbackupfabric/util/BackupFilesystemUtil.java new file mode 100644 index 0000000..5b8ba5a --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/util/BackupFilesystemUtil.java @@ -0,0 +1,53 @@ +package com.keuin.kbackupfabric.util; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.world.ThreadedAnvilChunkStorage; +import net.minecraft.world.World; + +import java.io.File; + +/** + * Functions deal with file name, directory name about Minecraft saves. + */ +public final class BackupFilesystemUtil { + + private static final String backupSaveDirectoryName = "backups"; + private static final String backupFileNamePrefix = "kbackup-"; + + public static String getBackupFileName(String backupName) { + return backupFileNamePrefix + backupName + ".zip"; + } + + public static String getBackupName(String backupFileName) { + try { + if (backupFileName.matches(backupFileNamePrefix + ".+\\.zip")) + return backupFileName.substring(backupFileNamePrefix.length(), backupFileName.length() - 4); + } catch (IndexOutOfBoundsException ignored) { + } + return backupFileName; + } + + public static boolean isBackupNameValid(String backupName, MinecraftServer server) { + File backupFile = new File(getBackupSaveDirectory(server), getBackupFileName(backupName)); + return backupFile.isFile(); + } + + public static File getBackupSaveDirectory(MinecraftServer server) { + return new File(server.getRunDirectory(), backupSaveDirectoryName); + } + + public static String getLevelPath(MinecraftServer server) { + return (new File(server.getRunDirectory(), server.getLevelName())).getAbsolutePath(); + } + + public static String getWorldDirectoryName(World world) throws NoSuchFieldException, IllegalAccessException { + File saveDir; + ThreadedAnvilChunkStorage threadedAnvilChunkStorage = (ThreadedAnvilChunkStorage) ReflectionUtils.getPrivateField(world.getChunkManager(), "threadedAnvilChunkStorage"); + saveDir = (File) ReflectionUtils.getPrivateField(threadedAnvilChunkStorage, "saveDir"); + return saveDir.getName(); + } + + public static String getBackupFileNamePrefix() { + return backupFileNamePrefix; + } +} diff --git a/src/main/java/com/keuin/kbackupfabric/util/IO.java b/src/main/java/com/keuin/kbackupfabric/util/IO.java deleted file mode 100644 index 6d969ba..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/IO.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.keuin.kbackupfabric.util; - -import com.mojang.brigadier.context.CommandContext; -import net.minecraft.server.command.ServerCommandSource; -import net.minecraft.text.LiteralText; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -public class IO { - - private static final Logger LOGGER = LogManager.getLogger(); - private static final boolean printDebugMessages = true; - private static final boolean printErrorMessages = true; - private static final boolean printInfoMessages = true; - private static final Object syncDebug = new Object(); - private static final Object syncError = new Object(); - private static final Object syncInfo = new Object(); - - public static CommandContext message(CommandContext context, String messageText) { - return message(context, messageText, false); - } - - public static CommandContext message(CommandContext context, String messageText, boolean broadcastToOps) { - context.getSource().sendFeedback(new LiteralText("[KBackup] " + messageText), broadcastToOps); - return context; - } - - public static void debug(String message) { - synchronized (syncDebug) { - if (printDebugMessages) { - System.out.println(String.format("[DEBUG] [KBackup] %s", message)); - LOGGER.debug(message); - } - } - } - - public static void error(String message) { - synchronized (syncError) { - if (printErrorMessages) { - System.out.println(String.format("[ERROR] [KBackup] %s", message)); - LOGGER.error(message); - } - } - } - - public static void info(String message) { - synchronized (syncInfo) { - if (printInfoMessages) { - System.out.println(String.format("[INFO] [KBackup] %s", message)); - LOGGER.info(message); - } - } - } -} diff --git a/src/main/java/com/keuin/kbackupfabric/util/PermissionValidator.java b/src/main/java/com/keuin/kbackupfabric/util/PermissionValidator.java new file mode 100644 index 0000000..9d37901 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/util/PermissionValidator.java @@ -0,0 +1,9 @@ +package com.keuin.kbackupfabric.util; + +import net.minecraft.server.command.ServerCommandSource; + +public class PermissionValidator { + public static boolean op(ServerCommandSource commandSource) { + return commandSource.hasPermissionLevel(4); + } +} diff --git a/src/main/java/com/keuin/kbackupfabric/util/PostProgressRestoreThread.java b/src/main/java/com/keuin/kbackupfabric/util/PostProgressRestoreThread.java deleted file mode 100644 index 8427a2c..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/PostProgressRestoreThread.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.keuin.kbackupfabric.util; - -import java.io.File; -import java.io.IOException; - -import static com.keuin.kbackupfabric.util.IO.*; -import static org.apache.commons.io.FileUtils.forceDelete; - -/** - * This thread wait the server to be stopped (must invoke stop out of this thread), - * then delete current level, and restore our backup. - */ -public class PostProgressRestoreThread implements Runnable { - private final Thread serverThread; - private final String backupFilePath; - private final String levelDirectory; - - public PostProgressRestoreThread(Thread serverThread, String backupFilePath, String levelDirectory) { - this.serverThread = serverThread; - this.backupFilePath = backupFilePath; - this.levelDirectory = levelDirectory; - } - - @Override - public void run() { - try { - // Wait server thread die - debug("Waiting server thread stopping ..."); - while (serverThread.isAlive()) { - try { - serverThread.join(); - } catch (InterruptedException ignored) { - } - } - - debug("Waiting ..."); - try { - Thread.sleep(5000); - } catch (InterruptedException ignored) { - } - // Delete old level - debug("Server stopped. Deleting old level ..."); - File levelDirFile = new File(levelDirectory); - - int failedCounter = 0; - final int MAX_RETRY_TIMES = 20; - while (failedCounter < MAX_RETRY_TIMES) { - System.gc(); - if (!levelDirFile.delete() && levelDirFile.exists()) { - System.gc(); - forceDelete(levelDirFile); // Try to force delete. - } - if (levelDirFile.exists()) - ++failedCounter; - else - break; - } - if (levelDirFile.exists()) { - error(String.format("Cannot restore: failed to delete old level %s .", levelDirFile.getName())); - return; - } - - // Decompress archive - debug("Decompressing archived level"); - ZipUtil.unzip(backupFilePath, levelDirectory, false); - info("Restore complete! Please restart the server manually."); - } catch (SecurityException | IOException | ZipUtilException e) { - error("An exception occurred while restoring: " + e.getMessage()); - } - } -} diff --git a/src/main/java/com/keuin/kbackupfabric/util/PrintUtil.java b/src/main/java/com/keuin/kbackupfabric/util/PrintUtil.java new file mode 100644 index 0000000..e76155f --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/util/PrintUtil.java @@ -0,0 +1,57 @@ +package com.keuin.kbackupfabric.util; + +import com.mojang.brigadier.context.CommandContext; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.LiteralText; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public final class PrintUtil { + + private static final Logger LOGGER = LogManager.getLogger(); + private static final boolean printDebugMessages = true; + private static final boolean printErrorMessages = true; + private static final boolean printInfoMessages = true; + private static final Object syncDebug = new Object(); + private static final Object syncError = new Object(); + private static final Object syncInfo = new Object(); + private static final Object syncMessage = new Object(); + + public static CommandContext message(CommandContext context, String messageText) { + return message(context, messageText, false); + } + + public static CommandContext message(CommandContext context, String messageText, boolean broadcastToOps) { + synchronized (syncMessage) { + context.getSource().sendFeedback(new LiteralText("[KBackup] " + messageText), broadcastToOps); + } + return context; + } + + public static void debug(String message) { + synchronized (syncDebug) { + if (printDebugMessages) { + System.out.println(String.format("[DEBUG] [KBackup] %s", message)); + LOGGER.debug(message); + } + } + } + + public static void error(String message) { + synchronized (syncError) { + if (printErrorMessages) { + System.out.println(String.format("[ERROR] [KBackup] %s", message)); + LOGGER.error(message); + } + } + } + + public static void info(String message) { + synchronized (syncInfo) { + if (printInfoMessages) { + System.out.println(String.format("[INFO] [KBackup] %s", message)); + LOGGER.info(message); + } + } + } +} diff --git a/src/main/java/com/keuin/kbackupfabric/util/ReflectionUtils.java b/src/main/java/com/keuin/kbackupfabric/util/ReflectionUtils.java index f7bc351..2acc562 100644 --- a/src/main/java/com/keuin/kbackupfabric/util/ReflectionUtils.java +++ b/src/main/java/com/keuin/kbackupfabric/util/ReflectionUtils.java @@ -7,9 +7,9 @@ import java.lang.reflect.Method; /** * @Author 落叶飞翔的蜗牛 * @Date 2018/3/10 - * @Description + * @Description 常用反射函数 */ -public class ReflectionUtils { +public final class ReflectionUtils { /** * 获取私有成员变量的值 diff --git a/src/main/java/com/keuin/kbackupfabric/util/WorldUtil.java b/src/main/java/com/keuin/kbackupfabric/util/WorldUtil.java deleted file mode 100644 index badc068..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/WorldUtil.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.keuin.kbackupfabric.util; - -import net.minecraft.server.world.ThreadedAnvilChunkStorage; -import net.minecraft.world.World; - -import java.io.File; - -public class WorldUtil { - public static String getWorldDirectoryName(World world) throws NoSuchFieldException, IllegalAccessException { - File saveDir; - ThreadedAnvilChunkStorage threadedAnvilChunkStorage = (ThreadedAnvilChunkStorage) ReflectionUtils.getPrivateField(world.getChunkManager(), "threadedAnvilChunkStorage"); - saveDir = (File) ReflectionUtils.getPrivateField(threadedAnvilChunkStorage, "saveDir"); - return saveDir.getName(); - } -} diff --git a/src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java b/src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java index 0ab93ca..1ceeb60 100644 --- a/src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java +++ b/src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java @@ -4,7 +4,7 @@ import java.io.*; import java.util.Enumeration; import java.util.zip.*; -public class ZipUtil { +public final class ZipUtil { /** * 递归压缩文件夹 diff --git a/src/main/java/com/keuin/kbackupfabric/worker/BackupWorker.java b/src/main/java/com/keuin/kbackupfabric/worker/BackupWorker.java new file mode 100644 index 0000000..30fd8b5 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/worker/BackupWorker.java @@ -0,0 +1,92 @@ +package com.keuin.kbackupfabric.worker; + +import com.keuin.kbackupfabric.util.ZipUtil; +import com.keuin.kbackupfabric.util.ZipUtilException; +import com.mojang.brigadier.context.CommandContext; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.world.World; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static com.keuin.kbackupfabric.util.BackupFilesystemUtil.*; +import static com.keuin.kbackupfabric.util.PrintUtil.debug; +import static com.keuin.kbackupfabric.util.PrintUtil.message; + +/** + * The backup worker + * To invoke this worker, simply call invoke() method. + */ +public final class BackupWorker implements Runnable { + private final CommandContext context; + private final String backupName; + private final Map oldWorldsSavingDisabled; + + + private BackupWorker(CommandContext context, String backupName, Map oldWorldsSavingDisabled) { + this.context = context; + this.backupName = backupName; + this.oldWorldsSavingDisabled = oldWorldsSavingDisabled; + } + + public static void invoke(CommandContext context, String backupName) { + //// Save world, save old autosave configs + + message(context, String.format("Making backup %s, please wait ...", backupName), true); + Map oldWorldsSavingDisabled = new HashMap<>(); // old switch stat + + // Get server + MinecraftServer server = context.getSource().getMinecraftServer(); + + // Save old autosave switch stat temporally + server.getWorlds().forEach(world -> { + oldWorldsSavingDisabled.put(world, world.savingDisabled); + world.savingDisabled = true; + }); + + // Force to save all player data and worlds + debug("Saving players ..."); + server.getPlayerManager().saveAllPlayerData(); + debug("Saving worlds ..."); + server.save(true, true, true); + + // Start threaded worker + BackupWorker worker = new BackupWorker(context, backupName, oldWorldsSavingDisabled); + Thread workerThread = new Thread(worker, "BackupWorker"); + workerThread.start(); + } + + @Override + public void run() { + String destPathFolderName = ""; + MinecraftServer server = context.getSource().getMinecraftServer(); + try { + //// Do our main backup logic + + // Create backup saving directory + File destPathFile = getBackupSaveDirectory(server); + destPathFolderName = destPathFile.getName(); + if (!destPathFile.isDirectory() && !destPathFile.mkdir()) { + message(context, String.format("Failed to create backup saving directory: %s. Failed to backup.", destPathFolderName)); + return; + } + + // Make zip + String levelPath = getLevelPath(server); + debug(String.format("zip(srcPath=%s, destPath=%s)", levelPath, destPathFile.toString())); + ZipUtil.zip(levelPath, destPathFile.toString(), getBackupFileName(backupName)); + + // Restore old autosave switch stat + server.getWorlds().forEach(world -> world.savingDisabled = oldWorldsSavingDisabled.getOrDefault(world, true)); + + message(context, "Done.", true); + } catch (SecurityException e) { + message(context, String.format("Failed to create backup saving directory: %s. Failed to backup.", destPathFolderName)); + } catch (IOException | ZipUtilException e) { + message(context, "Failed to make zip: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/keuin/kbackupfabric/worker/RestoreWorker.java b/src/main/java/com/keuin/kbackupfabric/worker/RestoreWorker.java new file mode 100644 index 0000000..9e424d7 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/worker/RestoreWorker.java @@ -0,0 +1,90 @@ +package com.keuin.kbackupfabric.worker; + +import com.keuin.kbackupfabric.util.ZipUtil; +import com.keuin.kbackupfabric.util.ZipUtilException; +import net.minecraft.server.MinecraftServer; + +import java.io.File; +import java.io.IOException; + +import static com.keuin.kbackupfabric.util.PrintUtil.*; +import static org.apache.commons.io.FileUtils.forceDelete; + +/** + * The restore worker + * To invoke this worker, simply call invoke() method. + */ +public final class RestoreWorker implements Runnable { + private final Thread serverThread; + private final String backupFilePath; + private final String levelDirectory; + + private RestoreWorker(Thread serverThread, String backupFilePath, String levelDirectory) { + this.serverThread = serverThread; + this.backupFilePath = backupFilePath; + this.levelDirectory = levelDirectory; + } + + public static void invoke(MinecraftServer server, String backupFilePath, String levelDirectory) { + RestoreWorker worker = new RestoreWorker(server.getThread(), backupFilePath, levelDirectory); + Thread workerThread = new Thread(worker, "RestoreWorker"); + workerThread.start(); + try { + Thread.sleep(500); + } catch (InterruptedException ignored) { + } + server.stop(false); + } + + @Override + public void run() { + try { + // Wait server thread die + debug("Waiting server thread stopping ..."); + while (serverThread.isAlive()) { + try { + serverThread.join(); + } catch (InterruptedException ignored) { + } + } + + debug("Wait for 5 seconds ..."); + try { + Thread.sleep(5000); + } catch (InterruptedException ignored) { + } + + // Delete old level + debug("Server stopped. Deleting old level ..."); + File levelDirFile = new File(levelDirectory); + + int failedCounter = 0; + final int MAX_RETRY_TIMES = 20; + while (failedCounter < MAX_RETRY_TIMES) { + System.gc(); + if (!levelDirFile.delete() && levelDirFile.exists()) { + System.gc(); + forceDelete(levelDirFile); // Try to force delete. + } + if (!levelDirFile.exists()) + break; + ++failedCounter; + try { + Thread.sleep(500); + } catch (InterruptedException ignored) { + } + } + if (levelDirFile.exists()) { + error(String.format("Cannot restore: failed to delete old level %s .", levelDirFile.getName())); + return; + } + + // Decompress archive + debug("Decompressing archived level"); + ZipUtil.unzip(backupFilePath, levelDirectory, false); + info("Restore complete! Please restart the server manually."); + } catch (SecurityException | IOException | ZipUtilException e) { + error("An exception occurred while restoring: " + e.getMessage()); + } + } +} -- cgit v1.2.3