diff options
author | Keuin <[email protected]> | 2020-11-30 13:15:15 +0800 |
---|---|---|
committer | keuin <[email protected]> | 2020-11-30 13:15:15 +0800 |
commit | 61ade9adad4668494dfc32da7751e1b86251dc97 (patch) | |
tree | c220fe41d0691d7ad6ed08e51d351537fa220a4f /src | |
parent | e59bdf4ac0b66ce683be1bbc36852bfce6f70a63 (diff) |
Implementing incremental backup
Diffstat (limited to 'src')
22 files changed, 446 insertions, 257 deletions
diff --git a/src/main/java/com/keuin/kbackupfabric/KBCommands.java b/src/main/java/com/keuin/kbackupfabric/KBCommands.java index 9568842..bc04291 100644 --- a/src/main/java/com/keuin/kbackupfabric/KBCommands.java +++ b/src/main/java/com/keuin/kbackupfabric/KBCommands.java @@ -1,14 +1,16 @@ package com.keuin.kbackupfabric; -import com.keuin.kbackupfabric.metadata.BackupMetadata; import com.keuin.kbackupfabric.metadata.MetadataHolder; import com.keuin.kbackupfabric.operation.BackupOperation; import com.keuin.kbackupfabric.operation.DeleteOperation; import com.keuin.kbackupfabric.operation.RestoreOperation; import com.keuin.kbackupfabric.operation.abstracts.i.Invokable; import com.keuin.kbackupfabric.operation.backup.BackupMethod; +import com.keuin.kbackupfabric.operation.backup.IncrementalBackupMethod; +import com.keuin.kbackupfabric.operation.backup.PrimitiveBackupMethod; import com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil; -import com.keuin.kbackupfabric.util.backup.BackupNameSuggestionProvider; +import com.keuin.kbackupfabric.util.backup.BackupType; +import com.keuin.kbackupfabric.util.backup.suggestion.BackupNameSuggestionProvider; import com.keuin.kbackupfabric.util.backup.BackupNameTimeFormatter; import com.keuin.kbackupfabric.util.PrintUtil; import com.mojang.brigadier.arguments.StringArgumentType; @@ -27,11 +29,13 @@ public final class KBCommands { private static final int SUCCESS = 1; private static final int FAILED = -1; + private static final String DEFAULT_BACKUP_NAME = "noname"; //private static final Logger LOGGER = LogManager.getLogger(); private static final List<String> backupNameList = new ArrayList<>(); // index -> backupName private static Invokable pendingOperation = null; + //private static BackupMethod activatedBackupMethod = new PrimitiveBackupMethod(); // The backup method we currently using /** * Print the help menu. @@ -41,12 +45,12 @@ public final class KBCommands { */ public static int help(CommandContext<ServerCommandSource> 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 <backup_name> 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."); + msgInfo(context, "/kb , /kb help - Print help menu."); + msgInfo(context, "/kb list - Show all backups."); + msgInfo(context, "/kb backup [incremental/zip] [backup_name] - Backup the whole level to backup_name. The default name is current system time."); + msgInfo(context, "/kb restore <backup_name> - 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; } @@ -103,7 +107,7 @@ public final class KBCommands { * @param context the context. * @return stat code. */ - public static int backup(CommandContext<ServerCommandSource> context) { + public static int primitiveBackup(CommandContext<ServerCommandSource> context) { //KBMain.backup("name") String backupName = StringArgumentType.getString(context, "backupName"); if (backupName.matches("[0-9]*")) { @@ -111,7 +115,32 @@ public final class KBCommands { backupName = String.format("a%s", backupName); msgWarn(context, String.format("Pure numeric name is not allowed. Renaming to %s", backupName)); } - return doBackup(context, backupName, BackupMethod); + return doBackup(context, backupName, PrimitiveBackupMethod.getInstance()); + } + + /** + * Backup with default name. + * + * @param context the context. + * @return stat code. + */ + public static int primitiveBackupWithDefaultName(CommandContext<ServerCommandSource> context) { + return doBackup(context, DEFAULT_BACKUP_NAME, PrimitiveBackupMethod.getInstance()); + } + + public static int incrementalBackup(CommandContext<ServerCommandSource> 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. Renaming to %s", backupName)); + } + return doBackup(context, backupName, IncrementalBackupMethod.getInstance()); + } + + public static int incrementalBackupWithDefaultName(CommandContext<ServerCommandSource> context) { + return doBackup(context, DEFAULT_BACKUP_NAME, IncrementalBackupMethod.getInstance()); } /** @@ -168,6 +197,9 @@ public final class KBCommands { return FAILED; } + // Detect backup type + + // Update pending task //pendingOperation = AbstractConfirmableOperation.createRestoreOperation(context, backupName); File backupFile = new File(getBackupSaveDirectory(server), getBackupFileName(backupName)); @@ -177,17 +209,6 @@ public final class KBCommands { return SUCCESS; } - - /** - * Backup with default name. - * - * @param context the context. - * @return stat code. - */ - public static int backupWithDefaultName(CommandContext<ServerCommandSource> context) { - return doBackup(context, "noname"); - } - private static int doBackup(CommandContext<ServerCommandSource> context, String customBackupName, BackupMethod backupMethod) { // Real backup name (compatible with legacy backup): date_name, such as 2020-04-23_21-03-00_test //KBMain.backup("name") @@ -287,6 +308,26 @@ public final class KBCommands { return SUCCESS; } + /** + * Select the backup method we use. + * @param context the context. + * @return stat code. + */ + public static int setMethod(CommandContext<ServerCommandSource> context) { + String desiredMethodName = StringArgumentType.getString(context, "backupMethod"); + List<BackupType> backupMethods = Arrays.asList(BackupType.PRIMITIVE_ZIP_BACKUP, BackupType.OBJECT_TREE_BACKUP); + for (BackupType method : backupMethods) { + if(method.getName().equals(desiredMethodName)) { + // Incremental backup +// activatedBackupMethod = + msgInfo(context, String.format("Backup method is set to: %s", desiredMethodName)); + return SUCCESS; + } + } + + return SUCCESS; + } + private static String parseBackupName(CommandContext<ServerCommandSource> context, String userInput) { try { diff --git a/src/main/java/com/keuin/kbackupfabric/KBCommandRegister.java b/src/main/java/com/keuin/kbackupfabric/KBCommandsRegister.java index bfbe3c1..3376ac9 100644 --- a/src/main/java/com/keuin/kbackupfabric/KBCommandRegister.java +++ b/src/main/java/com/keuin/kbackupfabric/KBCommandsRegister.java @@ -1,13 +1,14 @@ package com.keuin.kbackupfabric; -import com.keuin.kbackupfabric.util.backup.BackupNameSuggestionProvider; +import com.keuin.kbackupfabric.util.backup.suggestion.BackupMethodSuggestionProvider; +import com.keuin.kbackupfabric.util.backup.suggestion.BackupNameSuggestionProvider; 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 { +public final class KBCommandsRegister { // First make method to register public static void registerCommands(CommandDispatcher<ServerCommandSource> dispatcher) { @@ -18,8 +19,24 @@ public final class KBCommandRegister { // 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 zip [name] as a alias + dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("backup").then( + CommandManager.literal("zip").then( + CommandManager.argument("backupName", StringArgumentType.greedyString()).requires(PermissionValidator::op).executes(KBCommands::primitiveBackup) + ).requires(PermissionValidator::op).executes(KBCommands::primitiveBackupWithDefaultName))) + ); + + // register /kb backup incremental [name] + dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("backup").then( + CommandManager.literal("incremental").then( + CommandManager.argument("backupName", StringArgumentType.greedyString()).requires(PermissionValidator::op).executes(KBCommands::incrementalBackup) + ).requires(PermissionValidator::op).executes(KBCommands::incrementalBackupWithDefaultName))) + ); + // register /kb backup [name] for performing backup. OP is required. - dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("backup").then(CommandManager.argument("backupName", StringArgumentType.greedyString()).requires(PermissionValidator::op).executes(KBCommands::backup)).requires(PermissionValidator::op).executes(KBCommands::backupWithDefaultName))); + dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("backup").then( + CommandManager.argument("backupName", StringArgumentType.greedyString()).requires(PermissionValidator::op).executes(KBCommands::primitiveBackup) + ).requires(PermissionValidator::op).executes(KBCommands::primitiveBackupWithDefaultName))); // register /kb restore <name> for performing restore. OP is required. dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("restore").then(CommandManager.argument("backupName", StringArgumentType.greedyString()).suggests(BackupNameSuggestionProvider.getProvider()).requires(PermissionValidator::op).executes(KBCommands::restore)).executes(KBCommands::list))); @@ -35,5 +52,8 @@ public final class KBCommandRegister { // register /kb prev for showing the latest backup. dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("prev").requires(PermissionValidator::op).executes(KBCommands::prev))); + + // register /kb setMethod for selecting backup method (zip, incremental) + dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("setMethod").then(CommandManager.argument("backupMethod", StringArgumentType.string()).suggests(BackupMethodSuggestionProvider.getProvider()).requires(PermissionValidator::op).executes(KBCommands::setMethod)))); } } diff --git a/src/main/java/com/keuin/kbackupfabric/KBPluginEvents.java b/src/main/java/com/keuin/kbackupfabric/KBPluginEvents.java index d67d85b..3cb2dbe 100644 --- a/src/main/java/com/keuin/kbackupfabric/KBPluginEvents.java +++ b/src/main/java/com/keuin/kbackupfabric/KBPluginEvents.java @@ -3,7 +3,7 @@ package com.keuin.kbackupfabric; import com.keuin.kbackupfabric.metadata.BackupMetadata; import com.keuin.kbackupfabric.metadata.MetadataHolder; import com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil; -import com.keuin.kbackupfabric.util.backup.BackupNameSuggestionProvider; +import com.keuin.kbackupfabric.util.backup.suggestion.BackupNameSuggestionProvider; import com.keuin.kbackupfabric.util.PrintUtil; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.event.server.ServerStartCallback; @@ -30,7 +30,7 @@ public final class KBPluginEvents implements ModInitializer, ServerStartCallback @Override public void onInitialize() { System.out.println("Binding events and commands ..."); - CommandRegistry.INSTANCE.register(false, KBCommandRegister::registerCommands); + CommandRegistry.INSTANCE.register(false, KBCommandsRegister::registerCommands); ServerStartCallback.EVENT.register(this); } @@ -58,7 +58,7 @@ public final class KBPluginEvents implements ModInitializer, ServerStartCallback // Print metadata MetadataHolder.setMetadata(metadata); - PrintUtil.info("Restored from a previous backup:"); + PrintUtil.info("Restored world from a previous backup:"); PrintUtil.info("Backup Name: " + metadata.getBackupName()); PrintUtil.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/Random.java b/src/main/java/com/keuin/kbackupfabric/Random.java deleted file mode 100644 index e87e4c1..0000000 --- a/src/main/java/com/keuin/kbackupfabric/Random.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.keuin.kbackupfabric; - -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; - -public class Random { - public int random(int A, int B) { - return (int) (A + (B - A + 1) * Math.random()); - } - - public double[] randomArray(int MaxN, int lowA, int highA) { - double[] array = new double[random(1,MaxN)]; - for (int i = 0; i < array.length; i++) { - array[i] = lowA + (highA - lowA) * Math.random(); - } - return array; - } - - public double[] generate(int MaxN, int lowA, int highA) { - double[] testData = randomArray(MaxN,lowA,highA); - double x = testData[random(0,testData.length-1)]; - } - - public int R(double[] a, double x) { - List<Integer> list = new LinkedList<>(); - int k; - for (int i = 0; i < a.length; i++) { - if(Math.abs(a[i] - x) < 1e-6) - list.add(i); - } - assert !list.isEmpty(); - k = list.get(list.size() / 2); - return k; - } - - - public boolean domR(double[] a, double x) { - for (double v : a) { - if(Math.abs(v - x) < 1e-6) - return true; - } - return false; - } - - public boolean oracle(double[] a, double x, int k) { - if(!(Math.abs(a[k] - x) < 1e-6)) - return false; - int c1=0,c2=0; - for (int i = 0; i < k; i++) { - if(Math.abs(a[i] - x) < 1e-6) - ++c1; - } - for (int i = k; i < a.length; i++) { - if(Math.abs(a[i] - x) < 1e-6) - ++c2; - } - return c1 - c2 >= -1 && c1 - c2 <= 1; - } - - public boolean driver() { - int MaxN = 80, lowA = 20, highA = 50; - double[] a = randomArray(MaxN,lowA,highA); - double x = a[random(0,a.length-1)]; - return oracle(a,x,search(a,x)); - } - - - public int search(double[] a, double x) { - int counter = 0; - for (double i : a) { - if(Math.abs(i - x) < 1e-6) - ++counter; - } - counter /= 2; - for (int i = 0; i < a.length; i++) { - if(Math.abs(a[i] - x) < 1e-6) { - --counter; - if(counter == 0) - return i; - } - } - return -1; - } - - public static void Main() { - int pass = 0, fail = 0; - for (int i = 0; i < 10000; i++) { - if(driver()) - pass++; - else - fail++; - } - System.out.println(String.format("pass: %d, fail: %d.",pass,fail)); - } - -} diff --git a/src/main/java/com/keuin/kbackupfabric/operation/BackupOperation.java b/src/main/java/com/keuin/kbackupfabric/operation/BackupOperation.java index 071726b..d03c347 100644 --- a/src/main/java/com/keuin/kbackupfabric/operation/BackupOperation.java +++ b/src/main/java/com/keuin/kbackupfabric/operation/BackupOperation.java @@ -8,7 +8,6 @@ import com.keuin.kbackupfabric.util.PrintUtil; import com.keuin.kbackupfabric.util.backup.builder.BackupFileNameBuilder; import com.keuin.kbackupfabric.util.backup.formatter.BackupFileNameFormatter; import com.mojang.brigadier.context.CommandContext; -import com.sun.istack.internal.NotNull; import net.minecraft.server.MinecraftServer; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.world.World; @@ -30,7 +29,7 @@ public class BackupOperation extends InvokableAsyncBlockingOperation { private long startTime; - public BackupOperation(@NotNull CommandContext<ServerCommandSource> context, @NotNull String backupName, @NotNull BackupMethod backupMethod, @NotNull BackupFileNameBuilder backupFileNameBuilder, @NotNull BackupFileNameFormatter backupFileNameFormatter) { + public BackupOperation(CommandContext<ServerCommandSource> context, String backupName, BackupMethod backupMethod) { super("BackupWorker"); this.context = context; this.backupName = backupName; @@ -56,7 +55,8 @@ public class BackupOperation extends InvokableAsyncBlockingOperation { String levelPath = getLevelPath(server); String backupFileName = getBackupFileName(backupName); - if(backupMethod.backup(backupName,levelPath,backupSaveDirectory)) { + BackupMethod.BackupResult result = backupMethod.backup(backupName,levelPath,backupSaveDirectory); + if(result.isSuccess()) { // Restore old autosave switch stat server.getWorlds().forEach(world -> world.savingDisabled = oldWorldsSavingDisabled.getOrDefault(world, true)); @@ -64,10 +64,7 @@ public class BackupOperation extends InvokableAsyncBlockingOperation { long timeElapsedMillis = System.currentTimeMillis() - startTime; String msgText = String.format("Backup finished. Time elapsed: %.2fs.", timeElapsedMillis / 1000.0); File backupZipFile = new File(backupSaveDirectory, backupFileName); - try { - msgText += String.format(" File size: %s.", humanFileSize(backupZipFile.length())); - } catch (SecurityException ignored) { - } + msgText += String.format(" File size: %s.", humanFileSize(result.getBackupSizeBytes())); PrintUtil.msgInfo(context, msgText, true); } else { // failed diff --git a/src/main/java/com/keuin/kbackupfabric/operation/DeleteOperation.java b/src/main/java/com/keuin/kbackupfabric/operation/DeleteOperation.java index 30fdfc0..444ca9a 100644 --- a/src/main/java/com/keuin/kbackupfabric/operation/DeleteOperation.java +++ b/src/main/java/com/keuin/kbackupfabric/operation/DeleteOperation.java @@ -1,7 +1,7 @@ package com.keuin.kbackupfabric.operation; import com.keuin.kbackupfabric.operation.abstracts.InvokableAsyncBlockingOperation; -import com.keuin.kbackupfabric.util.backup.BackupNameSuggestionProvider; +import com.keuin.kbackupfabric.util.backup.suggestion.BackupNameSuggestionProvider; import com.keuin.kbackupfabric.util.PrintUtil; import com.mojang.brigadier.context.CommandContext; import net.minecraft.server.MinecraftServer; diff --git a/src/main/java/com/keuin/kbackupfabric/operation/RestoreOperation.java b/src/main/java/com/keuin/kbackupfabric/operation/RestoreOperation.java index ef6ab2b..22397a1 100644 --- a/src/main/java/com/keuin/kbackupfabric/operation/RestoreOperation.java +++ b/src/main/java/com/keuin/kbackupfabric/operation/RestoreOperation.java @@ -84,54 +84,21 @@ public class RestoreOperation extends InvokableBlockingOperation { } } - PrintUtil.info("Wait for 5 seconds ..."); - try { - Thread.sleep(5000); - } catch (InterruptedException ignored) { - } - - // Delete old level - PrintUtil.info("Server stopped. Deleting old level ..."); - File levelDirFile = new File(levelDirectory); - long startTime = System.currentTimeMillis(); - - 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); + int cnt = 5; + do { + PrintUtil.info(String.format("Wait %d seconds ...", cnt)); + try{ + Thread.sleep(1000); } catch (InterruptedException ignored) { } - } - if (levelDirFile.exists()) { - PrintUtil.error(String.format("Cannot restore: failed to delete old level %s .", levelDirFile.getName())); - return; - } + }while(--cnt > 0); - // Decompress archive - PrintUtil.info("Decompressing archived level ..."); - ZipUtil.unzip(backupFilePath, levelDirectory, false); - long endTime = System.currentTimeMillis(); - PrintUtil.info(String.format("Restore complete! (%.2fs) Please restart the server manually.", (endTime - startTime) / 1000.0)); - PrintUtil.info("If you want to restart automatically after restoring, please visit the project manual at: https://github.com/keuin/KBackup-Fabric/blob/master/README.md"); - - try { - Thread.sleep(1000); - } catch (InterruptedException ignored) { - } + //////////////////// //ServerRestartUtil.forkAndRestart(); System.exit(111); - } catch (SecurityException | IOException | ZipUtilException e) { + } catch (SecurityException e) { PrintUtil.error("An exception occurred while restoring: " + e.getMessage()); } } diff --git a/src/main/java/com/keuin/kbackupfabric/operation/backup/BackupMethod.java b/src/main/java/com/keuin/kbackupfabric/operation/backup/BackupMethod.java index b0b77cb..4e9eb6c 100644 --- a/src/main/java/com/keuin/kbackupfabric/operation/backup/BackupMethod.java +++ b/src/main/java/com/keuin/kbackupfabric/operation/backup/BackupMethod.java @@ -2,10 +2,12 @@ package com.keuin.kbackupfabric.operation.backup; import com.keuin.kbackupfabric.util.backup.builder.BackupFileNameBuilder; import com.keuin.kbackupfabric.util.backup.formatter.BackupFileNameFormatter; -import com.sun.istack.internal.NotNull; import java.io.IOException; +/** + * Provide specific backup method, which is implemented statelessly. + */ public interface BackupMethod { /** @@ -14,10 +16,29 @@ public interface BackupMethod { * @param backupName the backup name. * @return if the backup operation succeed. */ - boolean backup(@NotNull String backupName, @NotNull String levelPath, @NotNull String backupSaveDirectory) throws IOException; + BackupResult backup(String backupName, String levelPath, String backupSaveDirectory) throws IOException; + + boolean restore(String backupName, String levelPath, String backupSaveDirectory) throws IOException; BackupFileNameBuilder getBackupFileNameBuilder(); BackupFileNameFormatter getBackupFileNameFormatter(); + class BackupResult { + private final boolean success; + private final long backupSizeBytes; + + public BackupResult(boolean success, long backupSizeBytes) { + this.success = success; + this.backupSizeBytes = backupSizeBytes; + } + + public boolean isSuccess() { + return success; + } + + public long getBackupSizeBytes() { + return backupSizeBytes; + } + } } diff --git a/src/main/java/com/keuin/kbackupfabric/operation/backup/IncrementalBackupMethod.java b/src/main/java/com/keuin/kbackupfabric/operation/backup/IncrementalBackupMethod.java new file mode 100644 index 0000000..4a87bb3 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/operation/backup/IncrementalBackupMethod.java @@ -0,0 +1,62 @@ +package com.keuin.kbackupfabric.operation.backup; + +import com.google.gson.JsonObject; +import com.keuin.kbackupfabric.util.PrintUtil; +import com.keuin.kbackupfabric.util.backup.builder.BackupFileNameBuilder; +import com.keuin.kbackupfabric.util.backup.builder.ObjectTreeBackupFileNameBuilder; +import com.keuin.kbackupfabric.util.backup.formatter.BackupFileNameFormatter; +import com.keuin.kbackupfabric.util.backup.formatter.ObjectTreeBackupFileNameFormatter; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.time.LocalDateTime; + +public class IncrementalBackupMethod implements BackupMethod { + + private static final IncrementalBackupMethod INSTANCE = new IncrementalBackupMethod(); + + public static IncrementalBackupMethod getInstance() { + return INSTANCE; + } + + @Override + public BackupResult backup(String backupName, String levelPath, String backupSaveDirectory) throws IOException { + /* + 1. Analyze the save directory, to get a json containing md5 values of all files. + 2. Copy new files which we do not have in our backup repository. + 3. Save the above json as a backup file. When restoring from this, + what we have to do is just copy all files back from the repository, + based on their md5 digests. + */ + + boolean success = true; + // Generate JSON + JsonObject hashJson = IncrementalBackupUtil.generateDirectoryJsonObject(levelPath); + // Copy files + long newFilesSizeBytes = IncrementalBackupUtil.saveNewFiles(backupSaveDirectory, levelPath, hashJson); + if(newFilesSizeBytes < 0) { + success = false; + PrintUtil.error("Failed to copy new files to object tree."); + } + // Save JSON tree + File jsonFile = new File(String.valueOf(Paths.get(backupSaveDirectory, BackupFileNameBuilder.objectTreeBackup().build(LocalDateTime.now(), backupName)))); + // TODO + return new BackupResult(success, newFilesSizeBytes); + } + + @Override + public boolean restore(String backupName, String levelPath, String backupSaveDirectory) throws IOException { + return false; + } + + @Override + public BackupFileNameBuilder getBackupFileNameBuilder() { + return null; + } + + @Override + public BackupFileNameFormatter getBackupFileNameFormatter() { + return null; + } +} diff --git a/src/main/java/com/keuin/kbackupfabric/operation/backup/IncrementalBackupUtil.java b/src/main/java/com/keuin/kbackupfabric/operation/backup/IncrementalBackupUtil.java new file mode 100644 index 0000000..f90aef1 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/operation/backup/IncrementalBackupUtil.java @@ -0,0 +1,90 @@ +package com.keuin.kbackupfabric.operation.backup; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.keuin.kbackupfabric.util.FilesystemUtil; +import org.apache.commons.codec.digest.DigestUtils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.*; +import java.util.Map; + +public class IncrementalBackupUtil { + /** + * Generate a json object representing a directory and its all sub files and directories. + * @param path path to the directory. + * @return a json object. + */ + public static JsonObject generateDirectoryJsonObject(String path) throws IOException { + JsonObject json = new JsonObject(); + File directory = new File(path); + if (!(directory.isDirectory() && directory.exists())) + throw new IOException(String.format("Path %s is not a valid directory.", path)); + + // Iterate all sub files using BFS. + try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(Paths.get(path))) { + for (Path sub : directoryStream) { + if (sub.toFile().isFile()) { + // A sub file + // Just hash and add it as a string + try (InputStream is = Files.newInputStream(sub)) { + String md5 = org.apache.commons.codec.digest.DigestUtils.md5Hex(is); + json.addProperty(sub.getFileName().toString(), md5); + } + } else { + // A sub directory + // Search into + json.addProperty(String.valueOf(sub.getFileName()), sub.toString()); + } + } + } + + return json; + } + + /** + * Save new (or modified) files to target path, based on hash json. + * @param targetSavePath where we should save new files. + * @param sourcePath where new files come from. This path must be the base directory of given hash json. + * @param hashJson the json object obtained by calling generateDirectoryJsonObject method. + * @return total size of new files. If failed, will return -1. + */ + public static long saveNewFiles(String targetSavePath, String sourcePath, JsonObject hashJson) throws IOException { + long bytesCopied = 0; + for (Map.Entry<String, JsonElement> entry : hashJson.entrySet()) { + String key = entry.getKey(); + JsonElement value = entry.getValue(); + if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isString()) { + // A sub file + // key is file name + // value is file md5 + String md5 = value.getAsJsonPrimitive().getAsString(); + File saveTarget = new File(targetSavePath, md5); + if (!saveTarget.exists()) { + // Target file does not exist. We have to copy this to the target. + File sourceFile = new File(sourcePath, key); + Files.copy(sourceFile.toPath(), saveTarget.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); + try { + bytesCopied += sourceFile.length(); + } catch (SecurityException ignored) { + // failed to get the file size. Just ignore this. + } + } + } else if (value.isJsonObject()) { + // A sub directory + // key is directory name + // value is directory json object + // Go into + if(!value.isJsonObject()) + throw new IllegalArgumentException(String.format("Hash json contains illegal argument of a directory item: %s -> %s.", key, value)); + Path pathSource = Paths.get(sourcePath, key); + bytesCopied += saveNewFiles(targetSavePath, pathSource.toString(), value.getAsJsonObject()); + } else { + throw new IllegalArgumentException(String.format("Hash json contains illegal element: %s -> %s.", key, value)); + } + } + return bytesCopied; + } +} diff --git a/src/main/java/com/keuin/kbackupfabric/operation/backup/PrimitiveBackupMethod.java b/src/main/java/com/keuin/kbackupfabric/operation/backup/PrimitiveBackupMethod.java index bb1c8cb..854355d 100644 --- a/src/main/java/com/keuin/kbackupfabric/operation/backup/PrimitiveBackupMethod.java +++ b/src/main/java/com/keuin/kbackupfabric/operation/backup/PrimitiveBackupMethod.java @@ -2,6 +2,7 @@ package com.keuin.kbackupfabric.operation.backup; import com.keuin.kbackupfabric.exception.ZipUtilException; import com.keuin.kbackupfabric.metadata.BackupMetadata; +import com.keuin.kbackupfabric.util.FilesystemUtil; import com.keuin.kbackupfabric.util.PrintUtil; import com.keuin.kbackupfabric.util.ZipUtil; import com.keuin.kbackupfabric.util.backup.builder.BackupFileNameBuilder; @@ -9,14 +10,23 @@ import com.keuin.kbackupfabric.util.backup.formatter.BackupFileNameFormatter; import java.io.File; import java.io.IOException; -import java.time.LocalDate; +import java.nio.file.Paths; import java.time.LocalDateTime; +import static org.apache.commons.io.FileUtils.forceDelete; + public class PrimitiveBackupMethod implements BackupMethod { + + private static final PrimitiveBackupMethod INSTANCE = new PrimitiveBackupMethod(); + + public static PrimitiveBackupMethod getInstance() { + return INSTANCE; + } + @Override - public boolean backup(String backupName, String levelPath, String backupSaveDirectory) throws IOException { + public BackupResult backup(String backupName, String levelPath, String backupSaveDirectory) throws IOException { + String backupFileName = BackupFileNameBuilder.primitiveZipBackup().build(LocalDateTime.now(),backupName); try { - String backupFileName = BackupFileNameBuilder.primitiveZipBackup().build(LocalDateTime.now(),backupName); BackupMetadata backupMetadata = new BackupMetadata(System.currentTimeMillis(), backupName); PrintUtil.info(String.format("zip(srcPath=%s, destPath=%s)", levelPath, backupSaveDirectory)); @@ -25,8 +35,54 @@ public class PrimitiveBackupMethod implements BackupMethod { } catch (ZipUtilException exception) { PrintUtil.info("Infinite recursive of directory tree detected, backup was aborted."); + return new BackupResult(false, 0); + } + + // Get backup file size and return + return new BackupResult(true, FilesystemUtil.getFileSizeBytes(backupSaveDirectory, backupFileName)); + } + + @Override + public boolean restore(String backupName, String levelDirectory, String backupSaveDirectory) throws IOException { + // Delete old level + PrintUtil.info("Server stopped. Deleting old level ..."); + File levelDirFile = new File(levelDirectory); + long startTime = System.currentTimeMillis(); + + 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()) { + PrintUtil.error(String.format("Cannot restore: failed to delete old level %s .", levelDirFile.getName())); return false; } + + // TODO: Refactor this to the concrete BackupMethod. + // Decompress archive + PrintUtil.info("Decompressing archived level ..."); + ZipUtil.unzip(Paths.get(backupSaveDirectory, backupName).toString(), levelDirectory, false); + long endTime = System.currentTimeMillis(); + PrintUtil.info(String.format("Restore complete! (%.2fs) Please restart the server manually.", (endTime - startTime) / 1000.0)); + PrintUtil.info("If you want to restart automatically after restoring, please visit the project manual at: https://github.com/keuin/KBackup-Fabric/blob/master/README.md"); + +// try { +// Thread.sleep(1000); +// } catch (InterruptedException ignored) { +// } + return true; } @@ -37,6 +93,6 @@ public class PrimitiveBackupMethod implements BackupMethod { @Override public BackupFileNameFormatter getBackupFileNameFormatter() { - return BFNF; + return BackupFileNameFormatter.primitiveZipBackup(); } } diff --git a/src/main/java/com/keuin/kbackupfabric/util/FilesystemUtil.java b/src/main/java/com/keuin/kbackupfabric/util/FilesystemUtil.java new file mode 100644 index 0000000..7f74725 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/util/FilesystemUtil.java @@ -0,0 +1,33 @@ +package com.keuin.kbackupfabric.util; + +import java.io.File; + +public class FilesystemUtil { + + /** + * Get file sizes in bytes. + * @param parentDirectory path to specific file. + * @param fileName file name. + * @return bytes. If failed, return -1. + */ + public static long getFileSizeBytes(String parentDirectory, String fileName) { + long fileSize = -1; + try{ + File backupZipFile = new File(parentDirectory, fileName); + fileSize = backupZipFile.length(); + } catch (SecurityException ignored){ + } + return fileSize; + } + + public static long getFileSizeBytes(String filePath) { + long fileSize = -1; + try{ + File backupZipFile = new File(filePath); + fileSize = backupZipFile.length(); + } catch (SecurityException ignored){ + } + return fileSize; + } + +} diff --git a/src/main/java/com/keuin/kbackupfabric/util/ServerRestartUtil.java b/src/main/java/com/keuin/kbackupfabric/util/ServerRestartUtil.java deleted file mode 100644 index 1642e5c..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/ServerRestartUtil.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.keuin.kbackupfabric.util; - -public class ServerRestartUtil { - - public static void forkAndRestart() { -// Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { -// @Override -// public void run() { -// // Here we restart the minecraft server -// StringBuilder cmd = new StringBuilder(); -// cmd.append(System.getProperty("java.home")).append(File.separator).append("bin").append(File.separator).append("java "); -// for (String jvmArg : ManagementFactory.getRuntimeMXBean().getInputArguments()) { -// cmd.append(jvmArg + " "); -// } -// cmd.append("-cp ").append(ManagementFactory.getRuntimeMXBean().getClassPath()).append(" "); -// cmd.append(MinecraftServer.class.getName()).append(" "); -// for (String arg : args) { -// cmd.append(arg).append(" "); -// } -// Runtime.getRuntime().exec(cmd.toString()); -// System.exit(0); -// } -// })); - } - - private static void startRestartThread() { -// (new Thread(() -> { -// -// -// // kill threads -// Set<Thread> threads = Thread.getAllStackTraces().keySet(); -// Thread currentThread = Thread.currentThread(); -// for (Thread t : threads) { -// if(t != currentThread && t.isAlive()) { -// t.setUncaughtExceptionHandler((t1, e) -> { -// // set empty handler -// }); -// //t.interrupt(); -// try { -// t.setDaemon(true); -// } catch (Exception ignored) { -// } -// -// t.stop(); -// } -// } -// -// try { -// Thread.sleep(5000); -// } catch (InterruptedException ignored) { -// } -// -// // restart Minecraft server -// String[] args = new String[]{}; -// MinecraftServer.main(args); -// })).start(); - } -} diff --git a/src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java b/src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java index 0ca8da3..c670cf1 100644 --- a/src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java +++ b/src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java @@ -147,9 +147,9 @@ public final class ZipUtil { * @param unzipFilePath 解压后的文件保存的路径 * @param includeZipFileName 解压后的文件保存的路径是否包含压缩文件的文件名。true-包含;false-不包含 */ - public static void unzip(String zipFilePath, String unzipFilePath, boolean includeZipFileName) throws ZipUtilException, IOException { + public static void unzip(String zipFilePath, String unzipFilePath, boolean includeZipFileName) throws IOException { if (zipFilePath.isEmpty() || unzipFilePath.isEmpty()) { - throw new ZipUtilException("Parameter for unzip() contains null."); + throw new IllegalArgumentException("Parameter for unzip() contains null."); } File zipFile = new File(zipFilePath); // 如果解压后的文件保存路径包含压缩文件的文件名,则追加该文件名到解压路径 diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/BackupType.java b/src/main/java/com/keuin/kbackupfabric/util/backup/BackupType.java index 95a32ae..d02ce77 100644 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/BackupType.java +++ b/src/main/java/com/keuin/kbackupfabric/util/backup/BackupType.java @@ -4,18 +4,30 @@ package com.keuin.kbackupfabric.util.backup; * Representing the backup type. * Should only be used in BackupFileNameBuilder and BackupFileNameFormatter */ +@Deprecated public enum BackupType { - PRIMITIVE_ZIP_BACKUP("Primitive Zip Backup"), - OBJECT_TREE_BACKUP("Object Tree Backup"); + PRIMITIVE_ZIP_BACKUP("Primitive Zip Backup", "zip"), + OBJECT_TREE_BACKUP("Object Tree Backup", "incremental"); - private final String name; - BackupType(String name) { + private final String friendlyName; // e.g. Primitive Zip Backup + private final String name; // e.g. zip + + BackupType(String friendlyName, String name) { + this.friendlyName = friendlyName; this.name = name; } + /** + * Get name used in command. + * @return name (such as "zip", "incremental"). + */ + public String getName() { + return name; + } + @Override public String toString() { - return name; + return friendlyName; } } diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/builder/BackupFileNameBuilder.java b/src/main/java/com/keuin/kbackupfabric/util/backup/builder/BackupFileNameBuilder.java index 16c598b..f57302c 100644 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/builder/BackupFileNameBuilder.java +++ b/src/main/java/com/keuin/kbackupfabric/util/backup/builder/BackupFileNameBuilder.java @@ -1,6 +1,5 @@ package com.keuin.kbackupfabric.util.backup.builder; -import com.sun.istack.internal.NotNull; import java.time.LocalDateTime; @@ -20,6 +19,6 @@ public interface BackupFileNameBuilder { * @param backupName the custom name of this backup. Note that this should be a valid file name in current file system. * @return the backup file name string. */ - String build(@NotNull LocalDateTime time, @NotNull String backupName); + String build(LocalDateTime time, String backupName); } diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/formatter/BackupFileNameFormatter.java b/src/main/java/com/keuin/kbackupfabric/util/backup/formatter/BackupFileNameFormatter.java index eae3639..a437629 100644 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/formatter/BackupFileNameFormatter.java +++ b/src/main/java/com/keuin/kbackupfabric/util/backup/formatter/BackupFileNameFormatter.java @@ -1,7 +1,6 @@ package com.keuin.kbackupfabric.util.backup.formatter; import com.keuin.kbackupfabric.util.backup.BackupNameTimeFormatter; -import com.sun.istack.internal.NotNull; import java.time.LocalDateTime; import java.util.regex.Matcher; @@ -9,7 +8,7 @@ import java.util.regex.Pattern; public interface BackupFileNameFormatter { - BackupFileName format(@NotNull String fileName); + BackupFileName format(String fileName); class BackupFileName { public final LocalDateTime time; diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/formatter/ObjectTreeBackupFileNameFormatter.java b/src/main/java/com/keuin/kbackupfabric/util/backup/formatter/ObjectTreeBackupFileNameFormatter.java index e5503a9..08805b2 100644 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/formatter/ObjectTreeBackupFileNameFormatter.java +++ b/src/main/java/com/keuin/kbackupfabric/util/backup/formatter/ObjectTreeBackupFileNameFormatter.java @@ -2,8 +2,6 @@ package com.keuin.kbackupfabric.util.backup.formatter; import com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil; import com.keuin.kbackupfabric.util.backup.BackupNameTimeFormatter; -import com.sun.istack.internal.NotNull; -import com.sun.istack.internal.Nullable; import org.spongepowered.asm.mixin.Overwrite; import java.time.LocalDateTime; @@ -19,13 +17,12 @@ public class ObjectTreeBackupFileNameFormatter implements BackupFileNameFormatte } @Override - public BackupFileNameFormatter.BackupFileName format(@NotNull String fileName) { + public BackupFileNameFormatter.BackupFileName format(String fileName) { LocalDateTime time = getTime(fileName); String name = getBackupName(fileName); return new BackupFileNameFormatter.BackupFileName(time,name); } - @Nullable private LocalDateTime getTime(String fileName) { Matcher matcher = Pattern.compile("[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}").matcher(fileName); if (matcher.find()) { diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/formatter/PrimitiveZipBackupFileNameFormatter.java b/src/main/java/com/keuin/kbackupfabric/util/backup/formatter/PrimitiveZipBackupFileNameFormatter.java index 40450eb..2d50d17 100644 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/formatter/PrimitiveZipBackupFileNameFormatter.java +++ b/src/main/java/com/keuin/kbackupfabric/util/backup/formatter/PrimitiveZipBackupFileNameFormatter.java @@ -2,8 +2,6 @@ package com.keuin.kbackupfabric.util.backup.formatter; import com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil; import com.keuin.kbackupfabric.util.backup.BackupNameTimeFormatter; -import com.sun.istack.internal.NotNull; -import com.sun.istack.internal.Nullable; import java.time.LocalDateTime; import java.util.regex.Matcher; @@ -18,13 +16,12 @@ public class PrimitiveZipBackupFileNameFormatter implements BackupFileNameFormat } @Override - public BackupFileNameFormatter.BackupFileName format(@NotNull String fileName) { + public BackupFileNameFormatter.BackupFileName format(String fileName) { LocalDateTime time = getTime(fileName); String name = getBackupName(fileName); return new BackupFileNameFormatter.BackupFileName(time,name); } - @Nullable private LocalDateTime getTime(String fileName) { Matcher matcher = Pattern.compile("[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}").matcher(fileName); if (matcher.find()) { diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/suggestion/BackupMethodSuggestionProvider.java b/src/main/java/com/keuin/kbackupfabric/util/backup/suggestion/BackupMethodSuggestionProvider.java new file mode 100644 index 0000000..320d9bf --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/util/backup/suggestion/BackupMethodSuggestionProvider.java @@ -0,0 +1,36 @@ +package com.keuin.kbackupfabric.util.backup.suggestion; + +import com.keuin.kbackupfabric.util.backup.BackupType; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.server.command.ServerCommandSource; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CompletableFuture; + +public class BackupMethodSuggestionProvider { + + private static final List<String> suggestions = Arrays.asList( + BackupType.OBJECT_TREE_BACKUP.getName(), + BackupType.PRIMITIVE_ZIP_BACKUP.getName() + ); // All backup methods + + public static SuggestionProvider<ServerCommandSource> getProvider() { + return (context, builder) -> getCompletableFuture(builder); + } + + private static CompletableFuture<Suggestions> getCompletableFuture(SuggestionsBuilder builder) { + String remaining = builder.getRemaining().toLowerCase(Locale.ROOT); + for (String string : suggestions) { // Iterate through the supplied list + if (string.toLowerCase(Locale.ROOT).startsWith(remaining)) { + builder.suggest(string); // Add every single entry to suggestions list. + } + } + return builder.buildFuture(); // Create the CompletableFuture containing all the suggestions + } + +} diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/BackupNameSuggestionProvider.java b/src/main/java/com/keuin/kbackupfabric/util/backup/suggestion/BackupNameSuggestionProvider.java index 4639e99..f6f4056 100644 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/BackupNameSuggestionProvider.java +++ b/src/main/java/com/keuin/kbackupfabric/util/backup/suggestion/BackupNameSuggestionProvider.java @@ -1,5 +1,6 @@ -package com.keuin.kbackupfabric.util.backup; +package com.keuin.kbackupfabric.util.backup.suggestion; +import com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil; import com.mojang.brigadier.suggestion.SuggestionProvider; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; diff --git a/src/test/java/IncrementalBackupUtilTest.java b/src/test/java/IncrementalBackupUtilTest.java new file mode 100644 index 0000000..b050a91 --- /dev/null +++ b/src/test/java/IncrementalBackupUtilTest.java @@ -0,0 +1,16 @@ +import com.keuin.kbackupfabric.operation.backup.IncrementalBackupUtil; + +import java.io.IOException; + +public class IncrementalBackupUtilTest { + + @org.junit.Test + public void generateDirectoryJsonObject() { + try { + System.out.println(IncrementalBackupUtil.generateDirectoryJsonObject("D:\\1")); + } catch (IOException exception) { + exception.printStackTrace(); + } + + } +}
\ No newline at end of file |