summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/main/java/com/keuin/kbackupfabric/KBCommandHandler.java129
-rw-r--r--src/main/java/com/keuin/kbackupfabric/KBCommandRegister.java2
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/IO.java54
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/PostProgressRestoreThread.java71
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java34
5 files changed, 236 insertions, 54 deletions
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<Integer, String> 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,10 +72,44 @@ public class KBCommandHandler {
*/
public static int backup(CommandContext<ServerCommandSource> 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<ServerCommandSource> 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.
*
* @param context the context.
@@ -84,7 +125,7 @@ public class KBCommandHandler {
private static int doBackup(CommandContext<ServerCommandSource> 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<World, Boolean> 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<ServerCommandSource> 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<ServerCommandSource> context) {
+ if (restoreBackupNameToBeConfirmed == null) {
+ message(context, "Nothing to be confirmed. Please execute /kb restore <backup_name> 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<ServerCommandSource> message(CommandContext<ServerCommandSource> context, String messageText) {
- return message(context, messageText, false);
- }
-
- private static CommandContext<ServerCommandSource> message(CommandContext<ServerCommandSource> 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 <name> 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<ServerCommandSource> 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<ServerCommandSource> message(CommandContext<ServerCommandSource> context, String messageText) {
+ return message(context, messageText, false);
+ }
+
+ public static CommandContext<ServerCommandSource> message(CommandContext<ServerCommandSource> 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();
}