From b2d161cee43b9b1af04416486d6df82621b03ae3 Mon Sep 17 00:00:00 2001 From: Keuin Date: Thu, 23 Apr 2020 10:45:28 +0800 Subject: Version 1.0.0-dev TODO: Optimize ZipUtil --- .../com/keuin/kbackupfabric/KBCommandHandler.java | 129 ++++++++++++++------- .../com/keuin/kbackupfabric/KBCommandRegister.java | 2 + src/main/java/com/keuin/kbackupfabric/util/IO.java | 54 +++++++++ .../util/PostProgressRestoreThread.java | 71 ++++++++++++ .../java/com/keuin/kbackupfabric/util/ZipUtil.java | 34 +++--- 5 files changed, 236 insertions(+), 54 deletions(-) create mode 100644 src/main/java/com/keuin/kbackupfabric/util/IO.java create mode 100644 src/main/java/com/keuin/kbackupfabric/util/PostProgressRestoreThread.java diff --git a/src/main/java/com/keuin/kbackupfabric/KBCommandHandler.java b/src/main/java/com/keuin/kbackupfabric/KBCommandHandler.java index c1d176c..33d0f43 100644 --- a/src/main/java/com/keuin/kbackupfabric/KBCommandHandler.java +++ b/src/main/java/com/keuin/kbackupfabric/KBCommandHandler.java @@ -1,15 +1,13 @@ package com.keuin.kbackupfabric; +import com.keuin.kbackupfabric.util.PostProgressRestoreThread; import com.keuin.kbackupfabric.util.ZipUtil; import com.keuin.kbackupfabric.util.ZipUtilException; 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.text.LiteralText; import net.minecraft.world.World; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import java.io.File; import java.io.IOException; @@ -18,15 +16,19 @@ 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; + public class KBCommandHandler { - private static final Logger LOGGER = LogManager.getLogger(); + private static final int SUCCESS = 1; private static final int FAILED = -1; - private static final boolean printDebugMessages = true; - private static final boolean printErrorMessages = true; + 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; /** * Print the help menu. @@ -49,9 +51,14 @@ public class KBCommandHandler { File[] files = getBackupSaveDirectory(server).listFiles( (dir, name) -> dir.isDirectory() && name.toLowerCase().endsWith(".zip") && name.toLowerCase().startsWith(backupFileNamePrefix) ); + backupIndexNameMapper.clear(); if (files != null) { + int i = 0; for (File file : files) { - message(context, file.getName()); + ++i; + String backupName = getBackupName(file.getName()); + backupIndexNameMapper.put(i, backupName); + message(context, String.format("[%d] %s, size: %.1fMB", i, backupName, file.length() * 1.0 / 1024 / 1024)); } } return SUCCESS; @@ -65,9 +72,43 @@ public class KBCommandHandler { */ public static int backup(CommandContext context) { //KBMain.backup("name") - return doBackup(context, StringArgumentType.getString(context, "backupName")); + String backupName = StringArgumentType.getString(context, "backupName"); + if (backupName.matches("[0-9]*")) { + // Numeric param is not allowed + backupName = String.format("a%s", backupName); + message(context, String.format("Pure numeric name is not allowed. Renamed to %s", backupName)); + } + return doBackup(context, backupName); } + /** + * Restore with context parameter backupName. + * Simply set the pending backupName to given backupName, for the second confirmation. + * + * @param context the context. + * @return stat code. + */ + public static int restore(CommandContext context) { + //KBMain.restore("name") + String backupName = StringArgumentType.getString(context, "backupName"); + + if (backupName.matches("[0-9]*")) { + // If numeric input + Integer index = Integer.parseInt(backupName); + String realBackupName = backupIndexNameMapper.get(index); + if (realBackupName == null) { + return list(context); // Show the list and return + } + backupName = realBackupName; // Replace input number with real backup name. + } + + // 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); + return SUCCESS; + } + + /** * Backup with default name. * @@ -84,7 +125,7 @@ public class KBCommandHandler { private static int doBackup(CommandContext context, String backupName) { String destPathFolderName = ""; try { - message(context, String.format("Making backup %s ...", backupName), true); + message(context, String.format("Making backup %s, please wait ...", backupName), true); Map oldWorldsSavingDisabled = new HashMap<>(); // old switch stat // Get server @@ -104,9 +145,6 @@ public class KBCommandHandler { //// Do our main backup logic - // Get the level folder - File sourcePathFile = new File(server.getRunDirectory(), server.getLevelName()); - // Create backup saving directory File destPathFile = getBackupSaveDirectory(server); destPathFolderName = destPathFile.getName(); @@ -116,9 +154,9 @@ public class KBCommandHandler { } // Make zip - debug(String.format("zip(srcPath=%s, destPath=%s)", sourcePathFile.getAbsolutePath(), destPathFile.toString())); - ZipUtil.zip(sourcePathFile.getAbsolutePath(), destPathFile.toString(), backupFileNamePrefix + backupName + ".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)); @@ -134,51 +172,60 @@ public class KBCommandHandler { } } + 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(); + } + /** * Restore with context parameter backupName. * * @param context the context. * @return stat code. */ - public static int restore(CommandContext context) { - //KBMain.restore("name") - String backupName = StringArgumentType.getString(context, "backupName"); - message(context, String.format("Restoring worlds to %s ...", backupName), true); + public static int confirm(CommandContext context) { + if (restoreBackupNameToBeConfirmed == null) { + message(context, "Nothing to be confirmed. Please execute /kb restore first."); + return FAILED; + } + // do restore to backupName + String backupName = restoreBackupNameToBeConfirmed; + message(context, String.format("Restoring worlds to %s ...", backupName), true); - message(context, "Done.", true); + // Get server + MinecraftServer server = context.getSource().getMinecraftServer(); + 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); return SUCCESS; } - private static CommandContext message(CommandContext context, String messageText) { - return message(context, messageText, false); - } - - private static CommandContext message(CommandContext context, String messageText, boolean broadcastToOps) { - context.getSource().sendFeedback(new LiteralText("[KBackup] " + messageText), broadcastToOps); - return context; - } - static boolean opPermissionValidator(ServerCommandSource commandSource) { return commandSource.hasPermissionLevel(4); } - private static void debug(String debugMessage) { - if (printDebugMessages) { - System.out.println(String.format("[DEBUG] [KBackup] %s", debugMessage)); - LOGGER.debug(debugMessage); - } - } - private static void error(String errorMessage) { - if (printErrorMessages) { - System.out.println(String.format("[ERROR] [KBackup] %s", errorMessage)); - LOGGER.error(errorMessage); - } - } } diff --git a/src/main/java/com/keuin/kbackupfabric/KBCommandRegister.java b/src/main/java/com/keuin/kbackupfabric/KBCommandRegister.java index 96f05e8..d4823c4 100644 --- a/src/main/java/com/keuin/kbackupfabric/KBCommandRegister.java +++ b/src/main/java/com/keuin/kbackupfabric/KBCommandRegister.java @@ -22,6 +22,8 @@ public class KBCommandRegister { // 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))); + // register /kb confirm for confirming the execution + dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("confirm").executes(KBCommandHandler::confirm))); // 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/util/IO.java b/src/main/java/com/keuin/kbackupfabric/util/IO.java new file mode 100644 index 0000000..6d969ba --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/util/IO.java @@ -0,0 +1,54 @@ +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/PostProgressRestoreThread.java b/src/main/java/com/keuin/kbackupfabric/util/PostProgressRestoreThread.java new file mode 100644 index 0000000..8427a2c --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/util/PostProgressRestoreThread.java @@ -0,0 +1,71 @@ +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/ZipUtil.java b/src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java index d92ab12..29944c3 100644 --- a/src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java +++ b/src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java @@ -12,7 +12,7 @@ public class ZipUtil { * @param srcRootDir 压缩文件夹根目录的子路径 * @param file 当前递归压缩的文件或目录对象 * @param zos 压缩文件存储对象 - * @throws Exception + * @throws IOException IO Error */ private static void zip(String srcRootDir, File file, ZipOutputStream zos) throws IOException { if (file == null) { @@ -43,9 +43,9 @@ public class ZipUtil { else { // 压缩目录中的文件或子目录 File[] childFileList = file.listFiles(); - for (int n = 0; n < childFileList.length; n++) { - childFileList[n].getAbsolutePath().indexOf(file.getAbsolutePath()); - zip(srcRootDir, childFileList[n], zos); + for (File value : childFileList) { + value.getAbsolutePath().indexOf(file.getAbsolutePath()); + zip(srcRootDir, value, zos); } } } @@ -94,7 +94,7 @@ public class ZipUtil { //如果只是压缩一个文件,则需要截取该文件的父目录 String srcRootDir = srcPath; - if (srcFile.isFile() || true) { // Hack this stupid setting. We want to keep our least parent folder! + if (srcFile.isFile()) { // (Disabled) Hack this stupid setting. We want to keep our least parent folder! int index = srcPath.lastIndexOf(File.separator); if (index != -1) { srcRootDir = srcPath.substring(0, index); @@ -158,6 +158,7 @@ public class ZipUtil { entry = entries.nextElement(); // 构建压缩包中一个文件解压后保存的文件全路径 entryFilePath = unzipFilePath + File.separator + entry.getName(); + // 构建解压后保存的文件夹路径 index = entryFilePath.lastIndexOf(File.separator); if (index != -1) { @@ -180,15 +181,22 @@ public class ZipUtil { // 删除已存在的目标文件 entryFile.delete(); } - - // 写入文件 - bos = new BufferedOutputStream(new FileOutputStream(entryFile)); - bis = new BufferedInputStream(zip.getInputStream(entry)); - while ((count = bis.read(buffer, 0, bufferSize)) != -1) { - bos.write(buffer, 0, count); + if (entry.isDirectory()) { + // If the entry is a directory, we make its corresponding directory. + entryFile.mkdir(); + } else { + // Is a file, we write the data + // 写入文件 + bos = new BufferedOutputStream(new FileOutputStream(entryFile)); + bis = new BufferedInputStream(zip.getInputStream(entry)); + while ((count = bis.read(buffer, 0, bufferSize)) != -1) { + bos.write(buffer, 0, count); + } + bos.flush(); + bos.close(); } - bos.flush(); - bos.close(); + + } zip.close(); } -- cgit v1.2.3