From ba19ab350516e26a8e52cc217878bde63c0b7eee Mon Sep 17 00:00:00 2001 From: Keuin Date: Thu, 23 Apr 2020 20:13:35 +0800 Subject: Refactored code. Added startup message on the first start after restoring a backup. Adjusted text color. --- .../com/keuin/kbackupfabric/KBCommandHandler.java | 188 -------------------- .../com/keuin/kbackupfabric/KBCommandRegister.java | 32 ---- .../java/com/keuin/kbackupfabric/KBCommands.java | 190 +++++++++++++++++++++ .../com/keuin/kbackupfabric/KBPluginEvents.java | 65 +++++++ .../keuin/kbackupfabric/KBPluginInitializer.java | 12 -- .../java/com/keuin/kbackupfabric/KBRegister.java | 32 ++++ .../keuin/kbackupfabric/data/BackupMetadata.java | 23 +++ .../com/keuin/kbackupfabric/util/PrintUtil.java | 53 ++++-- .../java/com/keuin/kbackupfabric/util/ZipUtil.java | 58 ++++--- .../keuin/kbackupfabric/worker/BackupWorker.java | 50 ++++-- src/main/resources/fabric.mod.json | 2 +- 11 files changed, 425 insertions(+), 280 deletions(-) delete mode 100644 src/main/java/com/keuin/kbackupfabric/KBCommandHandler.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/KBCommandRegister.java create mode 100644 src/main/java/com/keuin/kbackupfabric/KBCommands.java create mode 100644 src/main/java/com/keuin/kbackupfabric/KBPluginEvents.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/KBPluginInitializer.java create mode 100644 src/main/java/com/keuin/kbackupfabric/KBRegister.java create mode 100644 src/main/java/com/keuin/kbackupfabric/data/BackupMetadata.java (limited to 'src') diff --git a/src/main/java/com/keuin/kbackupfabric/KBCommandHandler.java b/src/main/java/com/keuin/kbackupfabric/KBCommandHandler.java deleted file mode 100644 index 943fb68..0000000 --- a/src/main/java/com/keuin/kbackupfabric/KBCommandHandler.java +++ /dev/null @@ -1,188 +0,0 @@ -package com.keuin.kbackupfabric; - -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 java.io.File; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.HashMap; - -import static com.keuin.kbackupfabric.util.BackupFilesystemUtil.*; -import static com.keuin.kbackupfabric.util.PrintUtil.debug; -import static com.keuin.kbackupfabric.util.PrintUtil.message; - -public final class KBCommandHandler { - - - private static final int SUCCESS = 1; - private static final int FAILED = -1; - - - private static final HashMap backupIndexNameMapper = new HashMap<>(); // index -> backupName - private static String restoreBackupNameToBeConfirmed = null; - - /** - * Print the help menu. - * - * @param context the context. - * @return stat code. - */ - public static int help(CommandContext context) { - 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; - } - - public static int list(CommandContext context) { - 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(getBackupFileNamePrefix()) - ); - backupIndexNameMapper.clear(); - if (files != null) { - int i = 0; - for (File file : files) { - ++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; - } - - /** - * Backup with context parameter backupName. - * - * @param context the context. - * @return stat code. - */ - public static int backup(CommandContext context) { - //KBMain.backup("name") - 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") - MinecraftServer server = context.getSource().getMinecraftServer(); - 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. - } - - // 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); - return SUCCESS; - } - - - /** - * Backup with default name. - * - * @param context the context. - * @return stat code. - */ - public static int backupWithDefaultName(CommandContext context) { - //KBMain.backup("name") - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"); - String timeString = LocalDateTime.now().format(formatter); - return doBackup(context, timeString); - } - - private static int doBackup(CommandContext context, String backupName) { - BackupWorker.invoke(context, backupName); - return SUCCESS; - } - - /** - * Restore with context parameter backupName. - * - * @param context the context. - * @return stat code. - */ - 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); - - // Get server - MinecraftServer server = context.getSource().getMinecraftServer(); - String backupFileName = getBackupFileName(backupName); - debug("Backup file name: " + backupFileName); - File backupFile = new File(getBackupSaveDirectory(server), backupFileName); - 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; - } - - /** - * 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 deleted file mode 100644 index a9dcb38..0000000 --- a/src/main/java/com/keuin/kbackupfabric/KBCommandRegister.java +++ /dev/null @@ -1,32 +0,0 @@ -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 final class KBCommandRegister { - // First make method to register - public static void register(CommandDispatcher dispatcher) { - // register /kb and /kb help for help menu - 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. 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(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(PermissionValidator::op).executes(KBCommandHandler::restore)).executes(KBCommandHandler::list))); - - // 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))); - - } -} diff --git a/src/main/java/com/keuin/kbackupfabric/KBCommands.java b/src/main/java/com/keuin/kbackupfabric/KBCommands.java new file mode 100644 index 0000000..c636a0d --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/KBCommands.java @@ -0,0 +1,190 @@ +package com.keuin.kbackupfabric; + +import com.keuin.kbackupfabric.data.BackupMetadata; +import com.keuin.kbackupfabric.util.PrintUtil; +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 java.io.File; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; + +import static com.keuin.kbackupfabric.util.BackupFilesystemUtil.*; +import static com.keuin.kbackupfabric.util.PrintUtil.*; + +public final class KBCommands { + + + private static final int SUCCESS = 1; + private static final int FAILED = -1; + + + private static final HashMap backupIndexNameMapper = new HashMap<>(); // index -> backupName + private static String restoreBackupNameToBeConfirmed = null; + + /** + * Print the help menu. + * + * @param context the context. + * @return stat code. + */ + public static int help(CommandContext context) { + msgInfo(context, "==== KBackup Manual ===="); + msgInfo(context, "/kb /kb help Print help menu."); + msgInfo(context, "/kb list Show all backups."); + msgInfo(context, "/kb backup [backup_name] Backup the whole level to backup_name. The default name is current system time."); + msgInfo(context, "/kb restore Delete the whole current level and restore from given backup. /kb restore is identical with /kb list."); + msgInfo(context, "/kb confirm Confirm and start restoring."); + msgInfo(context, "/kb cancel Cancel the restoration to be confirmed. If cancelled, /kb confirm will not run."); + return SUCCESS; + } + + public static int list(CommandContext context) { + msgInfo(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(getBackupFileNamePrefix()) + ); + backupIndexNameMapper.clear(); + if (files != null) { + int i = 0; + for (File file : files) { + ++i; + String backupName = getBackupName(file.getName()); + backupIndexNameMapper.put(i, backupName); + msgInfo(context, String.format("[%d] %s, size: %.1fMB", i, backupName, file.length() * 1.0 / 1024 / 1024)); + } + } + return SUCCESS; + } + + /** + * Backup with context parameter backupName. + * + * @param context the context. + * @return stat code. + */ + public static int backup(CommandContext context) { + //KBMain.backup("name") + String backupName = StringArgumentType.getString(context, "backupName"); + if (backupName.matches("[0-9]*")) { + // Numeric param is not allowed + backupName = String.format("a%s", backupName); + msgWarn(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") + MinecraftServer server = context.getSource().getMinecraftServer(); + 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. + } + + // Validate backupName + if (!isBackupNameValid(backupName, server)) { + // Invalid backupName + msgErr(context, "Invalid backup name! Please check your input. The list index number is also valid.", false); + return FAILED; + } + + // Update confirm pending variable + restoreBackupNameToBeConfirmed = backupName; + msgWarn(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. + * @return stat code. + */ + public static int backupWithDefaultName(CommandContext context) { + //KBMain.backup("name") + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"); + String timeString = LocalDateTime.now().format(formatter); + return doBackup(context, timeString); + } + + private static int doBackup(CommandContext context, String backupName) { + BackupMetadata metadata = new BackupMetadata(System.currentTimeMillis(), backupName); + BackupWorker.invoke(context, backupName, metadata); + return SUCCESS; + } + + /** + * Restore with context parameter backupName. + * + * @param context the context. + * @return stat code. + */ + public static int confirm(CommandContext context) { + if (restoreBackupNameToBeConfirmed == null) { + msgInfo(context, "Nothing to be confirmed. Please execute /kb restore first."); + return FAILED; + } + + // do restore to backupName + String backupName = restoreBackupNameToBeConfirmed; + PrintUtil.msgInfo(context, String.format("Restoring worlds to %s ...", backupName), true); + + // Get server + MinecraftServer server = context.getSource().getMinecraftServer(); + String backupFileName = getBackupFileName(backupName); + debug("Backup file name: " + backupFileName); + File backupFile = new File(getBackupSaveDirectory(server), backupFileName); + PrintUtil.msgInfo(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); + PrintUtil.msgInfo(context, "Please do not force the server stop, or the level would be broken.", true); + PrintUtil.msgInfo(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) { + } + } + PrintUtil.msgInfo(context, "Shutting down ...", true); + RestoreWorker.invoke(server, backupFile.getPath(), getLevelPath(server)); + return SUCCESS; + } + + /** + * Cancel the execution to be confirmed. + * + * @param context the context. + * @return stat code. + */ + public static int cancel(CommandContext context) { + if (restoreBackupNameToBeConfirmed != null) { + restoreBackupNameToBeConfirmed = null; + PrintUtil.msgInfo(context, "The restoration is cancelled.", true); + return SUCCESS; + } else { + msgErr(context, "Nothing to cancel."); + return FAILED; + } + } +} diff --git a/src/main/java/com/keuin/kbackupfabric/KBPluginEvents.java b/src/main/java/com/keuin/kbackupfabric/KBPluginEvents.java new file mode 100644 index 0000000..4f0f1ff --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/KBPluginEvents.java @@ -0,0 +1,65 @@ +package com.keuin.kbackupfabric; + +import com.keuin.kbackupfabric.data.BackupMetadata; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.event.server.ServerStartCallback; +import net.fabricmc.fabric.api.registry.CommandRegistry; +import net.minecraft.server.MinecraftServer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.text.SimpleDateFormat; +import java.util.Date; + +import static com.keuin.kbackupfabric.util.PrintUtil.debug; +import static com.keuin.kbackupfabric.util.PrintUtil.info; +import static org.apache.commons.io.FileUtils.forceDelete; + +/** + * This is the Main file of this plugin. + * It contains all events, including the init event. + */ +public final class KBPluginEvents implements ModInitializer, ServerStartCallback { + @Override + public void onInitialize() { + System.out.println("Initializing KBackup..."); + CommandRegistry.INSTANCE.register(false, KBRegister::registerCommands); + ServerStartCallback.EVENT.register(this); + } + + @Override + public void onStartServer(MinecraftServer server) { + // When the server starts, we check if we have just recovered from a backup. + // If so, then we print some message. + debug("KBackup onStartServer"); + // TODO: + // Check + try { + File levelDirectory = new File(server.getRunDirectory(), server.getLevelName()); + File metadataFile = new File(levelDirectory, BackupMetadata.metadataFileName); + if (metadataFile.exists()) { + // Metadata exists. Deserialize. + BackupMetadata metadata = null; + FileInputStream fileInputStream = new FileInputStream(metadataFile); + ObjectInputStream in = new ObjectInputStream(fileInputStream); + metadata = (BackupMetadata) in.readObject(); + in.close(); + fileInputStream.close(); + + // Print metadata + info("Recovered from previous backup:"); + info("Backup Name: " + metadata.getBackupName()); + info("Create Time: " + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")).format(new Date(metadata.getBackupTime()))); + + // Delete metadata file + if (!metadataFile.delete()) { + System.gc(); + forceDelete(metadataFile); + } + } + } catch (IOException | ClassNotFoundException ignored) { + } + } +} diff --git a/src/main/java/com/keuin/kbackupfabric/KBPluginInitializer.java b/src/main/java/com/keuin/kbackupfabric/KBPluginInitializer.java deleted file mode 100644 index c3582c7..0000000 --- a/src/main/java/com/keuin/kbackupfabric/KBPluginInitializer.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.keuin.kbackupfabric; - -import net.fabricmc.api.ModInitializer; -import net.fabricmc.fabric.api.registry.CommandRegistry; - -public final class KBPluginInitializer implements ModInitializer { - @Override - public void onInitialize() { - System.out.println("Initializing KBackup..."); - CommandRegistry.INSTANCE.register(false, KBCommandRegister::register); - } -} diff --git a/src/main/java/com/keuin/kbackupfabric/KBRegister.java b/src/main/java/com/keuin/kbackupfabric/KBRegister.java new file mode 100644 index 0000000..11f05a1 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/KBRegister.java @@ -0,0 +1,32 @@ +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 final class KBRegister { + // First make method to register + public static void registerCommands(CommandDispatcher dispatcher) { + // register /kb and /kb help for help menu + dispatcher.register(CommandManager.literal("kb").executes(KBCommands::help)); + dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("help").executes(KBCommands::help))); + + // register /kb list for showing the backup list. OP is required. + dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("list").requires(PermissionValidator::op).executes(KBCommands::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(PermissionValidator::op).executes(KBCommands::backup)).requires(PermissionValidator::op).executes(KBCommands::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(PermissionValidator::op).executes(KBCommands::restore)).executes(KBCommands::list))); + + // register /kb confirm for confirming the execution. OP is required. + dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("confirm").requires(PermissionValidator::op).executes(KBCommands::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(KBCommands::cancel))); + + } +} diff --git a/src/main/java/com/keuin/kbackupfabric/data/BackupMetadata.java b/src/main/java/com/keuin/kbackupfabric/data/BackupMetadata.java new file mode 100644 index 0000000..48670c6 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/data/BackupMetadata.java @@ -0,0 +1,23 @@ +package com.keuin.kbackupfabric.data; + +import java.io.Serializable; + +public class BackupMetadata implements Serializable { + public static final String metadataFileName = "kbackup_metadata"; + private static final long serialVersionUID = 1L; + private final long BackupTime; + private final String backupName; + + public BackupMetadata(long backupTime, String backupName) { + BackupTime = backupTime; + this.backupName = backupName; + } + + public long getBackupTime() { + return BackupTime; + } + + public String getBackupName() { + return backupName; + } +} diff --git a/src/main/java/com/keuin/kbackupfabric/util/PrintUtil.java b/src/main/java/com/keuin/kbackupfabric/util/PrintUtil.java index 8c8a687..4b93d17 100644 --- a/src/main/java/com/keuin/kbackupfabric/util/PrintUtil.java +++ b/src/main/java/com/keuin/kbackupfabric/util/PrintUtil.java @@ -3,27 +3,60 @@ package com.keuin.kbackupfabric.util; import com.mojang.brigadier.context.CommandContext; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.text.LiteralText; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; 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 void message(CommandContext context, String messageText) { - message(context, messageText, false); + private static final Style infoStyle = new Style().setColor(Formatting.WHITE); + private static final Style debugStyle = new Style().setUnderline(true); + private static final Style warnStyle = new Style().setColor(Formatting.YELLOW); + private static final Style errorStyle = new Style().setColor(Formatting.DARK_RED); + + + public static CommandContext msgInfo(CommandContext context, String messageText) { + return msgInfo(context, messageText, false); + } + + public static CommandContext msgWarn(CommandContext context, String messageText) { + return msgWarn(context, messageText, false); + } + + public static CommandContext msgErr(CommandContext context, String messageText) { + return msgErr(context, messageText, false); + } + + public static CommandContext msgInfo(CommandContext context, String messageText, boolean broadcastToOps) { + return message(context, messageText, broadcastToOps, infoStyle); + } + + public static CommandContext msgWarn(CommandContext context, String messageText, boolean broadcastToOps) { + return message(context, messageText, broadcastToOps, warnStyle); + } + + public static CommandContext msgErr(CommandContext context, String messageText, boolean broadcastToOps) { + return message(context, messageText, broadcastToOps, errorStyle); } - public static CommandContext message(CommandContext context, String messageText, boolean broadcastToOps) { + public static CommandContext message(CommandContext context, String messageText, boolean broadcastToOps, Style style) { synchronized (syncMessage) { - context.getSource().sendFeedback(new LiteralText("[KBackup] " + messageText), broadcastToOps); + Text text = new LiteralText(messageText); + text.setStyle(style); + context.getSource().sendFeedback(text, broadcastToOps); } return context; } @@ -31,8 +64,8 @@ public final class PrintUtil { public static void debug(String message) { synchronized (syncDebug) { if (printDebugMessages) { - System.out.println(String.format("[DEBUG] [KBackup] %s", message)); - LOGGER.debug(message); + //System.out.println(String.format("[DBG] [KB] %s", message)); + LOGGER.debug("[KB][DEBUG] " + message); } } } @@ -40,8 +73,8 @@ public final class PrintUtil { public static void error(String message) { synchronized (syncError) { if (printErrorMessages) { - System.out.println(String.format("[ERROR] [KBackup] %s", message)); - LOGGER.error(message); + //System.out.println(String.format("[ERR] [KB] %s", message)); + LOGGER.error("[KB][ERROR]" + message); } } } @@ -49,8 +82,8 @@ public final class PrintUtil { public static void info(String message) { synchronized (syncInfo) { if (printInfoMessages) { - System.out.println(String.format("[INFO] [KBackup] %s", message)); - LOGGER.info(message); + //System.out.println(String.format("[INF] [KB] %s", message)); + LOGGER.info("[KB][INFO] " + message); } } } diff --git a/src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java b/src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java index fec4436..5297dda 100644 --- a/src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java +++ b/src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java @@ -1,5 +1,7 @@ package com.keuin.kbackupfabric.util; +import com.keuin.kbackupfabric.data.BackupMetadata; + import java.io.*; import java.util.Enumeration; import java.util.zip.*; @@ -9,12 +11,12 @@ public final class ZipUtil { /** * 递归压缩文件夹 * - * @param srcRootDir 压缩文件夹根目录的子路径 - * @param file 当前递归压缩的文件或目录对象 - * @param zos 压缩文件存储对象 + * @param srcRootDir 压缩文件夹根目录的子路径 + * @param file 当前递归压缩的文件或目录对象 + * @param zipOutputStream 压缩文件存储对象 * @throws IOException IO Error */ - private static void zip(String srcRootDir, File file, ZipOutputStream zos) throws IOException { + private static void zip(String srcRootDir, File file, ZipOutputStream zipOutputStream) throws IOException { if (file == null) { return; } @@ -31,13 +33,13 @@ public final class ZipUtil { subPath = subPath.substring(srcRootDir.length() + File.separator.length()); } ZipEntry entry = new ZipEntry(subPath); - zos.putNextEntry(entry); - BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); - while ((count = bis.read(data, 0, bufferLen)) != -1) { - zos.write(data, 0, count); + zipOutputStream.putNextEntry(entry); + BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file)); + while ((count = inputStream.read(data, 0, bufferLen)) != -1) { + zipOutputStream.write(data, 0, count); } - bis.close(); - zos.closeEntry(); + inputStream.close(); + zipOutputStream.closeEntry(); } // 如果是目录,则压缩整个目录 else { @@ -45,11 +47,16 @@ public final class ZipUtil { File[] childFileList = file.listFiles(); if (childFileList != null) { for (File value : childFileList) - zip(srcRootDir, value, zos); + zip(srcRootDir, value, zipOutputStream); } } } +// public static void makeZipBackup(String srcPath, String zipPath, String zipFileName) throws IOException, ZipUtilException { +// zip(srcPath, zipPath, zipFileName, null); +// } + + /** * 对文件或文件目录进行压缩 * @@ -59,12 +66,12 @@ public final class ZipUtil { * @throws IOException IO Error * @throws ZipUtilException General exception, such as loop recursion or invalid input. */ - public static void zip(String srcPath, String zipPath, String zipFileName) throws IOException, ZipUtilException { + public static void makeBackupZip(String srcPath, String zipPath, String zipFileName, BackupMetadata backupMetadata) throws IOException, ZipUtilException { if (srcPath.isEmpty() || zipPath.isEmpty() || zipFileName.isEmpty()) { throw new ZipUtilException("Parameter for zip() contains null."); } - CheckedOutputStream cos; - ZipOutputStream zos = null; + CheckedOutputStream checkedOutputStream; + ZipOutputStream zipOutputStream = null; try { File srcFile = new File(srcPath); @@ -94,8 +101,18 @@ public final class ZipUtil { } } - cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32()); - zos = new ZipOutputStream(cos); + checkedOutputStream = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32()); + zipOutputStream = new ZipOutputStream(checkedOutputStream); + + // If with backup metadata, we serialize it and write it into file "kbackup_metadata" + ZipEntry metadataEntry = new ZipEntry(BackupMetadata.metadataFileName); + zipOutputStream.putNextEntry(metadataEntry); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(backupMetadata); + oos.close(); + zipOutputStream.write(baos.toByteArray()); + zipOutputStream.closeEntry(); //如果只是压缩一个文件,则需要截取该文件的父目录 String srcRootDir = srcPath; @@ -106,12 +123,12 @@ public final class ZipUtil { } } //调用递归压缩方法进行目录或文件压缩 - zip(srcRootDir, srcFile, zos); - zos.flush(); + zip(srcRootDir, srcFile, zipOutputStream); + zipOutputStream.flush(); } finally { try { - if (zos != null) { - zos.close(); + if (zipOutputStream != null) { + zipOutputStream.close(); } } catch (Exception e) { e.printStackTrace(); @@ -126,7 +143,6 @@ public final class ZipUtil { * @param unzipFilePath 解压后的文件保存的路径 * @param includeZipFileName 解压后的文件保存的路径是否包含压缩文件的文件名。true-包含;false-不包含 */ - @SuppressWarnings("unchecked") public static void unzip(String zipFilePath, String unzipFilePath, boolean includeZipFileName) throws ZipUtilException, IOException { if (zipFilePath.isEmpty() || unzipFilePath.isEmpty()) { throw new ZipUtilException("Parameter for unzip() contains null."); diff --git a/src/main/java/com/keuin/kbackupfabric/worker/BackupWorker.java b/src/main/java/com/keuin/kbackupfabric/worker/BackupWorker.java index 30fd8b5..0ca9015 100644 --- a/src/main/java/com/keuin/kbackupfabric/worker/BackupWorker.java +++ b/src/main/java/com/keuin/kbackupfabric/worker/BackupWorker.java @@ -1,5 +1,7 @@ package com.keuin.kbackupfabric.worker; +import com.keuin.kbackupfabric.data.BackupMetadata; +import com.keuin.kbackupfabric.util.PrintUtil; import com.keuin.kbackupfabric.util.ZipUtil; import com.keuin.kbackupfabric.util.ZipUtilException; import com.mojang.brigadier.context.CommandContext; @@ -14,7 +16,7 @@ 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; +import static com.keuin.kbackupfabric.util.PrintUtil.msgInfo; /** * The backup worker @@ -24,18 +26,21 @@ public final class BackupWorker implements Runnable { private final CommandContext context; private final String backupName; private final Map oldWorldsSavingDisabled; + private final BackupMetadata backupMetadata; + private final long startTime; - - private BackupWorker(CommandContext context, String backupName, Map oldWorldsSavingDisabled) { + private BackupWorker(CommandContext context, String backupName, Map oldWorldsSavingDisabled, BackupMetadata backupMetadata, long startTime) { this.context = context; this.backupName = backupName; this.oldWorldsSavingDisabled = oldWorldsSavingDisabled; + this.backupMetadata = backupMetadata; + this.startTime = startTime; } - public static void invoke(CommandContext context, String backupName) { + public static void invoke(CommandContext context, String backupName, BackupMetadata backupMetadata) { //// Save world, save old autosave configs - message(context, String.format("Making backup %s, please wait ...", backupName), true); + PrintUtil.msgInfo(context, String.format("Making backup %s, please wait ...", backupName), true); Map oldWorldsSavingDisabled = new HashMap<>(); // old switch stat // Get server @@ -54,39 +59,52 @@ public final class BackupWorker implements Runnable { server.save(true, true, true); // Start threaded worker - BackupWorker worker = new BackupWorker(context, backupName, oldWorldsSavingDisabled); + BackupWorker worker = new BackupWorker(context, backupName, oldWorldsSavingDisabled, backupMetadata, System.currentTimeMillis()); Thread workerThread = new Thread(worker, "BackupWorker"); workerThread.start(); } @Override public void run() { - String destPathFolderName = ""; + String backupSaveDirectory = ""; 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)); + File backupSaveDirectoryFile = getBackupSaveDirectory(server); + backupSaveDirectory = backupSaveDirectoryFile.getName(); + if (!backupSaveDirectoryFile.isDirectory() && !backupSaveDirectoryFile.mkdir()) { + msgInfo(context, String.format("Failed to create backup saving directory: %s. Failed to backup.", backupSaveDirectory)); 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)); + String backupFileName = getBackupFileName(backupName); + debug(String.format("zip(srcPath=%s, destPath=%s)", levelPath, backupSaveDirectoryFile.toString())); + ZipUtil.makeBackupZip(levelPath, backupSaveDirectoryFile.toString(), backupFileName, backupMetadata); + File backupZipFile = new File(backupSaveDirectoryFile, backupFileName); // Restore old autosave switch stat server.getWorlds().forEach(world -> world.savingDisabled = oldWorldsSavingDisabled.getOrDefault(world, true)); - message(context, "Done.", true); + // Print finish message: time elapsed and file size + 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)); + } catch (SecurityException ignored) { + } + } catch (SecurityException e) { - message(context, String.format("Failed to create backup saving directory: %s. Failed to backup.", destPathFolderName)); + msgInfo(context, String.format("Failed to create backup saving directory: %s. Failed to backup.", backupSaveDirectory)); } catch (IOException | ZipUtilException e) { - message(context, "Failed to make zip: " + e.getMessage()); + msgInfo(context, "Failed to make zip: " + e.getMessage()); } } } diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 18dfd31..ce26dd2 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -16,7 +16,7 @@ "environment": "*", "entrypoints": { "main": [ - "com.keuin.kbackupfabric.KBPluginInitializer" + "com.keuin.kbackupfabric.KBPluginEvents" ] }, "mixins": [ -- cgit v1.2.3