diff options
8 files changed, 120 insertions, 31 deletions
@@ -14,10 +14,13 @@ commands: - **/kb restore \<backup_name\>**: restore to a certain backup. This command needs a confirm to execute. - **/kb confirm**: confirm executing restore operation. The operation is irreversible. - **/kb delete**: delete an existing backup. +- **/kb prev**: Find and select the most recent backup file. To-Do List: +- Add /kb prev command for easily select previous backup. +- Use OCP to refactor pending task. - Optimize log output, normal output and op broadcast output. - More thorough test. - Enhance ZipUtil for hashing sub-files and generating incremental diff-table. (A:Add, M:Modification, D:Deletion) diff --git a/src/main/java/com/keuin/kbackupfabric/KBCommandRegister.java b/src/main/java/com/keuin/kbackupfabric/KBCommandRegister.java index 4b4dd59..79029bf 100644 --- a/src/main/java/com/keuin/kbackupfabric/KBCommandRegister.java +++ b/src/main/java/com/keuin/kbackupfabric/KBCommandRegister.java @@ -33,5 +33,7 @@ public final class KBCommandRegister { // 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(KBCommands::cancel))); + // register /kb prev for showing the latest backup. + dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("prev").requires(PermissionValidator::op).executes(KBCommands::prev))); } } diff --git a/src/main/java/com/keuin/kbackupfabric/KBCommands.java b/src/main/java/com/keuin/kbackupfabric/KBCommands.java index be4d151..5302150 100644 --- a/src/main/java/com/keuin/kbackupfabric/KBCommands.java +++ b/src/main/java/com/keuin/kbackupfabric/KBCommands.java @@ -2,6 +2,8 @@ package com.keuin.kbackupfabric; import com.keuin.kbackupfabric.data.BackupMetadata; import com.keuin.kbackupfabric.data.PendingOperation; +import com.keuin.kbackupfabric.util.BackupFilesystemUtil; +import com.keuin.kbackupfabric.util.BackupNameTimeFormatter; import com.keuin.kbackupfabric.util.PrintUtil; import com.keuin.kbackupfabric.worker.BackupWorker; import com.keuin.kbackupfabric.worker.RestoreWorker; @@ -14,9 +16,10 @@ import org.apache.logging.log4j.Logger; import java.io.File; import java.io.IOException; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.HashMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; import static com.keuin.kbackupfabric.util.BackupFilesystemUtil.*; import static com.keuin.kbackupfabric.util.PrintUtil.*; @@ -30,7 +33,7 @@ public final class KBCommands { private static final Logger LOGGER = LogManager.getLogger(); - private static final HashMap<Integer, String> backupIndexNameMapper = new HashMap<>(); // index -> backupName + private static final List<String> backupNameList = new ArrayList<>(); // index -> backupName private static PendingOperation pendingOperation = null; /** @@ -56,13 +59,13 @@ public final class KBCommands { File[] files = getBackupSaveDirectory(server).listFiles( (dir, name) -> dir.isDirectory() && name.toLowerCase().endsWith(".zip") && name.toLowerCase().startsWith(getBackupFileNamePrefix()) ); - backupIndexNameMapper.clear(); + backupNameList.clear(); if (files != null) { int i = 0; for (File file : files) { ++i; String backupName = getBackupName(file.getName()); - backupIndexNameMapper.put(i, backupName); + backupNameList.add(backupName); msgInfo(context, String.format("[%d] %s, size: %.1fMB", i, backupName, file.length() * 1.0 / 1024 / 1024)); } } @@ -160,9 +163,7 @@ public final class KBCommands { private static int doBackup(CommandContext<ServerCommandSource> context, String customName) { // Real backup name (compatible with legacy backup): date_name, such as 2020-04-23_21-03-00_test //KBMain.backup("name") - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"); - String timeString = LocalDateTime.now().format(formatter); - String backupName = timeString + "_" + customName; + String backupName = BackupNameTimeFormatter.getTimeString() + "_" + customName; // Validate file name final char[] ILLEGAL_CHARACTERS = {'/', '\n', '\r', '\t', '\0', '\f', '`', '?', '*', '\\', '<', '>', '|', '\"', ':'}; @@ -265,14 +266,51 @@ public final class KBCommands { } } - private static String parseBackupName(CommandContext<ServerCommandSource> context, String userInput) { - MinecraftServer server = context.getSource().getMinecraftServer(); - String backupName = StringArgumentType.getString(context, "backupName"); + /** + * Show the most recent backup. + * If there is no available backup, print specific info. + * + * @param context the context. + * @return stat code. + */ + public static int prev(CommandContext<ServerCommandSource> context) { + try { + // List all backups + MinecraftServer server = context.getSource().getMinecraftServer(); + List<File> files = Arrays.asList(Objects.requireNonNull(getBackupSaveDirectory(server).listFiles())); + files.removeIf(f -> !f.getName().startsWith(BackupFilesystemUtil.getBackupFileNamePrefix())); + files.sort((x, y) -> (int) (BackupFilesystemUtil.getBackupTimeFromBackupFileName(y.getName()) - BackupFilesystemUtil.getBackupTimeFromBackupFileName(x.getName()))); + File prevBackupFile = files.get(0); + String backupName = getBackupName(prevBackupFile.getName()); + int i = backupNameList.indexOf(backupName); + if (i == -1) { + backupNameList.add(backupName); + i = backupNameList.size(); + } else { + ++i; + } + msgInfo(context, String.format("The most recent backup: [%d] %s , size: %s", i, backupName, humanFileSize(prevBackupFile.length()))); + } catch (NullPointerException e) { + msgInfo(context, "There are no backups available."); + } catch (SecurityException ignored) { + msgErr(context, "Failed to read file."); + return FAILED; + } + return SUCCESS; + } - if (backupName.matches("[0-9]*")) { - // If numeric input - Integer index = Integer.parseInt(backupName); - return backupIndexNameMapper.get(index); // Replace input number with real backup name. + + private static String parseBackupName(CommandContext<ServerCommandSource> context, String userInput) { + try { + MinecraftServer server = context.getSource().getMinecraftServer(); + String backupName = StringArgumentType.getString(context, "backupName"); + + if (backupName.matches("[0-9]*")) { + // If numeric input + int index = Integer.parseInt(backupName) - 1; + return backupNameList.get(index); // Replace input number with real backup name. + } + } catch (NumberFormatException | IndexOutOfBoundsException ignored) { } return userInput; } diff --git a/src/main/java/com/keuin/kbackupfabric/KBPluginEvents.java b/src/main/java/com/keuin/kbackupfabric/KBPluginEvents.java index 3b1901e..47983b7 100644 --- a/src/main/java/com/keuin/kbackupfabric/KBPluginEvents.java +++ b/src/main/java/com/keuin/kbackupfabric/KBPluginEvents.java @@ -29,14 +29,14 @@ public final class KBPluginEvents implements ModInitializer, ServerStartCallback @Override public void onInitialize() { - System.out.println("KBackup: Binding events and commands ..."); + System.out.println("[KBackup] Binding events and commands ..."); CommandRegistry.INSTANCE.register(false, KBCommandRegister::registerCommands); ServerStartCallback.EVENT.register(this); } @Override public void onStartServer(MinecraftServer server) { - LOGGER.debug("KBackup: Initializing ..."); + LOGGER.debug("[KBackup] Initializing ..."); // Update backup suggestion list BackupNameSuggestionProvider.setBackupSaveDirectory(BackupFilesystemUtil.getBackupSaveDirectory(server).getPath()); @@ -55,7 +55,7 @@ public final class KBPluginEvents implements ModInitializer, ServerStartCallback fileInputStream.close(); // Print metadata - LOGGER.info("Recovered from previous backup:"); + LOGGER.info("[KBackup] Recovered from previous backup:"); LOGGER.info("Backup Name: " + metadata.getBackupName()); LOGGER.info("Create Time: " + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")).format(new Date(metadata.getBackupTime()))); diff --git a/src/main/java/com/keuin/kbackupfabric/util/BackupFilesystemUtil.java b/src/main/java/com/keuin/kbackupfabric/util/BackupFilesystemUtil.java index 5b8ba5a..ce39615 100644 --- a/src/main/java/com/keuin/kbackupfabric/util/BackupFilesystemUtil.java +++ b/src/main/java/com/keuin/kbackupfabric/util/BackupFilesystemUtil.java @@ -5,6 +5,8 @@ import net.minecraft.server.world.ThreadedAnvilChunkStorage; import net.minecraft.world.World; import java.io.File; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Functions deal with file name, directory name about Minecraft saves. @@ -50,4 +52,27 @@ public final class BackupFilesystemUtil { public static String getBackupFileNamePrefix() { return backupFileNamePrefix; } + + public static long getBackupTimeFromBackupFileName(String backupFileName) { + Matcher matcher = Pattern.compile("[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}").matcher(backupFileName); + if (matcher.find()) { + String timeString = matcher.group(0); + long timeStamp = BackupNameTimeFormatter.timeStringToEpochSeconds(timeString); + System.out.println(backupFileName + " -> " + timeStamp); + return timeStamp; + } else { + System.err.println("Failed to extract time from " + backupFileName); + } + return -1; + } + + public static String humanFileSize(long size) { + double fileSize = size * 1.0 / 1024 / 1024; // Default unit is MB + if (fileSize > 1000) + //msgInfo(context, String.format("File size: %.2fGB", fileSize / 1024)); + return String.format("%.2fGB", fileSize / 1024); + else + //msgInfo(context, String.format("File size: %.2fMB", fileSize)); + return String.format("%.2fMB", fileSize); + } } diff --git a/src/main/java/com/keuin/kbackupfabric/util/BackupNameTimeFormatter.java b/src/main/java/com/keuin/kbackupfabric/util/BackupNameTimeFormatter.java new file mode 100644 index 0000000..5d00270 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/util/BackupNameTimeFormatter.java @@ -0,0 +1,22 @@ +package com.keuin.kbackupfabric.util; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + +public class BackupNameTimeFormatter { + private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"); + + public static String getTimeString() { + return LocalDateTime.now().format(formatter); + } + + public static long timeStringToEpochSeconds(String timeString) { + ZoneId systemZone = ZoneId.systemDefault(); // my timezone + LocalDateTime localDateTime = LocalDateTime.parse(timeString, formatter); + ZoneOffset currentOffsetForMyZone = systemZone.getRules().getOffset(localDateTime); + return localDateTime.toEpochSecond(currentOffsetForMyZone); + } + +} diff --git a/src/main/java/com/keuin/kbackupfabric/worker/BackupWorker.java b/src/main/java/com/keuin/kbackupfabric/worker/BackupWorker.java index 57e59fb..3303f70 100644 --- a/src/main/java/com/keuin/kbackupfabric/worker/BackupWorker.java +++ b/src/main/java/com/keuin/kbackupfabric/worker/BackupWorker.java @@ -56,9 +56,9 @@ public final class BackupWorker implements Runnable { }); // Force to save all player data and worlds - LOGGER.debug("Saving players ..."); + LOGGER.info("Saving players ..."); server.getPlayerManager().saveAllPlayerData(); - LOGGER.debug("Saving worlds ..."); + LOGGER.info("Saving worlds ..."); server.save(true, true, true); // Start threaded worker @@ -86,6 +86,7 @@ public final class BackupWorker implements Runnable { String levelPath = getLevelPath(server); String backupFileName = getBackupFileName(backupName); LOGGER.debug(String.format("zip(srcPath=%s, destPath=%s)", levelPath, backupSaveDirectoryFile.toString())); + LOGGER.info("Compressing level ..."); ZipUtil.makeBackupZip(levelPath, backupSaveDirectoryFile.toString(), backupFileName, backupMetadata); File backupZipFile = new File(backupSaveDirectoryFile, backupFileName); @@ -96,11 +97,7 @@ public final class BackupWorker implements Runnable { long timeEscapedMillis = System.currentTimeMillis() - startTime; msgInfo(context, String.format("Backup finished. (%.2fs)", timeEscapedMillis / 1000.0), true); try { - double fileSize = backupZipFile.length() * 1.0 / 1024 / 1024; - if (fileSize > 1000) - msgInfo(context, String.format("File size: %.2fGB", fileSize / 1024)); - else - msgInfo(context, String.format("File size: %.2fMB", fileSize)); + msgInfo(context, String.format("File size: %s", humanFileSize(backupZipFile.length()))); } catch (SecurityException ignored) { } diff --git a/src/main/java/com/keuin/kbackupfabric/worker/RestoreWorker.java b/src/main/java/com/keuin/kbackupfabric/worker/RestoreWorker.java index 8d367d7..54e7beb 100644 --- a/src/main/java/com/keuin/kbackupfabric/worker/RestoreWorker.java +++ b/src/main/java/com/keuin/kbackupfabric/worker/RestoreWorker.java @@ -44,7 +44,7 @@ public final class RestoreWorker implements Runnable { public void run() { try { // Wait server thread die - LOGGER.debug("Waiting for the server thread to exit ..."); + LOGGER.info("Waiting for the server thread to exit ..."); while (serverThread.isAlive()) { try { serverThread.join(); @@ -52,15 +52,16 @@ public final class RestoreWorker implements Runnable { } } - LOGGER.debug("Wait for 5 seconds ..."); + LOGGER.info("Wait for 5 seconds ..."); try { Thread.sleep(5000); } catch (InterruptedException ignored) { } // Delete old level - LOGGER.debug("Server stopped. Deleting old level ..."); + LOGGER.info("Server stopped. Deleting old level ..."); File levelDirFile = new File(levelDirectory); + long startTime = System.currentTimeMillis(); int failedCounter = 0; final int MAX_RETRY_TIMES = 20; @@ -84,9 +85,10 @@ public final class RestoreWorker implements Runnable { } // Decompress archive - LOGGER.debug("Decompressing archived level"); + LOGGER.info("Decompressing archived level"); ZipUtil.unzip(backupFilePath, levelDirectory, false); - LOGGER.info("Restore complete! Please restart the server manually."); + long endTime = System.currentTimeMillis(); + LOGGER.info(String.format("Restore complete! (%.2fs) Please restart the server manually.", (endTime - startTime) / 1000.0)); } catch (SecurityException | IOException | ZipUtilException e) { LOGGER.error("An exception occurred while restoring: " + e.getMessage()); } |