summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--README.md45
-rw-r--r--gradle.properties6
-rw-r--r--src/main/java/com/keuin/kbackupfabric/KBCommands.java303
-rw-r--r--src/main/java/com/keuin/kbackupfabric/KBCommandsRegister.java26
-rw-r--r--src/main/java/com/keuin/kbackupfabric/exception/ZipUtilException.java7
-rw-r--r--src/main/java/com/keuin/kbackupfabric/operation/BackupOperation.java44
-rw-r--r--src/main/java/com/keuin/kbackupfabric/operation/DeleteOperation.java20
-rw-r--r--src/main/java/com/keuin/kbackupfabric/operation/RestoreOperation.java58
-rw-r--r--src/main/java/com/keuin/kbackupfabric/operation/backup/BackupMethod.java44
-rw-r--r--src/main/java/com/keuin/kbackupfabric/operation/backup/IncrementalBackupMethod.java62
-rw-r--r--src/main/java/com/keuin/kbackupfabric/operation/backup/IncrementalBackupUtil.java90
-rw-r--r--src/main/java/com/keuin/kbackupfabric/operation/backup/PrimitiveBackupMethod.java99
-rw-r--r--src/main/java/com/keuin/kbackupfabric/operation/backup/feedback/BackupFeedback.java6
-rw-r--r--src/main/java/com/keuin/kbackupfabric/operation/backup/feedback/IncrementalBackupFeedback.java28
-rw-r--r--src/main/java/com/keuin/kbackupfabric/operation/backup/feedback/PrimitiveBackupFeedback.java30
-rw-r--r--src/main/java/com/keuin/kbackupfabric/operation/backup/method/ConfiguredBackupMethod.java37
-rw-r--r--src/main/java/com/keuin/kbackupfabric/operation/backup/method/ConfiguredIncrementalBackupMethod.java92
-rw-r--r--src/main/java/com/keuin/kbackupfabric/operation/backup/method/ConfiguredPrimitiveBackupMethod.java88
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/BytesUtil.java31
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/FilesystemUtil.java38
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java20
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/BackupFilesystemUtil.java52
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/BackupType.java33
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/builder/BackupFileNameBuilder.java24
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/builder/ObjectTreeBackupFileNameBuilder.java20
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/builder/PrimitiveZipBackupFileNameBuilder.java22
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/formatter/BackupFileNameFormatter.java31
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/formatter/ObjectTreeBackupFileNameFormatter.java44
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/formatter/PrimitiveZipBackupFileNameFormatter.java43
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollection.java65
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionFactory.java55
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionSerializer.java33
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectElement.java60
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/FileIdentifierProvider.java15
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/ObjectIdentifier.java13
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/Sha256Identifier.java88
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/SingleHashIdentifier.java53
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/StorageObjectLoader.java25
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/incremental/manager/IncrementalBackupStorageManager.java188
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/name/BackupFileNameEncoder.java64
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/name/IncrementalBackupFileNameEncoder.java37
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/name/PrimitiveBackupFileNameEncoder.java33
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/provider/AvailableBackupProvider.java8
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/provider/IncrementalBackupInformation.java13
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/provider/PrimitiveBackupInformation.java14
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/suggestion/BackupMethodSuggestionProvider.java36
-rw-r--r--src/main/java/com/keuin/kbackupfabric/util/backup/suggestion/BackupNameSuggestionProvider.java3
-rw-r--r--src/test/java/IncrementalBackupUtilTest.java16
-rw-r--r--src/test/java/com/keuin/kbackupfabric/operation/backup/method/ConfiguredIncrementalBackupMethodTest.java213
-rw-r--r--src/test/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionFactoryTest.java80
-rw-r--r--src/test/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionSerializerTest.java29
-rw-r--r--src/test/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/Sha256IdentifierTest.java25
-rw-r--r--src/test/java/com/keuin/kbackupfabric/util/backup/name/IncrementalBackupFileNameEncoderTest.java38
-rw-r--r--src/test/java/com/keuin/kbackupfabric/util/backup/name/PrimitiveBackupFileNameEncoderTest.java50
-rw-r--r--testfile/ObjectCollectionFactoryTest/1/11/111/abin0 -> 1024 bytes
-rw-r--r--testfile/ObjectCollectionFactoryTest/1/11/111/bbin0 -> 1024 bytes
-rw-r--r--testfile/ObjectCollectionFactoryTest/1/abin0 -> 1024 bytes
-rw-r--r--testfile/ObjectCollectionFactoryTest/1/bbin0 -> 1024 bytes
-rw-r--r--testfile/ObjectCollectionFactoryTest/abin0 -> 1024 bytes
-rw-r--r--testfile/ObjectCollectionFactoryTest/bbin0 -> 1024 bytes
-rw-r--r--testfile/Sha256IdentifierTest1
62 files changed, 1880 insertions, 819 deletions
diff --git a/.gitignore b/.gitignore
index 550b373..ed36e39 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,3 +27,4 @@ bin/
# fabric
run/
+logs/ \ No newline at end of file
diff --git a/README.md b/README.md
index 2c34c2f..b639657 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,40 @@
# KBackup-Fabric
-A simple backup mod for fabric Minecraft server.
+A simple backup mod for **fabric** Minecraft server, which makes **normal `.zip` backup** of your world, or self-implemented **incremental backup**, with slower increasing disk usage.
-Minecraft version: 1.14.4
+一个简单的Fabric备份Mod,支持普通备份(将存档整体压缩为 `.zip` 文件,保存在 `backups` 目录下)和增量备份(按需保存到 `incremental` 目录下,并将目录树结构保存在 `backups` 目录下)
+
+Supported Minecraft version: 1.14.4, 1.15.2, 1.16.4/1.16.5
[Fabric API](https://minecraft.curseforge.com/projects/fabric/files) is required!
## 1. Commands
+(In English)
+
- **/kb** or **/kb help**: show command list
- **/kb list**: show existing backups
-- **/kb backup \[backup_name\]**: make a backup with given name or the current system time by default
-- **/kb restore \<backup_name\>**: restore to a certain backup. This command needs a confirm to execute.
+- **/kb backup \[backup_name\]**: make a backup with given name or with the current system time by default
+- **/kb incbak \[backup_name\]**: make an incremental backup which will be saved in `incremental` folder. (However, the index file will be saved in `backups` folder)
+- **/kb restore \<backup_name\>**: restore to a certain backup. This command needs a confirmation 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.
+- **/kb prev**: Find and select the most recent backup file. After executing this command, you can use `/kb restore 1` to restore to this backup.
+
+(In simplified Chinese, 简体中文版本)
+
+- **/kb** or **/kb help**: 显示命令列表
+- **/kb list**: 显示所有已有的备份
+- **/kb backup \[backup_name\]**: 以给定名字创建一个新备份,缺省的名字是“noname”
+- **/kb incbak \[backup_name\]**: 创建一个增量备份,保存在 `incremental` 目录下。 (目录树的整体结构仍然被保存在 `backups` 目录下)
+- **/kb restore \<backup_name\>**: 还原到指定的备份。该命令需要二次确认才会真正被执行
+- **/kb confirm**: 二次确认,一旦确认,等待确认的命令会立刻被执行。这个命令是不可逆的
+- **/kb delete**: 删除一个现有的备份
+- **/kb prev**: 显示并且选中最近的一个备份,执行这个命令后,可以直接使用 `/kb restore 1` 进行还原
## 2. Script for auto-restart after restoring
-Due to the nature of JVM: the Java language's running environment, there is no elegant way to restart Minecraft server in a server plugin. In order to achieve auto restarting, the outer system-based script is required, i.e the script is a batch or a shell script.
+Due to the nature of JVM: the Java language's running environment, there is no elegant way to restart Minecraft server in a server plugin. In order to auto restart after restoring, an outer system-based script is required, i.e. a batch or a shell script.
KBackup exit JVM with a special code `111` after restoring the level successfully. The startup script just check the exit code and restart Minecraft server if the code is `111`.
@@ -36,7 +52,7 @@ rem kbackup restore auto restart
pause
```
-### 2.2 Script for Linux or U\*ix using shell
+### 2.2 Script for Linux or U\*ix using shell (Not tested, I use Windows for the most time, test it on your own)
```shell
#!/bin/sh
@@ -51,14 +67,7 @@ done
## 3. To-Do List:
-- New version checker.
-- Refactor code.
-- More thorough test.
-- Implement incremental backup.
- + Restore: trace-back (recursively, then generate file dependence tree)
- - Implement unZipRecursively (unzip a .zip.inc file recursively until reaches the root (i.e. the last full backup).)
- + Backup: base-diff (select most recently backup as the base, then diff)
- - Implement zipDiff (make a new zip with the latest backup as the base, store diff-table in zip comment (A:Add, M:Modification, D:Deletion))
-- Optimize help menu. (colored command help menu)
-- Add op login hint in the next start after restoring.
-- Implement incremental backup.
+- Op login hint in the next start after restoring
+- A more friendly help menu (colored command help menu)
+- New version checker
+- Code refactor for maintainability
diff --git a/gradle.properties b/gradle.properties
index a131c0a..ef7c112 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -4,11 +4,11 @@ org.gradle.jvmargs=-Xmx1G
# check these on https://fabricmc.net/use
minecraft_version=1.14.4
yarn_mappings=1.14.4+build.18
-loader_version=0.10.8
+loader_version=0.11.0
# Mod Properties
-mod_version=1.3.3
+mod_version=1.4.2
maven_group=com.keuin.kbackupfabric
archives_base_name=kbackup-fabric
# Dependencies
# currently not on the main fabric site, check on the maven: https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api
-fabric_version=0.23.2+1.14
+fabric_version=0.28.4+1.14
diff --git a/src/main/java/com/keuin/kbackupfabric/KBCommands.java b/src/main/java/com/keuin/kbackupfabric/KBCommands.java
index bc04291..1b52b6b 100644
--- a/src/main/java/com/keuin/kbackupfabric/KBCommands.java
+++ b/src/main/java/com/keuin/kbackupfabric/KBCommands.java
@@ -5,24 +5,29 @@ 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.operation.backup.method.ConfiguredBackupMethod;
+import com.keuin.kbackupfabric.operation.backup.method.ConfiguredIncrementalBackupMethod;
+import com.keuin.kbackupfabric.operation.backup.method.ConfiguredPrimitiveBackupMethod;
+import com.keuin.kbackupfabric.util.PrintUtil;
import com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil;
-import com.keuin.kbackupfabric.util.backup.BackupType;
+import com.keuin.kbackupfabric.util.backup.name.IncrementalBackupFileNameEncoder;
+import com.keuin.kbackupfabric.util.backup.name.PrimitiveBackupFileNameEncoder;
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;
import com.mojang.brigadier.context.CommandContext;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.command.ServerCommandSource;
import java.io.File;
-import java.util.*;
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
-import static com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil.*;
import static com.keuin.kbackupfabric.util.PrintUtil.*;
+import static com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil.*;
public final class KBCommands {
@@ -30,10 +35,11 @@ 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 boolean notifiedPreviousRestoration = false;
//private static final Logger LOGGER = LogManager.getLogger();
- private static final List<String> backupNameList = new ArrayList<>(); // index -> backupName
+ private static final List<String> backupFileNameList = new ArrayList<>(); // index -> backupName
private static Invokable pendingOperation = null;
//private static BackupMethod activatedBackupMethod = new PrimitiveBackupMethod(); // The backup method we currently using
@@ -62,8 +68,9 @@ public final class KBCommands {
*/
public static int kb(CommandContext<ServerCommandSource> context) {
int statCode = list(context);
- if (MetadataHolder.hasMetadata()) {
+ if (MetadataHolder.hasMetadata() && !notifiedPreviousRestoration) {
// Output metadata info
+ notifiedPreviousRestoration = true;
msgStress(context, "Restored from backup " + MetadataHolder.getMetadata().getBackupName());
}
return statCode;
@@ -78,25 +85,29 @@ public final class KBCommands {
public static int list(CommandContext<ServerCommandSource> context) {
MinecraftServer server = context.getSource().getMinecraftServer();
File[] files = getBackupSaveDirectory(server).listFiles(
- (dir, name) -> dir.isDirectory() && name.toLowerCase().endsWith(".zip") && name.toLowerCase().startsWith(getBackupFileNamePrefix())
+ (dir, name) -> dir.isDirectory() &&
+ (name.toLowerCase().endsWith(".zip") && name.toLowerCase().startsWith(getBackupFileNamePrefix())
+ || name.toLowerCase().endsWith(".kbi"))
);
- backupNameList.clear();
- if (files != null) {
- if (files.length != 0) {
- msgInfo(context, "Available backups: (file is not checked, manipulation may affect this plugin)");
+ synchronized (backupFileNameList) {
+ backupFileNameList.clear();
+ if (files != null) {
+ if (files.length != 0) {
+ msgInfo(context, "Available backups: (file is not checked, manipulation may affect this plugin)");
+ } else {
+ msgInfo(context, "There are no available backups. To make a new backup, run /kb backup.");
+ }
+ int i = 0;
+ for (File file : files) {
+ ++i;
+ String backupFileName = file.getName();
+ msgInfo(context, String.format("[%d] %s", i, getPrimitiveBackupInformationString(backupFileName, file.length())));
+ backupFileNameList.add(backupFileName);
+ }
} else {
- msgInfo(context, "There are no available backups. To make a new backup, check /kb backup.");
- }
- int i = 0;
- for (File file : files) {
- ++i;
- String backupName = getBackupName(file.getName());
- backupNameList.add(backupName);
- msgInfo(context, String.format("[%d] %s, size: %.1fMB", i, backupName, file.length() * 1.0 / 1024 / 1024));
+ msgErr(context, "Error: failed to list files in backup folder.");
}
- } else {
- msgErr(context, "Error: failed to list files in backup folder.");
}
return SUCCESS;
}
@@ -109,13 +120,13 @@ public final class KBCommands {
*/
public static int primitiveBackup(CommandContext<ServerCommandSource> context) {
//KBMain.backup("name")
- String backupName = StringArgumentType.getString(context, "backupName");
- if (backupName.matches("[0-9]*")) {
+ String customBackupName = StringArgumentType.getString(context, "backupName");
+ if (customBackupName.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));
+ customBackupName = String.format("a%s", customBackupName);
+ msgWarn(context, String.format("Pure numeric name is not allowed. Renaming to %s", customBackupName));
}
- return doBackup(context, backupName, PrimitiveBackupMethod.getInstance());
+ return doBackup(context, customBackupName, false);
}
/**
@@ -125,24 +136,39 @@ public final class KBCommands {
* @return stat code.
*/
public static int primitiveBackupWithDefaultName(CommandContext<ServerCommandSource> context) {
- return doBackup(context, DEFAULT_BACKUP_NAME, PrimitiveBackupMethod.getInstance());
+ return doBackup(context, DEFAULT_BACKUP_NAME, false);
}
public static int incrementalBackup(CommandContext<ServerCommandSource> context) {
- //KBMain.backup("name")
- String backupName = StringArgumentType.getString(context, "backupName");
- if (backupName.matches("[0-9]*")) {
+ String customBackupName = StringArgumentType.getString(context, "backupName");
+ if (customBackupName.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));
+ customBackupName = String.format("a%s", customBackupName);
+ msgWarn(context, String.format("Pure numeric name is not allowed. Renaming to %s", customBackupName));
}
- return doBackup(context, backupName, IncrementalBackupMethod.getInstance());
+ return doBackup(context, customBackupName, true);
}
public static int incrementalBackupWithDefaultName(CommandContext<ServerCommandSource> context) {
- return doBackup(context, DEFAULT_BACKUP_NAME, IncrementalBackupMethod.getInstance());
+ return doBackup(context, DEFAULT_BACKUP_NAME, true);
}
+
+// 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());
+// }
+
/**
* Delete an existing backup with context parameter backupName.
* Simply set the pending backupName to given backupName, for the second confirmation.
@@ -152,14 +178,14 @@ public final class KBCommands {
*/
public static int delete(CommandContext<ServerCommandSource> context) {
- String backupName = parseBackupName(context, StringArgumentType.getString(context, "backupName"));
+ String backupFileName = parseBackupFileName(context, StringArgumentType.getString(context, "backupName"));
MinecraftServer server = context.getSource().getMinecraftServer();
- if (backupName == null)
+ if (backupFileName == null)
return list(context); // Show the list and return
// Validate backupName
- if (!isBackupNameValid(backupName, server)) {
+ if (!isBackupFileExists(backupFileName, server)) {
// Invalid backupName
msgErr(context, "Invalid backup name! Please check your input. The list index number is also valid.");
return FAILED;
@@ -167,9 +193,9 @@ public final class KBCommands {
// Update pending task
//pendingOperation = AbstractConfirmableOperation.createDeleteOperation(context, backupName);
- pendingOperation = new DeleteOperation(context, backupName);
+ pendingOperation = new DeleteOperation(context, backupFileName);
- msgWarn(context, String.format("DELETION WARNING: The deletion is irreversible! You will lose the backup %s permanently. Use /kb confirm to start or /kb cancel to abort.", backupName), true);
+ msgWarn(context, String.format("DELETION WARNING: The deletion is irreversible! You will lose the backup %s permanently. Use /kb confirm to start or /kb cancel to abort.", backupFileName), true);
return SUCCESS;
}
@@ -182,55 +208,89 @@ public final class KBCommands {
* @return stat code.
*/
public static int restore(CommandContext<ServerCommandSource> context) {
- //KBMain.restore("name")
- MinecraftServer server = context.getSource().getMinecraftServer();
- String backupName = parseBackupName(context, StringArgumentType.getString(context, "backupName"));
- backupName = parseBackupName(context, backupName);
-
- if (backupName == null)
- return list(context); // Show the list and return
-
- // 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;
- }
-
- // Detect backup type
+ try {
+ //KBMain.restore("name")
+ MinecraftServer server = context.getSource().getMinecraftServer();
+ String backupFileName = parseBackupFileName(context, StringArgumentType.getString(context, "backupName"));
+ backupFileName = parseBackupFileName(context, backupFileName);
+ if (backupFileName == null)
+ return list(context); // Show the list and return
- // Update pending task
- //pendingOperation = AbstractConfirmableOperation.createRestoreOperation(context, backupName);
- File backupFile = new File(getBackupSaveDirectory(server), getBackupFileName(backupName));
- pendingOperation = new RestoreOperation(context, backupFile.getAbsolutePath(), getLevelPath(server), backupName);
+ // Validate backupName
+ if (!isBackupFileExists(backupFileName, server)) {
+ // Invalid backupName
+ msgErr(context, "Invalid backup name! Please check your input. The list index number is also valid.", false);
+ return FAILED;
+ }
- msgWarn(context, String.format("RESET WARNING: You will LOSE YOUR CURRENT WORLD PERMANENTLY! The worlds will be replaced with backup %s . Use /kb confirm to start or /kb cancel to abort.", backupName), true);
- return SUCCESS;
+ // Detect backup type
+
+
+ // Update pending task
+ //pendingOperation = AbstractConfirmableOperation.createRestoreOperation(context, backupName);
+// File backupFile = new File(getBackupSaveDirectory(server), getBackupFileName(backupName));
+ // TODO: improve this
+ ConfiguredBackupMethod method = backupFileName.endsWith(".zip") ?
+ new ConfiguredPrimitiveBackupMethod(
+ backupFileName, getLevelPath(server), getBackupSaveDirectory(server).getAbsolutePath()
+ ) : new ConfiguredIncrementalBackupMethod(
+ backupFileName, getLevelPath(server),
+ getBackupSaveDirectory(server).getAbsolutePath(),
+ getIncrementalBackupBaseDirectory(server).getAbsolutePath()
+ );
+ // String backupSavePath, String levelPath, String backupFileName
+// getBackupSaveDirectory(server).getAbsolutePath(), getLevelPath(server), backupFileName
+ pendingOperation = new RestoreOperation(context, method);
+
+ msgWarn(context, String.format("RESET WARNING: You will LOSE YOUR CURRENT WORLD PERMANENTLY! The worlds will be replaced with backup %s . Use /kb confirm to start or /kb cancel to abort.", backupFileName), true);
+ return SUCCESS;
+ } catch (IOException e) {
+ msgErr(context, String.format("An I/O exception occurred while making backup: %s", e));
+ }
+ return FAILED;
}
- 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")
- String backupName = BackupNameTimeFormatter.getTimeString() + "_" + customBackupName;
+ private static int doBackup(CommandContext<ServerCommandSource> context, String customBackupName, boolean incremental) {
+ try {
+ // Real backup name (compatible with legacy backup): date_name, such as 2020-04-23_21-03-00_test
+ //KBMain.backup("name")
+// String backupName = BackupNameTimeFormatter.getTimeString() + "_" + customBackupName;
+
+ // Validate file name
+ final char[] ILLEGAL_CHARACTERS = {'/', '\n', '\r', '\t', '\0', '\f', '`', '?', '*', '\\', '<', '>', '|', '\"', ':'};
+ for (char c : ILLEGAL_CHARACTERS) {
+ if (customBackupName.contains(String.valueOf(c))) {
+ msgErr(context, String.format("Name cannot contain special character \"%c\".", c));
+ return FAILED;
+ }
+ }
- // Validate file name
- final char[] ILLEGAL_CHARACTERS = {'/', '\n', '\r', '\t', '\0', '\f', '`', '?', '*', '\\', '<', '>', '|', '\"', ':'};
- for (char c : ILLEGAL_CHARACTERS) {
- if (backupName.contains(String.valueOf(c))) {
- msgErr(context, String.format("Name cannot contain special character \"%c\".", c));
+ PrintUtil.info("Start backup...");
+
+ // configure backup method
+ MinecraftServer server = context.getSource().getMinecraftServer();
+ ConfiguredBackupMethod method = !incremental ? new ConfiguredPrimitiveBackupMethod(
+ new PrimitiveBackupFileNameEncoder().encode(customBackupName, LocalDateTime.now()),
+ getLevelPath(server),
+ getBackupSaveDirectory(server).getAbsolutePath()
+ ) : new ConfiguredIncrementalBackupMethod(
+ new IncrementalBackupFileNameEncoder().encode(customBackupName, LocalDateTime.now()),
+ getLevelPath(server),
+ getBackupSaveDirectory(server).getAbsolutePath(),
+ getIncrementalBackupBaseDirectory(server).getAbsolutePath()
+ );
+
+ // dispatch to operation worker
+ BackupOperation operation = new BackupOperation(context, method);
+ if (operation.invoke()) {
+ return SUCCESS;
+ } else if (operation.isBlocked()) {
+ msgWarn(context, "Another task is running, cannot issue new backup at once.");
return FAILED;
}
- }
-
- // Do backup
- PrintUtil.info("Invoking backup worker ...");
- //BackupWorker.invoke(context, backupName, metadata);
- BackupOperation operation = new BackupOperation(context, backupName, backupMethod);
- if (operation.invoke()) {
- return SUCCESS;
- } else if (operation.isBlocked()) {
- msgWarn(context, "Another task is running, cannot issue new backup at once.");
+ } catch (IOException e) {
+ msgErr(context, String.format("An I/O exception occurred while making backup: %s", e));
}
return FAILED;
}
@@ -290,15 +350,22 @@ public final class KBCommands {
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;
+ String backupFileName = prevBackupFile.getName();
+ int i;
+ synchronized (backupFileNameList) {
+ i = backupFileNameList.indexOf(backupFileName);
+ if (i == -1) {
+ backupFileNameList.add(backupFileName);
+ i = backupFileNameList.size();
+ } else {
+ ++i;
+ }
}
- msgInfo(context, String.format("The most recent backup: [%d] %s , size: %s", i, backupName, humanFileSize(prevBackupFile.length())));
+ msgInfo(context, String.format(
+ "The most recent backup: [%d] %s",
+ i,
+ getPrimitiveBackupInformationString(backupFileName, prevBackupFile.length())
+ ));
} catch (NullPointerException e) {
msgInfo(context, "There are no backups available.");
} catch (SecurityException ignored) {
@@ -308,35 +375,45 @@ 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 getPrimitiveBackupInformationString(String backupFileName, long backupFileSizeBytes) {
+ return String.format(
+ "%s , size: %s",
+ new PrimitiveBackupFileNameEncoder().decode(backupFileName),
+ getFriendlyFileSizeString(backupFileSizeBytes)
+ );
}
-
- private static String parseBackupName(CommandContext<ServerCommandSource> context, String userInput) {
+// /**
+// * 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 parseBackupFileName(CommandContext<ServerCommandSource> context, String userInput) {
try {
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.
+ synchronized (backupFileNameList) {
+ return backupFileNameList.get(index); // Replace input number with real backup file name.
+ }
}
} catch (NumberFormatException | IndexOutOfBoundsException ignored) {
}
diff --git a/src/main/java/com/keuin/kbackupfabric/KBCommandsRegister.java b/src/main/java/com/keuin/kbackupfabric/KBCommandsRegister.java
index 3376ac9..d9ace66 100644
--- a/src/main/java/com/keuin/kbackupfabric/KBCommandsRegister.java
+++ b/src/main/java/com/keuin/kbackupfabric/KBCommandsRegister.java
@@ -1,8 +1,7 @@
package com.keuin.kbackupfabric;
-import com.keuin.kbackupfabric.util.backup.suggestion.BackupMethodSuggestionProvider;
-import com.keuin.kbackupfabric.util.backup.suggestion.BackupNameSuggestionProvider;
import com.keuin.kbackupfabric.util.PermissionValidator;
+import com.keuin.kbackupfabric.util.backup.suggestion.BackupNameSuggestionProvider;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType;
import net.minecraft.server.command.CommandManager;
@@ -19,25 +18,16 @@ public final class KBCommandsRegister {
// 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::primitiveBackup)
).requires(PermissionValidator::op).executes(KBCommands::primitiveBackupWithDefaultName)));
+ // register /kb incbak [name] for performing incremental backup. OP is required.
+ dispatcher.register(CommandManager.literal("kb").then(CommandManager.literal("incbak").then(
+ CommandManager.argument("backupName", StringArgumentType.greedyString()).requires(PermissionValidator::op).executes(KBCommands::incrementalBackup)
+ ).requires(PermissionValidator::op).executes(KBCommands::incrementalBackupWithDefaultName)));
+
// 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)));
@@ -53,7 +43,7 @@ public final class KBCommandsRegister {
// 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))));
+// // 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/exception/ZipUtilException.java b/src/main/java/com/keuin/kbackupfabric/exception/ZipUtilException.java
index c23ce26..88b1d18 100644
--- a/src/main/java/com/keuin/kbackupfabric/exception/ZipUtilException.java
+++ b/src/main/java/com/keuin/kbackupfabric/exception/ZipUtilException.java
@@ -1,10 +1,7 @@
package com.keuin.kbackupfabric.exception;
public class ZipUtilException extends Exception {
- public ZipUtilException() {
- }
-
- public ZipUtilException(String gripe) {
- super(gripe);
+ public ZipUtilException(String string) {
+ super(string);
}
}
diff --git a/src/main/java/com/keuin/kbackupfabric/operation/BackupOperation.java b/src/main/java/com/keuin/kbackupfabric/operation/BackupOperation.java
index d03c347..b38921d 100644
--- a/src/main/java/com/keuin/kbackupfabric/operation/BackupOperation.java
+++ b/src/main/java/com/keuin/kbackupfabric/operation/BackupOperation.java
@@ -1,39 +1,32 @@
package com.keuin.kbackupfabric.operation;
-import com.keuin.kbackupfabric.exception.ZipUtilException;
-import com.keuin.kbackupfabric.metadata.BackupMetadata;
import com.keuin.kbackupfabric.operation.abstracts.InvokableAsyncBlockingOperation;
-import com.keuin.kbackupfabric.operation.backup.BackupMethod;
+import com.keuin.kbackupfabric.operation.backup.feedback.BackupFeedback;
+import com.keuin.kbackupfabric.operation.backup.method.ConfiguredBackupMethod;
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 net.minecraft.server.MinecraftServer;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.world.World;
-import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
-import static com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil.*;
import static com.keuin.kbackupfabric.util.PrintUtil.msgInfo;
public class BackupOperation extends InvokableAsyncBlockingOperation {
private final CommandContext<ServerCommandSource> context;
- private final String backupName;
private final Map<World, Boolean> oldWorldsSavingDisabled = new HashMap<>();
- private final BackupMethod backupMethod;
+ private final ConfiguredBackupMethod configuredBackupMethod;
private long startTime;
- public BackupOperation(CommandContext<ServerCommandSource> context, String backupName, BackupMethod backupMethod) {
+ public BackupOperation(CommandContext<ServerCommandSource> context, ConfiguredBackupMethod configuredBackupMethod) {
super("BackupWorker");
this.context = context;
- this.backupName = backupName;
- this.backupMethod = backupMethod;
+ this.configuredBackupMethod = configuredBackupMethod;
}
@Override
@@ -44,27 +37,20 @@ public class BackupOperation extends InvokableAsyncBlockingOperation {
//// Do our main backup logic
// Create backup saving directory
- 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));
+ if (!configuredBackupMethod.touch()) {
+ PrintUtil.msgErr(context, "Failed to create backup save directory. Cannot backup.");
return;
}
- // Make zip
- String levelPath = getLevelPath(server);
- String backupFileName = getBackupFileName(backupName);
-
- BackupMethod.BackupResult result = backupMethod.backup(backupName,levelPath,backupSaveDirectory);
- if(result.isSuccess()) {
- // Restore old autosave switch stat
+ // Backup
+ BackupFeedback result = configuredBackupMethod.backup();
+ if (result.isSuccess()) {
+ // Restore old auto-save switch stat
server.getWorlds().forEach(world -> world.savingDisabled = oldWorldsSavingDisabled.getOrDefault(world, true));
// Print finish message: time elapsed and file size
long timeElapsedMillis = System.currentTimeMillis() - startTime;
- String msgText = String.format("Backup finished. Time elapsed: %.2fs.", timeElapsedMillis / 1000.0);
- File backupZipFile = new File(backupSaveDirectory, backupFileName);
- msgText += String.format(" File size: %s.", humanFileSize(result.getBackupSizeBytes()));
+ String msgText = String.format("Backup finished. Time elapsed: %.2fs. ", timeElapsedMillis / 1000.0) + result.getFeedback();
PrintUtil.msgInfo(context, msgText, true);
} else {
// failed
@@ -79,14 +65,14 @@ public class BackupOperation extends InvokableAsyncBlockingOperation {
@Override
protected boolean sync() {
- //// Save world, save old autosave configs
+ //// Save world, save old auto-save configs
- PrintUtil.broadcast(String.format("Making backup %s, please wait ...", backupName));
+ PrintUtil.broadcast("Making backup, please wait ...");
// Get server
MinecraftServer server = context.getSource().getMinecraftServer();
- // Save old autosave switch stat temporally
+ // Save old auto-save switch state for restoration after finished
oldWorldsSavingDisabled.clear();
server.getWorlds().forEach(world -> {
oldWorldsSavingDisabled.put(world, world.savingDisabled);
diff --git a/src/main/java/com/keuin/kbackupfabric/operation/DeleteOperation.java b/src/main/java/com/keuin/kbackupfabric/operation/DeleteOperation.java
index 444ca9a..3ae09f5 100644
--- a/src/main/java/com/keuin/kbackupfabric/operation/DeleteOperation.java
+++ b/src/main/java/com/keuin/kbackupfabric/operation/DeleteOperation.java
@@ -1,8 +1,8 @@
package com.keuin.kbackupfabric.operation;
import com.keuin.kbackupfabric.operation.abstracts.InvokableAsyncBlockingOperation;
-import com.keuin.kbackupfabric.util.backup.suggestion.BackupNameSuggestionProvider;
import com.keuin.kbackupfabric.util.PrintUtil;
+import com.keuin.kbackupfabric.util.backup.suggestion.BackupNameSuggestionProvider;
import com.mojang.brigadier.context.CommandContext;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.command.ServerCommandSource;
@@ -10,27 +10,26 @@ import net.minecraft.server.command.ServerCommandSource;
import java.io.File;
import java.io.IOException;
-import static com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil.getBackupFileName;
-import static com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil.getBackupSaveDirectory;
import static com.keuin.kbackupfabric.util.PrintUtil.msgErr;
import static com.keuin.kbackupfabric.util.PrintUtil.msgInfo;
+import static com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil.getBackupSaveDirectory;
import static org.apache.commons.io.FileUtils.forceDelete;
public class DeleteOperation extends InvokableAsyncBlockingOperation {
//private static final Logger LOGGER = LogManager.getLogger();
- private final String backupName;
+ private final String backupFileName;
private final CommandContext<ServerCommandSource> context;
- public DeleteOperation(CommandContext<ServerCommandSource> context, String backupName) {
+ public DeleteOperation(CommandContext<ServerCommandSource> context, String backupFileName) {
super("BackupDeletingWorker");
- this.backupName = backupName;
+ this.backupFileName = backupFileName;
this.context = context;
}
@Override
public String toString() {
- return String.format("deletion of %s", backupName);
+ return String.format("deletion of %s", backupFileName);
}
@Override
@@ -41,8 +40,7 @@ public class DeleteOperation extends InvokableAsyncBlockingOperation {
private void delete() {
MinecraftServer server = context.getSource().getMinecraftServer();
- String backupFileName = getBackupFileName(backupName);
- PrintUtil.info("Deleting backup " + backupName);
+ PrintUtil.info("Deleting backup file " + this.backupFileName);
File backupFile = new File(getBackupSaveDirectory(server), backupFileName);
int tryCounter = 0;
do {
@@ -59,7 +57,7 @@ public class DeleteOperation extends InvokableAsyncBlockingOperation {
}
++tryCounter;
} while (backupFile.exists());
- PrintUtil.info("Deleted backup " + backupName);
- msgInfo(context, "Deleted backup " + backupName);
+ PrintUtil.info("Successfully deleted backup file " + this.backupFileName);
+ msgInfo(context, "Successfully deleted backup file " + this.backupFileName);
}
}
diff --git a/src/main/java/com/keuin/kbackupfabric/operation/RestoreOperation.java b/src/main/java/com/keuin/kbackupfabric/operation/RestoreOperation.java
index 22397a1..b870746 100644
--- a/src/main/java/com/keuin/kbackupfabric/operation/RestoreOperation.java
+++ b/src/main/java/com/keuin/kbackupfabric/operation/RestoreOperation.java
@@ -1,47 +1,36 @@
package com.keuin.kbackupfabric.operation;
-import com.keuin.kbackupfabric.exception.ZipUtilException;
import com.keuin.kbackupfabric.operation.abstracts.InvokableBlockingOperation;
+import com.keuin.kbackupfabric.operation.backup.method.ConfiguredBackupMethod;
import com.keuin.kbackupfabric.util.PrintUtil;
-import com.keuin.kbackupfabric.util.ZipUtil;
import com.mojang.brigadier.context.CommandContext;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.command.ServerCommandSource;
-import java.io.File;
import java.io.IOException;
-
-import static com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil.getBackupFileName;
-import static com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil.getBackupSaveDirectory;
-import static org.apache.commons.io.FileUtils.forceDelete;
+import java.util.Objects;
public class RestoreOperation extends InvokableBlockingOperation {
//private static final Logger LOGGER = LogManager.getLogger();
- private final String backupName;
private final Thread serverThread;
- private final String backupFilePath;
- private final String levelDirectory;
private final CommandContext<ServerCommandSource> context;
private final MinecraftServer server;
+ private final ConfiguredBackupMethod configuredBackupMethod;
- public RestoreOperation(CommandContext<ServerCommandSource> context, String backupFilePath, String levelDirectory, String backupName) {
- server = context.getSource().getMinecraftServer();
- this.backupName = backupName;
- this.serverThread = server.getThread();
- this.backupFilePath = backupFilePath;
- this.levelDirectory = levelDirectory;
- this.context = context;
+ public RestoreOperation(CommandContext<ServerCommandSource> context, ConfiguredBackupMethod configuredBackupMethod) {
+ server = Objects.requireNonNull(context.getSource().getMinecraftServer());
+ this.serverThread = Objects.requireNonNull(server.getThread());
+ this.context = Objects.requireNonNull(context);
+ this.configuredBackupMethod = Objects.requireNonNull(configuredBackupMethod);
}
@Override
protected boolean blockingContext() {
// do restore to backupName
- PrintUtil.broadcast(String.format("Restoring to previous world %s ...", backupName));
+ PrintUtil.broadcast(String.format("Restoring to backup %s ...", configuredBackupMethod.getBackupFileName()));
- String backupFileName = getBackupFileName(backupName);
- PrintUtil.debug("Backup file name: " + backupFileName);
- File backupFile = new File(getBackupSaveDirectory(server), backupFileName);
+ PrintUtil.debug("Backup file name: " + configuredBackupMethod.getBackupFileName());
PrintUtil.msgInfo(context, "Server will shutdown in a few seconds, depending on world size and disk speed, the progress may take from seconds to minutes.", true);
PrintUtil.msgInfo(context, "Please do not force the server stop, or the level would be broken.", true);
@@ -67,7 +56,7 @@ public class RestoreOperation extends InvokableBlockingOperation {
@Override
public String toString() {
- return String.format("restoration from %s", backupName);
+ return String.format("restoration from %s", configuredBackupMethod.getBackupFileName());
}
private class WorkerThread implements Runnable {
@@ -80,7 +69,7 @@ public class RestoreOperation extends InvokableBlockingOperation {
while (serverThread.isAlive()) {
try {
serverThread.join();
- } catch (InterruptedException ignored) {
+ } catch (InterruptedException | RuntimeException ignored) {
}
}
@@ -94,13 +83,30 @@ public class RestoreOperation extends InvokableBlockingOperation {
}while(--cnt > 0);
////////////////////
-
- //ServerRestartUtil.forkAndRestart();
- System.exit(111);
+ long startTime = System.currentTimeMillis();
+ if (configuredBackupMethod.restore()) {
+ 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 check the manual at: https://github.com/keuin/KBackup-Fabric/blob/master/README.md");
+ //ServerRestartUtil.forkAndRestart();
+ System.exit(111);
+ } else {
+ PrintUtil.error("Failed to restore! server will not restart automatically.");
+ }
} catch (SecurityException e) {
PrintUtil.error("An exception occurred while restoring: " + e.getMessage());
+ e.printStackTrace();
+ } catch (IOException e) {
+ PrintUtil.error(e.toString());
+ PrintUtil.error("Failed to restore due to an unhandled I/O exception.");
+ e.printStackTrace();
}
+ System.exit(0); // all failed restoration will eventually go here
}
}
}
diff --git a/src/main/java/com/keuin/kbackupfabric/operation/backup/BackupMethod.java b/src/main/java/com/keuin/kbackupfabric/operation/backup/BackupMethod.java
deleted file mode 100644
index 4e9eb6c..0000000
--- a/src/main/java/com/keuin/kbackupfabric/operation/backup/BackupMethod.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.keuin.kbackupfabric.operation.backup;
-
-import com.keuin.kbackupfabric.util.backup.builder.BackupFileNameBuilder;
-import com.keuin.kbackupfabric.util.backup.formatter.BackupFileNameFormatter;
-
-import java.io.IOException;
-
-/**
- * Provide specific backup method, which is implemented statelessly.
- */
-public interface BackupMethod {
-
- /**
- * Perform a backup with given method. The backup will be saved as the given name.
- * Note: real file name depends on the backup type.
- * @param backupName the backup name.
- * @return if the backup operation succeed.
- */
- 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
deleted file mode 100644
index 4a87bb3..0000000
--- a/src/main/java/com/keuin/kbackupfabric/operation/backup/IncrementalBackupMethod.java
+++ /dev/null
@@ -1,62 +0,0 @@
-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
deleted file mode 100644
index f90aef1..0000000
--- a/src/main/java/com/keuin/kbackupfabric/operation/backup/IncrementalBackupUtil.java
+++ /dev/null
@@ -1,90 +0,0 @@
-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
deleted file mode 100644
index 72e9cdb..0000000
--- a/src/main/java/com/keuin/kbackupfabric/operation/backup/PrimitiveBackupMethod.java
+++ /dev/null
@@ -1,99 +0,0 @@
-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;
-import com.keuin.kbackupfabric.util.backup.formatter.BackupFileNameFormatter;
-
-import java.io.File;
-import java.io.IOException;
-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();
- private static int zipLevel = 9;
-
- public static PrimitiveBackupMethod getInstance() {
- return INSTANCE;
- }
-
- @Override
- public BackupResult backup(String backupName, String levelPath, String backupSaveDirectory) throws IOException {
- String backupFileName = BackupFileNameBuilder.primitiveZipBackup().build(LocalDateTime.now(),backupName);
- try {
- BackupMetadata backupMetadata = new BackupMetadata(System.currentTimeMillis(), backupName);
-
- PrintUtil.info(String.format("zip(srcPath=%s, destPath=%s)", levelPath, backupSaveDirectory));
- PrintUtil.info("Compressing level ...");
- ZipUtil.makeBackupZip(levelPath, backupSaveDirectory, backupFileName, backupMetadata, zipLevel);
-
- } 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;
- }
-
- @Override
- public BackupFileNameBuilder getBackupFileNameBuilder() {
- return BackupFileNameBuilder.primitiveZipBackup();
- }
-
- @Override
- public BackupFileNameFormatter getBackupFileNameFormatter() {
- return BackupFileNameFormatter.primitiveZipBackup();
- }
-}
diff --git a/src/main/java/com/keuin/kbackupfabric/operation/backup/feedback/BackupFeedback.java b/src/main/java/com/keuin/kbackupfabric/operation/backup/feedback/BackupFeedback.java
new file mode 100644
index 0000000..0fe0766
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/operation/backup/feedback/BackupFeedback.java
@@ -0,0 +1,6 @@
+package com.keuin.kbackupfabric.operation.backup.feedback;
+
+public interface BackupFeedback {
+ String getFeedback();
+ boolean isSuccess();
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/operation/backup/feedback/IncrementalBackupFeedback.java b/src/main/java/com/keuin/kbackupfabric/operation/backup/feedback/IncrementalBackupFeedback.java
new file mode 100644
index 0000000..f39fde6
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/operation/backup/feedback/IncrementalBackupFeedback.java
@@ -0,0 +1,28 @@
+package com.keuin.kbackupfabric.operation.backup.feedback;
+
+public class IncrementalBackupFeedback implements BackupFeedback {
+ private final boolean success;
+ private final int newFilesAdded;
+
+ public IncrementalBackupFeedback(boolean success, int newFilesAdded) {
+ this.success = success;
+ this.newFilesAdded = newFilesAdded;
+ }
+
+ @Override
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public long getNewFilesAdded() {
+ return newFilesAdded;
+ }
+
+ @Override
+ public String getFeedback() {
+ if (success && newFilesAdded >= 0)
+ return String.format("File(s) added: %d.", newFilesAdded);
+ else
+ return "";
+ }
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/operation/backup/feedback/PrimitiveBackupFeedback.java b/src/main/java/com/keuin/kbackupfabric/operation/backup/feedback/PrimitiveBackupFeedback.java
new file mode 100644
index 0000000..3bcd012
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/operation/backup/feedback/PrimitiveBackupFeedback.java
@@ -0,0 +1,30 @@
+package com.keuin.kbackupfabric.operation.backup.feedback;
+
+import static com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil.getFriendlyFileSizeString;
+
+public class PrimitiveBackupFeedback implements BackupFeedback {
+ private final boolean success;
+ private final long backupSizeBytes;
+
+ public PrimitiveBackupFeedback(boolean success, long backupSizeBytes) {
+ this.success = success;
+ this.backupSizeBytes = backupSizeBytes;
+ }
+
+ @Override
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public long getBackupSizeBytes() {
+ return backupSizeBytes;
+ }
+
+ @Override
+ public String getFeedback() {
+ if (success && backupSizeBytes >= 0)
+ return String.format("File size: %s.", getFriendlyFileSizeString(backupSizeBytes));
+ else
+ return "";
+ }
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/operation/backup/method/ConfiguredBackupMethod.java b/src/main/java/com/keuin/kbackupfabric/operation/backup/method/ConfiguredBackupMethod.java
new file mode 100644
index 0000000..bb80c80
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/operation/backup/method/ConfiguredBackupMethod.java
@@ -0,0 +1,37 @@
+package com.keuin.kbackupfabric.operation.backup.method;
+
+import com.keuin.kbackupfabric.operation.backup.feedback.BackupFeedback;
+
+import java.io.IOException;
+
+/**
+ * Provide specific backup method, which has been configured with proper settings,
+ * such as saving directory and level path.
+ */
+public interface ConfiguredBackupMethod {
+
+ /**
+ * Perform a backup with given method. The backup will be saved as the given name.
+ * Note: real file name depends on the backup type.
+ *
+ * @return backup result.
+ */
+ BackupFeedback backup() throws IOException;
+
+ boolean restore() throws IOException;
+
+ /**
+ * Create backup save directory and do some essential initialization before the backup process.
+ *
+ * @return false if failed, then the backup process won't proceed.
+ */
+ boolean touch();
+
+ /**
+ * Get the used backup file name.
+ *
+ * @return the file name.
+ */
+ String getBackupFileName();
+
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/operation/backup/method/ConfiguredIncrementalBackupMethod.java b/src/main/java/com/keuin/kbackupfabric/operation/backup/method/ConfiguredIncrementalBackupMethod.java
new file mode 100644
index 0000000..b5d2463
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/operation/backup/method/ConfiguredIncrementalBackupMethod.java
@@ -0,0 +1,92 @@
+package com.keuin.kbackupfabric.operation.backup.method;
+
+import com.keuin.kbackupfabric.operation.backup.feedback.IncrementalBackupFeedback;
+import com.keuin.kbackupfabric.util.FilesystemUtil;
+import com.keuin.kbackupfabric.util.PrintUtil;
+import com.keuin.kbackupfabric.util.backup.incremental.ObjectCollection;
+import com.keuin.kbackupfabric.util.backup.incremental.ObjectCollectionFactory;
+import com.keuin.kbackupfabric.util.backup.incremental.ObjectCollectionSerializer;
+import com.keuin.kbackupfabric.util.backup.incremental.identifier.Sha256Identifier;
+import com.keuin.kbackupfabric.util.backup.incremental.manager.IncrementalBackupStorageManager;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.HashSet;
+
+public class ConfiguredIncrementalBackupMethod implements ConfiguredBackupMethod {
+
+ private final String backupIndexFileName;
+ private final String levelPath;
+ private final String backupIndexFileSaveDirectory;
+ private final String backupBaseDirectory;
+
+ public ConfiguredIncrementalBackupMethod(String backupIndexFileName, String levelPath, String backupIndexFileSaveDirectory, String backupBaseDirectory) {
+ this.backupIndexFileName = backupIndexFileName;
+ this.levelPath = levelPath;
+ this.backupIndexFileSaveDirectory = backupIndexFileSaveDirectory;
+ this.backupBaseDirectory = backupBaseDirectory;
+ }
+
+ @Override
+ public IncrementalBackupFeedback backup() throws IOException {
+ File levelPathFile = new File(levelPath);
+
+ // construct incremental backup index
+ PrintUtil.info("Hashing files...");
+ ObjectCollection collection = new ObjectCollectionFactory<>(Sha256Identifier.getFactory())
+ .fromDirectory(levelPathFile, new HashSet<>(Arrays.asList("session.lock", "kbackup_metadata")));
+
+ // update storage
+ PrintUtil.info("Copying files...");
+ IncrementalBackupStorageManager storageManager = new IncrementalBackupStorageManager(Paths.get(backupBaseDirectory));
+ int filesAdded = storageManager.addObjectCollection(collection, levelPathFile);
+
+ // save index file
+ PrintUtil.info("Saving index file...");
+ ObjectCollectionSerializer.toFile(collection, new File(backupIndexFileSaveDirectory, backupIndexFileName));
+
+ // return result
+ PrintUtil.info("Incremental backup finished.");
+ return new IncrementalBackupFeedback(filesAdded >= 0, filesAdded);
+ }
+
+ @Override
+ public boolean restore() throws IOException {
+ // load collection
+ PrintUtil.info("Loading file list...");
+ ObjectCollection collection = ObjectCollectionSerializer.fromFile(
+ new File(backupIndexFileSaveDirectory, backupIndexFileName)
+ );
+
+ // delete old level
+ File levelPathFile = new File(levelPath);
+ PrintUtil.info("Deleting old level...");
+ if (!FilesystemUtil.forceDeleteDirectory(levelPathFile)) {
+ PrintUtil.info("Failed to delete old level!");
+ return false;
+ }
+
+ // restore file
+ PrintUtil.info("Copying files...");
+ IncrementalBackupStorageManager storageManager = new IncrementalBackupStorageManager(Paths.get(backupBaseDirectory));
+ int restoreObjectCount = storageManager.restoreObjectCollection(collection, levelPathFile);
+
+ PrintUtil.info(String.format("%d file(s) restored.", restoreObjectCount));
+ return true;
+ }
+
+ @Override
+ public boolean touch() {
+ File baseDirectoryFile = new File(backupBaseDirectory);
+ return baseDirectoryFile.isDirectory() || baseDirectoryFile.mkdir();
+ }
+
+ @Override
+ public String getBackupFileName() {
+ return backupIndexFileName;
+ }
+
+
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/operation/backup/method/ConfiguredPrimitiveBackupMethod.java b/src/main/java/com/keuin/kbackupfabric/operation/backup/method/ConfiguredPrimitiveBackupMethod.java
new file mode 100644
index 0000000..1c3c9f6
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/operation/backup/method/ConfiguredPrimitiveBackupMethod.java
@@ -0,0 +1,88 @@
+package com.keuin.kbackupfabric.operation.backup.method;
+
+import com.keuin.kbackupfabric.exception.ZipUtilException;
+import com.keuin.kbackupfabric.metadata.BackupMetadata;
+import com.keuin.kbackupfabric.operation.backup.feedback.PrimitiveBackupFeedback;
+import com.keuin.kbackupfabric.util.FilesystemUtil;
+import com.keuin.kbackupfabric.util.PrintUtil;
+import com.keuin.kbackupfabric.util.ZipUtil;
+import com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil;
+import com.keuin.kbackupfabric.util.backup.BackupNameTimeFormatter;
+import com.keuin.kbackupfabric.util.backup.name.PrimitiveBackupFileNameEncoder;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.time.LocalDateTime;
+
+public class ConfiguredPrimitiveBackupMethod implements ConfiguredBackupMethod {
+
+ private final String backupFileName;
+ private final String levelPath;
+ private final String backupSavePath;
+
+ public ConfiguredPrimitiveBackupMethod(String backupFileName, String levelPath, String backupSavePath) {
+ this.backupFileName = backupFileName;
+ this.levelPath = levelPath;
+ this.backupSavePath = backupSavePath;
+ }
+
+ @Deprecated
+ private String getBackupFileName(LocalDateTime time, String backupName) {
+ String timeString = BackupNameTimeFormatter.localDateTimeToString(time);
+ return String.format("%s%s_%s%s", BackupFilesystemUtil.getBackupFileNamePrefix(), timeString, backupName, ".zip");
+ }
+
+ @Override
+ public PrimitiveBackupFeedback backup() throws IOException {
+ try {
+ String customBackupName = new PrimitiveBackupFileNameEncoder().decode(backupFileName).customName;
+ BackupMetadata backupMetadata = new BackupMetadata(System.currentTimeMillis(), customBackupName);
+ PrintUtil.info(String.format("zip(srcPath=%s, destPath=%s)", levelPath, backupSavePath));
+ PrintUtil.info("Compressing level ...");
+ ZipUtil.makeBackupZip(levelPath, backupSavePath, backupFileName, backupMetadata);
+ } catch (ZipUtilException exception) {
+ PrintUtil.info("Infinite recursive of directory tree detected, backup was aborted.");
+ return new PrimitiveBackupFeedback(false, 0);
+ }
+
+ // Get backup file size and return
+ return new PrimitiveBackupFeedback(true, FilesystemUtil.getFileSizeBytes(backupSavePath, backupFileName));
+ }
+
+ @Override
+ public boolean restore() throws IOException {
+ // Delete old level
+ PrintUtil.info("Server stopped. Deleting old level ...");
+ if (!FilesystemUtil.forceDeleteDirectory(new File(levelPath))) {
+ PrintUtil.info("Failed to delete old level!");
+ return false;
+ }
+
+
+ // TODO: Refactor this to the concrete BackupMethod.
+ // Decompress archive
+ PrintUtil.info("Decompressing archived level ...");
+ ZipUtil.unzip(Paths.get(backupSavePath, backupFileName).toString(), levelPath, false);
+
+
+// try {
+// Thread.sleep(1000);
+// } catch (InterruptedException ignored) {
+// }
+
+ return true;
+ }
+
+ @Override
+ public boolean touch() {
+ File backupSaveDirectoryFile = new File(backupSavePath);
+ return backupSaveDirectoryFile.isDirectory() || backupSaveDirectoryFile.mkdir();
+ }
+
+ @Override
+ public String getBackupFileName() {
+ return backupFileName;
+ }
+
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/BytesUtil.java b/src/main/java/com/keuin/kbackupfabric/util/BytesUtil.java
new file mode 100644
index 0000000..c33c028
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/util/BytesUtil.java
@@ -0,0 +1,31 @@
+package com.keuin.kbackupfabric.util;
+
+import java.nio.charset.StandardCharsets;
+
+public class BytesUtil {
+ private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);
+
+ public static String bytesToHex(byte[] bytes) {
+ byte[] hexChars = new byte[bytes.length * 2];
+ for (int j = 0; j < bytes.length; j++) {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = HEX_ARRAY[v >>> 4];
+ hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
+ }
+ return new String(hexChars, StandardCharsets.UTF_8).toUpperCase();
+ }
+
+ public static byte[] hexToBytes(String s) {
+ int len = s.length();
+ if (len % 2 != 0)
+ throw new IllegalArgumentException("Invalid hex string.");
+ byte[] b = new byte[len / 2];
+ int index, v;
+ for (int i = 0; i < b.length; i++) {
+ index = i * 2;
+ v = Integer.parseInt(s.substring(index, index + 2), 16);
+ b[i] = (byte) v;
+ }
+ return b;
+ }
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/FilesystemUtil.java b/src/main/java/com/keuin/kbackupfabric/util/FilesystemUtil.java
index 7f74725..f245cff 100644
--- a/src/main/java/com/keuin/kbackupfabric/util/FilesystemUtil.java
+++ b/src/main/java/com/keuin/kbackupfabric/util/FilesystemUtil.java
@@ -1,6 +1,9 @@
package com.keuin.kbackupfabric.util;
import java.io.File;
+import java.io.IOException;
+
+import static org.apache.commons.io.FileUtils.forceDelete;
public class FilesystemUtil {
@@ -22,12 +25,43 @@ public class FilesystemUtil {
public static long getFileSizeBytes(String filePath) {
long fileSize = -1;
- try{
+ try {
File backupZipFile = new File(filePath);
fileSize = backupZipFile.length();
- } catch (SecurityException ignored){
+ } catch (SecurityException ignored) {
}
return fileSize;
}
+ public static boolean forceDeleteDirectory(File levelDirFile) throws IOException {
+ int failedCounter = 0;
+ final int MAX_RETRY_TIMES = 20;
+ IOException exception = null;
+ while (failedCounter < MAX_RETRY_TIMES) {
+ System.gc();
+ if (!levelDirFile.delete() && levelDirFile.exists()) {
+ System.gc();
+ try {
+ forceDelete(levelDirFile); // Try to force delete.
+ } catch (IOException e) {
+ exception = e;
+ }
+ }
+ if (!levelDirFile.exists())
+ break;
+ ++failedCounter;
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException ignored) {
+ }
+ }
+ if (exception != null)
+ throw exception;
+ if (levelDirFile.exists()) {
+ PrintUtil.error(String.format("Cannot restore: failed to delete old level %s .", levelDirFile.getName()));
+ return false;
+ }
+ return true;
+ }
+
}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java b/src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java
index 285fe83..9b7cb83 100644
--- a/src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java
+++ b/src/main/java/com/keuin/kbackupfabric/util/ZipUtil.java
@@ -4,7 +4,10 @@ import com.keuin.kbackupfabric.exception.ZipUtilException;
import com.keuin.kbackupfabric.metadata.BackupMetadata;
import java.io.*;
+import java.util.Collections;
import java.util.Enumeration;
+import java.util.Optional;
+import java.util.Set;
import java.util.zip.*;
import static java.util.zip.Deflater.DEFAULT_COMPRESSION;
@@ -17,9 +20,10 @@ public final class ZipUtil {
* @param srcRootDir 压缩文件夹根目录的子路径
* @param file 当前递归压缩的文件或目录对象
* @param zipOutputStream 压缩文件存储对象
+ * @param filesSkipping 被忽略的文件
* @throws IOException IO Error
*/
- private static void zip(String srcRootDir, File file, ZipOutputStream zipOutputStream) throws IOException {
+ private static void zip(String srcRootDir, File file, ZipOutputStream zipOutputStream, Set<String> filesSkipping) throws IOException {
if (file == null) {
return;
}
@@ -28,7 +32,8 @@ public final class ZipUtil {
return; // Reject
// 如果是文件,则直接压缩该文件
- if (file.isFile()) {
+ boolean skipping = Optional.ofNullable(filesSkipping).orElse(Collections.emptySet()).contains(file.getName());
+ if (file.isFile() && !skipping) {
int count, bufferLen = 1024;
byte[] data = new byte[bufferLen];
@@ -38,6 +43,8 @@ public final class ZipUtil {
if (index != -1) {
subPath = subPath.substring(srcRootDir.length() + File.separator.length());
}
+
+ // 写入压缩包
ZipEntry entry = new ZipEntry(subPath);
zipOutputStream.putNextEntry(entry);
BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file));
@@ -46,14 +53,13 @@ public final class ZipUtil {
}
inputStream.close();
zipOutputStream.closeEntry();
- }
- // 如果是目录,则压缩整个目录
- else {
+ } else {
+ // 如果是目录,则压缩整个目录
// 压缩目录中的文件或子目录
File[] childFileList = file.listFiles();
if (childFileList != null) {
for (File value : childFileList)
- zip(srcRootDir, value, zipOutputStream);
+ zip(srcRootDir, value, zipOutputStream, filesSkipping);
}
}
}
@@ -130,7 +136,7 @@ public final class ZipUtil {
}
}
//调用递归压缩方法进行目录或文件压缩
- zip(srcRootDir, srcFile, zipOutputStream);
+ zip(srcRootDir, srcFile, zipOutputStream, Collections.singleton("session.lock"));
zipOutputStream.flush();
} finally {
try {
diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/BackupFilesystemUtil.java b/src/main/java/com/keuin/kbackupfabric/util/backup/BackupFilesystemUtil.java
index 54c2f58..28ede70 100644
--- a/src/main/java/com/keuin/kbackupfabric/util/backup/BackupFilesystemUtil.java
+++ b/src/main/java/com/keuin/kbackupfabric/util/backup/BackupFilesystemUtil.java
@@ -2,10 +2,13 @@ package com.keuin.kbackupfabric.util.backup;
import com.keuin.kbackupfabric.util.ReflectionUtils;
import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.dedicated.MinecraftDedicatedServer;
import net.minecraft.server.world.ThreadedAnvilChunkStorage;
import net.minecraft.world.World;
import java.io.File;
+import java.io.IOException;
+import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -14,39 +17,46 @@ import java.util.regex.Pattern;
*/
public final class BackupFilesystemUtil {
- private static final String backupSaveDirectoryName = "backups";
+ private static final String BACKUP_SAVE_DIRECTORY_NAME = "backups";
+ private static final String INCREMENTAL_BASE_DIRECTORY_NAME = "incremental";
private static final String backupFileNamePrefix = "kbackup-";
+ @Deprecated
public static String getBackupFileNamePrefix() {
return backupFileNamePrefix;
}
- @Deprecated
- public static String getBackupFileName(String backupName) {
- return backupFileNamePrefix + backupName + ".zip";
- }
- @Deprecated
- public static String getBackupName(String backupFileName) {
- try {
- if (backupFileName.matches(backupFileNamePrefix + ".+\\.zip"))
- return backupFileName.substring(backupFileNamePrefix.length(), backupFileName.length() - 4);
- } catch (IndexOutOfBoundsException ignored) {
- }
- return backupFileName;
- }
+// @Deprecated
+// public static String getBackupName(String backupFileName) {
+// try {
+// if (backupFileName.matches(backupFileNamePrefix + ".+\\.zip"))
+// return backupFileName.substring(backupFileNamePrefix.length(), backupFileName.length() - 4);
+// } catch (IndexOutOfBoundsException ignored) {
+// }
+// return backupFileName;
+// }
- public static boolean isBackupNameValid(String backupName, MinecraftServer server) {
- File backupFile = new File(getBackupSaveDirectory(server), getBackupFileName(backupName));
+ public static boolean isBackupFileExists(String backupFileName, MinecraftServer server) {
+ File backupFile = new File(getBackupSaveDirectory(server), backupFileName);
return backupFile.isFile();
}
public static File getBackupSaveDirectory(MinecraftServer server) {
- return new File(server.getRunDirectory(), backupSaveDirectoryName);
+ return new File(server.getRunDirectory(), BACKUP_SAVE_DIRECTORY_NAME);
+ }
+
+ public static File getIncrementalBackupBaseDirectory(MinecraftServer server) {
+ return new File(server.getRunDirectory(), INCREMENTAL_BASE_DIRECTORY_NAME);
}
- public static String getLevelPath(MinecraftServer server) {
- return (new File(server.getRunDirectory(), server.getLevelName())).getAbsolutePath();
+ public static String getLevelPath(MinecraftServer server) throws IOException {
+ if (!(server instanceof MinecraftDedicatedServer))
+ throw new IllegalStateException("This plugin is server-side only.");
+ String path = (new File(server.getRunDirectory().getCanonicalPath(), ((MinecraftDedicatedServer) server).getLevelName())).getAbsolutePath();
+ Logger.getLogger("getLevelPath").info(String.format("Level path: %s", path));
+ assert (new File(path)).exists();
+ return path;
}
public static String getWorldDirectoryName(World world) throws NoSuchFieldException, IllegalAccessException {
@@ -70,8 +80,8 @@ public final class BackupFilesystemUtil {
return -1;
}
- public static String humanFileSize(long size) {
- double fileSize = size * 1.0 / 1024 / 1024; // Default unit is MB
+ public static String getFriendlyFileSizeString(long sizeBytes) {
+ double fileSize = sizeBytes * 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);
diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/BackupType.java b/src/main/java/com/keuin/kbackupfabric/util/backup/BackupType.java
deleted file mode 100644
index d02ce77..0000000
--- a/src/main/java/com/keuin/kbackupfabric/util/backup/BackupType.java
+++ /dev/null
@@ -1,33 +0,0 @@
-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", "zip"),
- OBJECT_TREE_BACKUP("Object Tree Backup", "incremental");
-
- 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 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
deleted file mode 100644
index f57302c..0000000
--- a/src/main/java/com/keuin/kbackupfabric/util/backup/builder/BackupFileNameBuilder.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.keuin.kbackupfabric.util.backup.builder;
-
-
-import java.time.LocalDateTime;
-
-public interface BackupFileNameBuilder {
-
- static BackupFileNameBuilder primitiveZipBackup() {
- return PrimitiveZipBackupFileNameBuilder.getInstance();
- }
-
- static BackupFileNameBuilder objectTreeBackup() {
- return ObjectTreeBackupFileNameBuilder.getInstance();
- }
-
- /**
- * Build a backup file name based on given information.
- * @param time when the backup was created.
- * @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(LocalDateTime time, String backupName);
-
-}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/builder/ObjectTreeBackupFileNameBuilder.java b/src/main/java/com/keuin/kbackupfabric/util/backup/builder/ObjectTreeBackupFileNameBuilder.java
deleted file mode 100644
index 3c15741..0000000
--- a/src/main/java/com/keuin/kbackupfabric/util/backup/builder/ObjectTreeBackupFileNameBuilder.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.keuin.kbackupfabric.util.backup.builder;
-
-import com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil;
-import com.keuin.kbackupfabric.util.backup.BackupNameTimeFormatter;
-
-import java.time.LocalDateTime;
-
-public class ObjectTreeBackupFileNameBuilder implements BackupFileNameBuilder {
- private static final ObjectTreeBackupFileNameBuilder instance = new ObjectTreeBackupFileNameBuilder();
-
- public static ObjectTreeBackupFileNameBuilder getInstance() {
- return instance;
- }
-
- @Override
- public String build(LocalDateTime time, String backupName) {
- String timeString = BackupNameTimeFormatter.localDateTimeToString(time);
- return String.format("%s%s_%s%s", BackupFilesystemUtil.getBackupFileNamePrefix(),timeString,backupName,".json");
- }
-}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/builder/PrimitiveZipBackupFileNameBuilder.java b/src/main/java/com/keuin/kbackupfabric/util/backup/builder/PrimitiveZipBackupFileNameBuilder.java
deleted file mode 100644
index f910c37..0000000
--- a/src/main/java/com/keuin/kbackupfabric/util/backup/builder/PrimitiveZipBackupFileNameBuilder.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.keuin.kbackupfabric.util.backup.builder;
-
-import com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil;
-import com.keuin.kbackupfabric.util.backup.BackupNameTimeFormatter;
-
-import java.time.LocalDateTime;
-
-public class PrimitiveZipBackupFileNameBuilder implements BackupFileNameBuilder {
-
- private static final PrimitiveZipBackupFileNameBuilder instance = new PrimitiveZipBackupFileNameBuilder();
-
- public static PrimitiveZipBackupFileNameBuilder getInstance() {
- return instance;
- }
-
- @Override
- public String build(LocalDateTime time, String backupName) {
- String timeString = BackupNameTimeFormatter.localDateTimeToString(time);
- return String.format("%s%s_%s%s", BackupFilesystemUtil.getBackupFileNamePrefix(),timeString,backupName,".zip");
- }
-
-}
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
deleted file mode 100644
index a437629..0000000
--- a/src/main/java/com/keuin/kbackupfabric/util/backup/formatter/BackupFileNameFormatter.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.keuin.kbackupfabric.util.backup.formatter;
-
-import com.keuin.kbackupfabric.util.backup.BackupNameTimeFormatter;
-
-import java.time.LocalDateTime;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public interface BackupFileNameFormatter {
-
- BackupFileName format(String fileName);
-
- class BackupFileName {
- public final LocalDateTime time;
- public final String name;
-
- public BackupFileName(LocalDateTime time, String name) {
- this.time = time;
- this.name = name;
- }
- }
-
- static BackupFileNameFormatter objectTreeBackup() {
- return ObjectTreeBackupFileNameFormatter.getInstance();
- }
-
- static BackupFileNameFormatter primitiveZipBackup() {
- return PrimitiveZipBackupFileNameFormatter.getInstance();
- }
-
-}
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
deleted file mode 100644
index 08805b2..0000000
--- a/src/main/java/com/keuin/kbackupfabric/util/backup/formatter/ObjectTreeBackupFileNameFormatter.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.keuin.kbackupfabric.util.backup.formatter;
-
-import com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil;
-import com.keuin.kbackupfabric.util.backup.BackupNameTimeFormatter;
-import org.spongepowered.asm.mixin.Overwrite;
-
-import java.time.LocalDateTime;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class ObjectTreeBackupFileNameFormatter implements BackupFileNameFormatter {
-
- private static final ObjectTreeBackupFileNameFormatter instance = new ObjectTreeBackupFileNameFormatter();
-
- public static ObjectTreeBackupFileNameFormatter getInstance() {
- return instance;
- }
-
- @Override
- public BackupFileNameFormatter.BackupFileName format(String fileName) {
- LocalDateTime time = getTime(fileName);
- String name = getBackupName(fileName);
- return new BackupFileNameFormatter.BackupFileName(time,name);
- }
-
- 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()) {
- String timeString = matcher.group(0);
- return BackupNameTimeFormatter.timeStringToLocalDateTime(timeString);
- }
- return null;
- }
-
- private String getBackupName(String backupFileName) {
- try {
- if (backupFileName.matches(BackupFilesystemUtil.getBackupFileNamePrefix() + ".+\\.json"))
- return backupFileName.substring(BackupFilesystemUtil.getBackupFileNamePrefix().length(), backupFileName.length() - 4);
- } catch (IndexOutOfBoundsException ignored) {
- }
- return backupFileName;
- }
-
-}
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
deleted file mode 100644
index 2d50d17..0000000
--- a/src/main/java/com/keuin/kbackupfabric/util/backup/formatter/PrimitiveZipBackupFileNameFormatter.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package com.keuin.kbackupfabric.util.backup.formatter;
-
-import com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil;
-import com.keuin.kbackupfabric.util.backup.BackupNameTimeFormatter;
-
-import java.time.LocalDateTime;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class PrimitiveZipBackupFileNameFormatter implements BackupFileNameFormatter {
-
- private static final PrimitiveZipBackupFileNameFormatter instance = new PrimitiveZipBackupFileNameFormatter();
-
- public static PrimitiveZipBackupFileNameFormatter getInstance() {
- return instance;
- }
-
- @Override
- public BackupFileNameFormatter.BackupFileName format(String fileName) {
- LocalDateTime time = getTime(fileName);
- String name = getBackupName(fileName);
- return new BackupFileNameFormatter.BackupFileName(time,name);
- }
-
- 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()) {
- String timeString = matcher.group(0);
- return BackupNameTimeFormatter.timeStringToLocalDateTime(timeString);
- }
- return null;
- }
-
- private String getBackupName(String backupFileName) {
- try {
- if (backupFileName.matches(BackupFilesystemUtil.getBackupFileNamePrefix() + ".+\\.zip"))
- return backupFileName.substring(BackupFilesystemUtil.getBackupFileNamePrefix().length(), backupFileName.length() - 4);
- } catch (IndexOutOfBoundsException ignored) {
- }
- return backupFileName;
- }
-
-}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollection.java b/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollection.java
new file mode 100644
index 0000000..16d95e6
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollection.java
@@ -0,0 +1,65 @@
+package com.keuin.kbackupfabric.util.backup.incremental;
+
+import java.io.Serializable;
+import java.util.*;
+
+public class ObjectCollection implements Serializable {
+ private final String name;
+ private final Map<String, ObjectElement> elements;
+ private final Map<String, ObjectCollection> subCollections;
+
+ ObjectCollection(String name, Set<ObjectElement> elements, Map<String, ObjectCollection> subCollections) {
+ this.name = Objects.requireNonNull(name);
+ this.elements = new HashMap<>();
+ for (ObjectElement e : elements) {
+ Objects.requireNonNull(e);
+ if (this.elements.put(e.getName(), e) != null) {
+ throw new IllegalStateException("elements conflict with the same name");
+ }
+ }
+ this.subCollections = new HashMap<>(Objects.requireNonNull(subCollections));
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Set<ObjectElement> getElementSet() {
+ return new HashSet<>(elements.values());
+ }
+
+ public Map<String, ObjectElement> getElementMap() {
+ return Collections.unmodifiableMap(elements);
+ }
+
+ public ObjectElement getElement(String name) {
+ return elements.get(name);
+ }
+
+ public Set<ObjectCollection> getSubCollectionSet() {
+ return new HashSet<>(subCollections.values());
+ }
+
+ public Map<String, ObjectCollection> getSubCollectionMap() {
+ return Collections.unmodifiableMap(subCollections);
+ }
+
+ public ObjectCollection getSubCollection(String name) {
+ return subCollections.get(name);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ObjectCollection that = (ObjectCollection) o;
+ return name.equals(that.name) &&
+ elements.equals(that.elements) &&
+ subCollections.equals(that.subCollections);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, elements, subCollections);
+ }
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionFactory.java b/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionFactory.java
new file mode 100644
index 0000000..2f3761c
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionFactory.java
@@ -0,0 +1,55 @@
+package com.keuin.kbackupfabric.util.backup.incremental;
+
+import com.keuin.kbackupfabric.util.PrintUtil;
+import com.keuin.kbackupfabric.util.backup.incremental.identifier.FileIdentifierProvider;
+import com.keuin.kbackupfabric.util.backup.incremental.identifier.ObjectIdentifier;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+
+/**
+ * Incremental backup is implemented as git-like file collection.
+ * Files are called `objects`, the collection contains all files distinguished by their
+ * identifiers. Usually, identifier is the combination of hash and other short information (such as size and another hash).
+ * The identifier should use hashes that are strong enough, to prevent possible collisions.
+ */
+public class ObjectCollectionFactory<T extends ObjectIdentifier> {
+
+ private final FileIdentifierProvider<T> identifierFactory;
+
+ public ObjectCollectionFactory(FileIdentifierProvider<T> identifierFactory) {
+ this.identifierFactory = identifierFactory;
+ }
+
+ public ObjectCollection fromDirectory(File directory, Set<String> ignoredFiles) throws IOException {
+ final Set<ObjectElement> subFiles = new HashSet<>();
+ final Map<String, ObjectCollection> subCollections = new HashMap<>();
+
+ if (!Objects.requireNonNull(directory).isDirectory())
+ throw new IllegalArgumentException("given file is not a directory");
+
+ for (Iterator<Path> iter = Files.walk(directory.toPath(), 1).iterator(); iter.hasNext(); ) {
+ Path path = iter.next();
+ if (Files.isSameFile(path, directory.toPath()))
+ continue;
+ File file = path.toFile();
+ if (file.isDirectory()) {
+ subCollections.put(file.getName(), fromDirectory(file, ignoredFiles));
+ } else if (!ignoredFiles.contains(file.getName())) {
+ subFiles.add(new ObjectElement(file.getName(), identifierFactory.fromFile(file)));
+ } else {
+ PrintUtil.info(String.format("Skipping file %s.", file.getName()));
+ }
+ }
+
+ return new ObjectCollection(directory.getName(), subFiles, subCollections);
+ }
+
+ public ObjectCollection fromDirectory(File directory) throws IOException {
+ return fromDirectory(directory, Collections.emptySet());
+ }
+
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionSerializer.java b/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionSerializer.java
new file mode 100644
index 0000000..6f9b792
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionSerializer.java
@@ -0,0 +1,33 @@
+package com.keuin.kbackupfabric.util.backup.incremental;
+
+import java.io.*;
+import java.util.Objects;
+
+/**
+ * Serialize and deserialize ObjectCollection from/to the disk file.
+ */
+public class ObjectCollectionSerializer {
+ public static ObjectCollection fromFile(File file) throws IOException {
+ Objects.requireNonNull(file);
+ ObjectCollection collection;
+ try (FileInputStream fileInputStream = new FileInputStream(file)) {
+ try (ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
+ collection = (ObjectCollection) objectInputStream.readObject();
+ } catch (ClassNotFoundException ignored) {
+ // this should not happen
+ return null;
+ }
+ }
+ return collection;
+ }
+
+ public static void toFile(ObjectCollection collection, File file) throws IOException {
+ Objects.requireNonNull(collection);
+ Objects.requireNonNull(file);
+ try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
+ try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {
+ objectOutputStream.writeObject(collection);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectElement.java b/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectElement.java
new file mode 100644
index 0000000..cbb886e
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectElement.java
@@ -0,0 +1,60 @@
+package com.keuin.kbackupfabric.util.backup.incremental;
+
+import com.keuin.kbackupfabric.util.backup.incremental.identifier.ObjectIdentifier;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * Representing a file in a ObjectCollection.
+ * Immutable.
+ */
+public class ObjectElement implements Serializable {
+ private final String name;
+ private final ObjectIdentifier identifier;
+
+ public ObjectElement(String name, ObjectIdentifier identifier) {
+ Objects.requireNonNull(name);
+ Objects.requireNonNull(identifier);
+ this.name = name;
+ this.identifier = identifier;
+ }
+
+ /**
+ * Get file name.
+ * @return the file name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Get file identifier, which is considered to be different between files with different contents.
+ * @return the identifier.
+ */
+ public ObjectIdentifier getIdentifier() {
+ return identifier;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ObjectElement that = (ObjectElement) o;
+ return name.equals(that.name) &&
+ identifier.equals(that.identifier);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, identifier);
+ }
+
+ @Override
+ public String toString() {
+ return "ObjectElement{" +
+ "name='" + name + '\'' +
+ ", identifier=" + identifier +
+ '}';
+ }
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/FileIdentifierProvider.java b/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/FileIdentifierProvider.java
new file mode 100644
index 0000000..3fbe284
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/FileIdentifierProvider.java
@@ -0,0 +1,15 @@
+package com.keuin.kbackupfabric.util.backup.incremental.identifier;
+
+import java.io.File;
+import java.io.IOException;
+
+public interface FileIdentifierProvider<T extends ObjectIdentifier> {
+ /**
+ * Generate file identifier from a random file. The file is not necessarily in the object base.
+ *
+ * @param file the file.
+ * @return the file identifier.
+ * @throws IOException when an I/O error occurs.
+ */
+ T fromFile(File file) throws IOException;
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/ObjectIdentifier.java b/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/ObjectIdentifier.java
new file mode 100644
index 0000000..aece07d
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/ObjectIdentifier.java
@@ -0,0 +1,13 @@
+package com.keuin.kbackupfabric.util.backup.incremental.identifier;
+
+import java.io.Serializable;
+
+/**
+ * The identifier distinguishing files in the object collection.
+ * It should be based on cryptographic hash function in order to prevent possible attacks to the backup system.
+ * All identifiers should be immutable and implement their own equals method.
+ * Immutable.
+ */
+public interface ObjectIdentifier extends Serializable {
+ String getIdentification();
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/Sha256Identifier.java b/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/Sha256Identifier.java
new file mode 100644
index 0000000..c1c87e1
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/Sha256Identifier.java
@@ -0,0 +1,88 @@
+package com.keuin.kbackupfabric.util.backup.incremental.identifier;
+
+import com.keuin.kbackupfabric.util.BytesUtil;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Objects;
+
+/**
+ * Identifier based on sha256.
+ * Immutable.
+ */
+public class Sha256Identifier extends SingleHashIdentifier {
+
+ private static final int SHA256_LENGTH = 32;
+ private static final Sha256Identifier DUMMY = new Sha256Identifier(new byte[SHA256_LENGTH]); // only for using its hash method
+ private static final FileIdentifierProvider<Sha256Identifier> factory = Sha256Identifier::fromFile;
+ private static final String marker = "S2";
+
+ public static Sha256Identifier fromFile(File file) throws IOException {
+ if (!file.isFile()) {
+ throw new IllegalArgumentException("file is not a file");
+ }
+ return new Sha256Identifier(DUMMY.hash(file));
+ }
+
+ /**
+ * Load sha-256 from a named file. Only used in StorageObjectLoader.
+ *
+ * @param fileName the file name.
+ * @return identifier.
+ */
+ static Sha256Identifier fromFileName(String fileName) {
+ if (!fileName.matches(marker + "-[0-9A-Fa-f]{32}"))
+ return null;
+ String hexString = fileName.substring(marker.length() + 1);
+ return new Sha256Identifier(BytesUtil.hexToBytes(hexString));
+ }
+
+ public static FileIdentifierProvider<Sha256Identifier> getFactory() {
+ return factory;
+ }
+
+ protected Sha256Identifier(byte[] hash) {
+ super(hash, marker);
+ Objects.requireNonNull(hash);
+ if (hash.length != SHA256_LENGTH) {
+ throw new IllegalStateException(String.format("SHA256 must be %d bytes", SHA256_LENGTH));
+ }
+ }
+
+ @Override
+ protected byte[] hash(File file) throws IOException {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+
+ try (FileInputStream inputStream = new FileInputStream(file)) {
+ // This does not work. I don't know why
+// FileChannel channel = inputStream.getChannel();
+// ByteBuffer buffer = ByteBuffer.allocate(128);
+// int readLength;
+// while ((readLength = channel.read(buffer)) > 0)
+// digest.update(buffer);
+
+ // This also works, without warnings
+ byte[] readBuffer = new byte[1024 * 1024];
+ int readLength;
+ while ((readLength = inputStream.read(readBuffer)) > 0)
+ digest.update(readBuffer, 0, readLength);
+
+ // The below lines also works, but the IDE will complain about the while loop
+// DigestInputStream digestInputStream = new DigestInputStream(inputStream, digest);
+// while(digestInputStream.read() > 0)
+// ;
+
+ return digest.digest();
+ }
+
+ } catch (NoSuchAlgorithmException ignored) {
+ // this shouldn't happen
+ return new byte[SHA256_LENGTH];
+ }
+ }
+
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/SingleHashIdentifier.java b/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/SingleHashIdentifier.java
new file mode 100644
index 0000000..0f62f2b
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/SingleHashIdentifier.java
@@ -0,0 +1,53 @@
+package com.keuin.kbackupfabric.util.backup.incremental.identifier;
+
+import com.keuin.kbackupfabric.util.BytesUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * A simple identifier based on a single hash function.
+ * Immutable.
+ */
+public abstract class SingleHashIdentifier implements ObjectIdentifier {
+
+ private final byte[] hash;
+ private final String type;
+
+ protected SingleHashIdentifier(byte[] hash, String type) {
+ Objects.requireNonNull(hash);
+ Objects.requireNonNull(type);
+ this.hash = Arrays.copyOf(hash, hash.length);
+ this.type = type;
+ }
+
+ /**
+ * The hash function.
+ *
+ * @param file the file to be hashed.
+ * @return the hash bytes.
+ */
+ protected abstract byte[] hash(File file) throws IOException;
+
+ @Override
+ public String getIdentification() {
+ return type + "-" + BytesUtil.bytesToHex(hash);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof SingleHashIdentifier)) {
+ return false;
+ }
+ return Arrays.equals(hash, ((SingleHashIdentifier) obj).hash);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hash(type);
+ result = 31 * result + Arrays.hashCode(hash);
+ return result;
+ }
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/StorageObjectLoader.java b/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/StorageObjectLoader.java
new file mode 100644
index 0000000..96bc295
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/StorageObjectLoader.java
@@ -0,0 +1,25 @@
+package com.keuin.kbackupfabric.util.backup.incremental.identifier;
+
+import java.io.File;
+import java.util.Objects;
+
+public class StorageObjectLoader {
+ /**
+ * Get identifier from storage file.
+ *
+ * @param file storage file.
+ * @return identifier. If failed, return null.
+ */
+ public static ObjectIdentifier asIdentifier(File file) {
+ Objects.requireNonNull(file);
+ String fileName = file.getName();
+ ObjectIdentifier identifier;
+
+ identifier = Sha256Identifier.fromFileName(fileName);
+ if (identifier != null)
+ return identifier;
+
+ // Add more identifiers.
+ return null;
+ }
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/manager/IncrementalBackupStorageManager.java b/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/manager/IncrementalBackupStorageManager.java
new file mode 100644
index 0000000..6fd339b
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/manager/IncrementalBackupStorageManager.java
@@ -0,0 +1,188 @@
+package com.keuin.kbackupfabric.util.backup.incremental.manager;
+
+import com.keuin.kbackupfabric.util.PrintUtil;
+import com.keuin.kbackupfabric.util.backup.incremental.ObjectCollection;
+import com.keuin.kbackupfabric.util.backup.incremental.ObjectElement;
+import com.keuin.kbackupfabric.util.backup.incremental.identifier.ObjectIdentifier;
+import com.keuin.kbackupfabric.util.backup.incremental.identifier.StorageObjectLoader;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+
+import static org.apache.commons.io.FileUtils.forceDelete;
+
+public class IncrementalBackupStorageManager {
+
+ private final Path backupStorageBase;
+ private final Map<ObjectIdentifier, File> map = new HashMap<>();
+ private boolean loaded = false;
+
+ public IncrementalBackupStorageManager(Path backupStorageBase) {
+ this.backupStorageBase = backupStorageBase;
+ }
+
+ /**
+ * Add a object collection to storage base.
+ * @param collection the collection.
+ * @return objects copied to the base.
+ * @throws IOException I/O Error.
+ */
+ public int addObjectCollection(ObjectCollection collection, File collectionBasePath) throws IOException {
+ if (!backupStorageBase.toFile().isDirectory()) {
+ if (!backupStorageBase.toFile().mkdirs())
+ throw new IOException("Backup storage base directory does not exist, and failed to create it.");
+ }
+ Objects.requireNonNull(collection);
+ Objects.requireNonNull(collectionBasePath);
+
+ int copyCount = 0;
+
+ // copy sub files
+ for (Map.Entry<String, ObjectElement> entry : collection.getElementMap().entrySet()) {
+ File copyDestination = new File(backupStorageBase.toFile(), entry.getValue().getIdentifier().getIdentification());
+ if (!baseContainsObject(entry.getValue())) {
+ // element does not exist. copy.
+ Files.copy(Paths.get(collectionBasePath.getAbsolutePath(), entry.getKey()), copyDestination.toPath());
+ ++copyCount;
+ }
+ }
+
+ //copy sub dirs recursively
+ for (Map.Entry<String, ObjectCollection> entry : collection.getSubCollectionMap().entrySet()) {
+ File newBase = new File(collectionBasePath, entry.getKey());
+ copyCount += addObjectCollection(entry.getValue(), newBase);
+ }
+
+ return copyCount;
+ }
+
+ /**
+ * Restore an object collection from the storage base. i.e., restore the save from backup storage.
+ * @param collection the collection to be restored.
+ * @param collectionBasePath save path of the collection.
+ * @return objects restored from the base.
+ * @throws IOException I/O Error.
+ */
+ public int restoreObjectCollection(ObjectCollection collection, File collectionBasePath) throws IOException {
+ Objects.requireNonNull(collection);
+ Objects.requireNonNull(collectionBasePath);
+
+ int copyCount = 0;
+
+ // touch directory
+ if (!collectionBasePath.exists()) {
+ int retryCounter = 0;
+ boolean success = false;
+ while (retryCounter++ < 5) {
+ if (collectionBasePath.mkdirs()) {
+ success = true;
+ break;
+ }
+ }
+ if (!success) {
+ throw new IOException("Failed to create directory " + collectionBasePath.getAbsolutePath());
+ }
+ }
+
+ // copy sub files
+ for (Map.Entry<String, ObjectElement> entry : collection.getElementMap().entrySet()) {
+ File copySource = new File(backupStorageBase.toFile(), entry.getValue().getIdentifier().getIdentification());
+ File copyTarget = new File(collectionBasePath.getAbsolutePath(), entry.getKey());
+
+ if (!baseContainsObject(entry.getValue())) {
+ throw new IOException(String.format("File %s does not exist in the base.", copySource.getName()));
+ }
+ if (copyTarget.exists()) {
+ boolean successDeleting = false;
+ for (int i = 0; i < 5; ++i) {
+ try {
+ forceDelete(copyTarget);
+ successDeleting = true;
+ break;
+ } catch (FileNotFoundException ignored) {
+ break;
+ } catch (IOException e) {
+ PrintUtil.error(String.format("Failed to delete file %s, retry.", copyTarget.getName()));
+ }
+ }
+ if (!successDeleting) {
+ String msg = String.format("Failed to delete file %s.", copyTarget.getName());
+ PrintUtil.error(msg);
+ throw new IOException(msg);
+ }
+ }
+
+ Files.copy(copySource.toPath(), copyTarget.toPath());
+ ++copyCount;
+ }
+
+ //copy sub dirs recursively
+ for (Map.Entry<String, ObjectCollection> entry : collection.getSubCollectionMap().entrySet()) {
+ File newBase = new File(collectionBasePath, entry.getKey());
+ copyCount += restoreObjectCollection(entry.getValue(), newBase);
+ }
+
+ return copyCount;
+ }
+
+ public int cleanUnusedObjects(Iterable<ObjectCollection> collectionIterable) {
+ // construct object list in memory
+ Set<String> objects = new HashSet<>();
+// backupStorageBase
+
+ for (ObjectCollection collection : collectionIterable) {
+ for (ObjectElement ele : collection.getElementMap().values()) {
+
+ }
+ }
+ throw new RuntimeException("not impl");
+ }
+
+ /**
+ * Check all objects, return unused ones.
+ *
+ * @return the unused ones.
+ */
+ private Map<ObjectIdentifier, File> markUnusedObjects() {
+ throw new RuntimeException("not impl");
+ }
+
+ /**
+ * Check if the backup base contains given element.
+ *
+ * @param objectElement the element.
+ * @return true or false.
+ */
+ private boolean baseContainsObject(ObjectElement objectElement) {
+ // This may be extended to use more variants of hash functions and combinations of other attributes (such as file size)
+ return (new File(backupStorageBase.toFile(), objectElement.getIdentifier().getIdentification())).exists();
+ }
+
+ private void lazyLoadStorage() throws IOException {
+ if (!loaded) {
+ loadStorage();
+ loaded = true;
+ }
+ }
+
+ private synchronized void loadStorage() throws IOException {
+ map.clear();
+ Files.walk(backupStorageBase, 1).forEach(path -> {
+ File file = path.toFile();
+ ObjectIdentifier identifier = StorageObjectLoader.asIdentifier(file);
+ if (identifier == null) {
+ map.clear();
+ throw new IllegalStateException(String.format(
+ "Bad storage object %s: cannot recognize identifier.", file.getName()
+ ));
+ }
+ map.put(identifier, file);
+ });
+ }
+
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/name/BackupFileNameEncoder.java b/src/main/java/com/keuin/kbackupfabric/util/backup/name/BackupFileNameEncoder.java
new file mode 100644
index 0000000..8ebc7ff
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/util/backup/name/BackupFileNameEncoder.java
@@ -0,0 +1,64 @@
+package com.keuin.kbackupfabric.util.backup.name;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * Encode and decode backup file name for a specific backup type.
+ */
+public interface BackupFileNameEncoder {
+
+ /**
+ * Construct full backup file name from custom name and creation time.
+ * @param customName the custom name. If the custom name contains invalid chars, an exception will be thrown.
+ * @param time the creation time.
+ * @return the file name.
+ */
+ String encode(String customName, LocalDateTime time);
+
+ /**
+ * Extract custom and backup time from backup file name.
+ *
+ * @param fileName the backup file name.
+ * @return the information. If the given file name is invalid, return null.
+ */
+ BackupBasicInformation decode(String fileName);
+
+ default boolean isValidFileName(String fileName) {
+ return decode(fileName) != null;
+ }
+
+ /**
+ * Check if the given string is a valid custom backup name.
+ *
+ * @param customName the custom backup name.
+ * @return if the name is valid.
+ */
+ default boolean isValidCustomName(String customName) {
+ final char[] ILLEGAL_CHARACTERS = {'/', '\n', '\r', '\t', '\0', '\f', '`', '?', '*', '\\', '<', '>', '|', '\"', ':'};
+ for (char c : ILLEGAL_CHARACTERS) {
+ if (customName.contains(String.valueOf(c))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ class BackupBasicInformation {
+
+ public final String customName;
+ public final LocalDateTime time;
+
+ private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm.ss");
+
+ protected BackupBasicInformation(String customName, LocalDateTime time) {
+ this.customName = customName;
+ this.time = time;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s, %s", customName, time.format(formatter));
+ }
+ }
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/name/IncrementalBackupFileNameEncoder.java b/src/main/java/com/keuin/kbackupfabric/util/backup/name/IncrementalBackupFileNameEncoder.java
new file mode 100644
index 0000000..926f47c
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/util/backup/name/IncrementalBackupFileNameEncoder.java
@@ -0,0 +1,37 @@
+package com.keuin.kbackupfabric.util.backup.name;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class IncrementalBackupFileNameEncoder implements BackupFileNameEncoder {
+ private static final String backupFileNamePrefix = "incremental";
+ private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
+
+ // TODO: make this private and use singleton pattern
+ public IncrementalBackupFileNameEncoder() {
+ }
+
+ @Override
+ public String encode(String customName, LocalDateTime time) {
+ if (!isValidCustomName(customName))
+ throw new IllegalArgumentException("Invalid custom name");
+ String timeString = time.format(formatter);
+ return backupFileNamePrefix + "-" + timeString + "_" + customName + ".kbi";
+ }
+
+ @Override
+ public BackupFileNameEncoder.BackupBasicInformation decode(String fileName) {
+ Pattern pattern = Pattern.compile(
+ "^" + backupFileNamePrefix + "-" + "([0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2})_(.+)\\.kbi" + "$"
+ );
+ Matcher matcher = pattern.matcher(fileName);
+ if (matcher.find()) {
+ String timeString = matcher.group(1);
+ String customName = matcher.group(2);
+ return new BackupFileNameEncoder.BackupBasicInformation(customName, LocalDateTime.parse(timeString, formatter));
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/name/PrimitiveBackupFileNameEncoder.java b/src/main/java/com/keuin/kbackupfabric/util/backup/name/PrimitiveBackupFileNameEncoder.java
new file mode 100644
index 0000000..ef15ae7
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/util/backup/name/PrimitiveBackupFileNameEncoder.java
@@ -0,0 +1,33 @@
+package com.keuin.kbackupfabric.util.backup.name;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class PrimitiveBackupFileNameEncoder implements BackupFileNameEncoder {
+ private static final String backupFileNamePrefix = "kbackup";
+ private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
+
+ @Override
+ public String encode(String customName, LocalDateTime time) {
+ if (!isValidCustomName(customName))
+ throw new IllegalArgumentException("Invalid custom name");
+ String timeString = time.format(formatter);
+ return backupFileNamePrefix + "-" + timeString + "_" + customName + ".zip";
+ }
+
+ @Override
+ public BackupBasicInformation decode(String fileName) {
+ Pattern pattern = Pattern.compile(
+ "^" + backupFileNamePrefix + "-" + "([0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2})_(.+)\\.zip" + "$"
+ );
+ Matcher matcher = pattern.matcher(fileName);
+ if (matcher.find()) {
+ String timeString = matcher.group(1);
+ String customName = matcher.group(2);
+ return new BackupBasicInformation(customName, LocalDateTime.parse(timeString, formatter));
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/provider/AvailableBackupProvider.java b/src/main/java/com/keuin/kbackupfabric/util/backup/provider/AvailableBackupProvider.java
new file mode 100644
index 0000000..caa0e84
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/util/backup/provider/AvailableBackupProvider.java
@@ -0,0 +1,8 @@
+package com.keuin.kbackupfabric.util.backup.provider;
+
+/**
+ * List all backup in disk. Provide their basic information as soon as possible.
+ */
+public class AvailableBackupProvider {
+ // TODO: remove obsolete impl. in command user interface. Use this instead.
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/provider/IncrementalBackupInformation.java b/src/main/java/com/keuin/kbackupfabric/util/backup/provider/IncrementalBackupInformation.java
new file mode 100644
index 0000000..861d210
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/util/backup/provider/IncrementalBackupInformation.java
@@ -0,0 +1,13 @@
+package com.keuin.kbackupfabric.util.backup.provider;
+
+import com.keuin.kbackupfabric.util.backup.name.BackupFileNameEncoder;
+
+import java.time.LocalDateTime;
+
+public class IncrementalBackupInformation extends BackupFileNameEncoder.BackupBasicInformation {
+ // TODO: show total size for incremental backup
+
+ public IncrementalBackupInformation(String customName, LocalDateTime time) {
+ super(customName, time);
+ }
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/provider/PrimitiveBackupInformation.java b/src/main/java/com/keuin/kbackupfabric/util/backup/provider/PrimitiveBackupInformation.java
new file mode 100644
index 0000000..d3d2db8
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/util/backup/provider/PrimitiveBackupInformation.java
@@ -0,0 +1,14 @@
+package com.keuin.kbackupfabric.util.backup.provider;
+
+import com.keuin.kbackupfabric.util.backup.name.BackupFileNameEncoder;
+
+import java.time.LocalDateTime;
+
+public class PrimitiveBackupInformation extends BackupFileNameEncoder.BackupBasicInformation {
+ public final long sizeBytes;
+
+ public PrimitiveBackupInformation(String customName, LocalDateTime time, long sizeBytes) {
+ super(customName, time);
+ this.sizeBytes = sizeBytes;
+ }
+}
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
deleted file mode 100644
index 320d9bf..0000000
--- a/src/main/java/com/keuin/kbackupfabric/util/backup/suggestion/BackupMethodSuggestionProvider.java
+++ /dev/null
@@ -1,36 +0,0 @@
-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/suggestion/BackupNameSuggestionProvider.java b/src/main/java/com/keuin/kbackupfabric/util/backup/suggestion/BackupNameSuggestionProvider.java
index f6f4056..01152c2 100644
--- a/src/main/java/com/keuin/kbackupfabric/util/backup/suggestion/BackupNameSuggestionProvider.java
+++ b/src/main/java/com/keuin/kbackupfabric/util/backup/suggestion/BackupNameSuggestionProvider.java
@@ -1,6 +1,5 @@
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;
@@ -38,7 +37,7 @@ public class BackupNameSuggestionProvider {
if (files == null)
return;
for (File f : files)
- candidateCacheList.add(BackupFilesystemUtil.getBackupName(f.getName()));
+ candidateCacheList.add(f.getName());
cacheUpdateTime = System.currentTimeMillis();
} catch (NullPointerException ignored) {
}
diff --git a/src/test/java/IncrementalBackupUtilTest.java b/src/test/java/IncrementalBackupUtilTest.java
deleted file mode 100644
index b050a91..0000000
--- a/src/test/java/IncrementalBackupUtilTest.java
+++ /dev/null
@@ -1,16 +0,0 @@
-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
diff --git a/src/test/java/com/keuin/kbackupfabric/operation/backup/method/ConfiguredIncrementalBackupMethodTest.java b/src/test/java/com/keuin/kbackupfabric/operation/backup/method/ConfiguredIncrementalBackupMethodTest.java
new file mode 100644
index 0000000..30f2d44
--- /dev/null
+++ b/src/test/java/com/keuin/kbackupfabric/operation/backup/method/ConfiguredIncrementalBackupMethodTest.java
@@ -0,0 +1,213 @@
+package com.keuin.kbackupfabric.operation.backup.method;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.junit.Test;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.function.Function;
+
+import static org.apache.commons.io.FileUtils.forceDelete;
+import static org.junit.Assert.*;
+
+public class ConfiguredIncrementalBackupMethodTest {
+
+ private final String testTempPath = (new File("R:\\").isDirectory()) ? "R:\\" : ".\\testfile\\ConfiguredIncrementalBackupMethodTest";
+ private final String sourceDirectoryName = "source";
+ private final String destDirectoryName = "destination";
+ private final String indexFileName = "index";
+
+ private final double directoryFactor = 0.02;
+ private final double fileFactor = 0.05;
+ private final int maxRandomFileSizeBytes = 1024 * 1024 * 16;
+ private final Function<Integer, Integer> scaleDecayFunc = (x) -> x - 1;
+
+ @Test
+ public void iterationTest() throws IOException {
+ int a = 12;
+ for (int i = 0; i < a; ++i) {
+ performTest(Math.min(i + 1, 10));
+ System.out.println("Round " + i + " passed.");
+ }
+ }
+
+ private void performTest(int scale) throws IOException {
+
+ // init source and destination
+ final Path sourcePath = Paths.get(testTempPath, sourceDirectoryName);
+ final Path destPath = Paths.get(testTempPath, destDirectoryName);
+ if (new File(sourcePath.toString()).exists()) {
+ forceDelete(new File(sourcePath.toString()));
+ if (!new File(sourcePath.toString()).mkdirs())
+ fail();
+ }
+ if (new File(destPath.toString()).exists()) {
+ forceDelete(new File(destPath.toString()));
+ if (!new File(destPath.toString()).mkdirs())
+ fail();
+ }
+ if (new File(testTempPath, indexFileName).exists()) {
+ if (!new File(testTempPath, indexFileName).delete())
+ fail();
+ }
+
+ // initialize src
+ createRandomDirectoryTree(sourcePath.toString(), scale);
+
+ String hash1 = calcMD5HashForDir(sourcePath.toFile(), true);
+
+ // copy src to dest
+ ConfiguredIncrementalBackupMethod method = new ConfiguredIncrementalBackupMethod(
+ indexFileName,
+ sourcePath.toString(),
+ testTempPath,
+ destPath.toString()
+ );
+ method.backup();
+
+ // delete src
+ forceDelete(sourcePath.toFile());
+ assertFalse(sourcePath.toFile().isDirectory());
+
+ // restore src
+ if (!method.restore())
+ fail();
+
+ boolean fake = scale % 2 != 0;
+
+ int[] success = new int[1];
+ if (fake) {
+ Files.walk(sourcePath).filter(path -> path.toFile().isFile()).limit(3).forEach(path -> {
+ if (!path.toFile().delete())
+ fail();
+ success[0]++;
+ });
+ if (success[0] == 0)
+ fake = false;
+ }
+
+ if (fake)
+ System.out.println("Fake: deleted " + success[0] + " file(s).");
+
+ String hash2 = calcMD5HashForDir(sourcePath.toFile(), true);
+
+ if (!fake)
+ assertEquals(hash1, hash2);
+ else
+ assertNotEquals(hash1, hash2);
+ }
+
+ private void createRandomDirectoryTree(String path, int scale) throws IOException {
+ if (scale <= 0) {
+ if (Math.random() < 0.5)
+ if (!new File(path).mkdirs() && !new File(path).exists())
+ throw new IOException("Failed to create directory " + path);
+ return;
+ }
+ if (!new File(path).isDirectory() && !new File(path).mkdirs())
+ throw new IOException("Failed to create directory " + path);
+
+ int subFileCount = (int) Math.round(Math.random() * 10 * scale * fileFactor);
+ for (int i = 0; i < subFileCount; i++) {
+ String subFile = null;
+ while (subFile == null || new File(path, subFile).exists())
+ subFile = getRandomString((int) (Math.random() * 16 + 5));
+ createRandomFile(new File(path, subFile), maxRandomFileSizeBytes);
+ }
+
+
+ int subDirCount = (int) Math.round(Math.random() * 10 * scale * directoryFactor);
+ for (int i = 0; i < subDirCount; i++) {
+ String subDir = null;
+ while (subDir == null || new File(path, subDir).exists())
+ subDir = getRandomString((int) (Math.random() * 16 + 5));
+ createRandomDirectoryTree(new File(path, subDir).getAbsolutePath(), scaleDecayFunc.apply(scale));
+ }
+ }
+
+ private static void createRandomFile(File file, int maxSizeBytes) throws IOException {
+ if (!file.createNewFile())
+ throw new IOException("Failed to create file " + file.getAbsolutePath());
+ try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
+ int fileBytes = (int) (maxSizeBytes * Math.random() + 1);
+ Random random = new Random();
+ final int chunkSize = 1024 * 4;
+ byte[] randomChunk = new byte[chunkSize];
+ for (int i = 0; i < fileBytes / chunkSize; i++) {
+ random.nextBytes(randomChunk);
+ fileOutputStream.write(randomChunk);
+ }
+ if (fileBytes % chunkSize != 0) {
+ randomChunk = new byte[fileBytes % chunkSize];
+ random.nextBytes(randomChunk);
+ fileOutputStream.write(randomChunk);
+ }
+ }
+ }
+
+ private static String getRandomString(int length) {
+ String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ Random random = new Random();
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ int number = random.nextInt(62);
+ sb.append(str.charAt(number));
+ }
+ return sb.toString();
+ }
+
+ public String calcMD5HashForDir(File dirToHash, boolean includeHiddenFiles) {
+
+ assert (dirToHash.isDirectory());
+ Vector<FileInputStream> fileStreams = new Vector<>();
+
+ System.out.println("Found files for hashing:");
+ collectInputStreams(dirToHash, fileStreams, includeHiddenFiles);
+
+ SequenceInputStream seqStream =
+ new SequenceInputStream(fileStreams.elements());
+
+ try {
+ String md5Hash = DigestUtils.md5Hex(seqStream);
+ seqStream.close();
+ return md5Hash;
+ } catch (IOException e) {
+ throw new RuntimeException("Error reading files to hash in "
+ + dirToHash.getAbsolutePath(), e);
+ }
+
+ }
+
+ private void collectInputStreams(File dir,
+ List<FileInputStream> foundStreams,
+ boolean includeHiddenFiles) {
+
+ File[] fileList = dir.listFiles();
+ Arrays.sort(fileList, // Need in reproducible order
+ new Comparator<File>() {
+ public int compare(File f1, File f2) {
+ return f1.getName().compareTo(f2.getName());
+ }
+ });
+
+ for (File f : fileList) {
+ if (!includeHiddenFiles && f.getName().startsWith(".")) {
+ // Skip it
+ } else if (f.isDirectory()) {
+ collectInputStreams(f, foundStreams, includeHiddenFiles);
+ } else {
+ try {
+ System.out.println("\t" + f.getAbsolutePath());
+ foundStreams.add(new FileInputStream(f));
+ } catch (FileNotFoundException e) {
+ throw new AssertionError(e.getMessage()
+ + ": file should never not be found!");
+ }
+ }
+ }
+
+ }
+} \ No newline at end of file
diff --git a/src/test/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionFactoryTest.java b/src/test/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionFactoryTest.java
new file mode 100644
index 0000000..3f722a4
--- /dev/null
+++ b/src/test/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionFactoryTest.java
@@ -0,0 +1,80 @@
+package com.keuin.kbackupfabric.util.backup.incremental;
+
+import com.keuin.kbackupfabric.util.backup.incremental.identifier.Sha256Identifier;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+
+import static org.junit.Assert.*;
+
+public class ObjectCollectionFactoryTest {
+
+ private void validate(ObjectCollection collection, List<String> subCollections, Map<String, String> subElements) {
+ assertEquals(subCollections.size(), collection.getSubCollectionMap().size());
+ assertEquals(subElements.size(), collection.getElementSet().size());
+ for (Map.Entry<String, ObjectCollection> c : collection.getSubCollectionMap().entrySet()) {
+ assertEquals(c.getKey(), c.getValue().getName());
+ assertTrue(subCollections.contains(c.getKey()));
+ }
+ for (Map.Entry<String, ObjectElement> entry : collection.getElementMap().entrySet()) {
+// assertTrue(subElements.contains(e.getIdentification()));
+ assertEquals(subElements.get(entry.getKey()).toUpperCase(), entry.getValue().getIdentifier().getIdentification().toUpperCase());
+ }
+ }
+
+ @Test
+ public void fromDirectory() {
+ try {
+ ObjectCollectionFactory<Sha256Identifier> factory =
+ new ObjectCollectionFactory<>(Sha256Identifier.getFactory());
+ ObjectCollection collection =
+ factory.fromDirectory(new File("./testfile/ObjectCollectionFactoryTest"));
+
+ assertEquals("ObjectCollectionFactoryTest", collection.getName());
+ assertEquals(3, collection.getSubCollectionMap().size());
+ assertEquals(2, collection.getElementSet().size());
+
+ final Map<String, String> elements = new HashMap<>();
+
+ // check root dir
+ elements.put("a", "S2-261CA0D59FEE8FD169802BB8030A07CF23E5C1593FA81A16C6D0A8CF27DAA2ED");
+ elements.put("b", "S2-B3FED75012C4969DC63A50EBC4E745FF77E4A06E0B04720EF71EF033032EBAF7");
+ validate(collection, Arrays.asList("1", "2", "3"), elements);
+ elements.clear();
+
+ // check `1`
+ elements.put("a", "S2-E8620F35A5DB33B1257CC51245DDACDA8AF3E0D431A8A38473575E468BCBD0BD");
+ elements.put("b", "S2-19EE41585A674274891DE5A4B365DBAB9C49C576AB6F86CD515B683724D2DBBD");
+ validate(collection.getSubCollectionMap().get("1"), Arrays.asList("11", "12"), elements);
+ elements.clear();
+
+ // check `2`
+ validate(collection.getSubCollectionMap().get("2"), Collections.emptyList(), Collections.emptyMap());
+
+ // check `3`
+ validate(collection.getSubCollectionMap().get("3"), Collections.emptyList(), Collections.emptyMap());
+
+ // check `11`
+ validate(collection.getSubCollectionMap().get("1").getSubCollectionMap().get("11"), Collections.singletonList("111"), Collections.emptyMap());
+
+ // check `111`
+ elements.put("a", "S2-1EDBE882A757E1FAFCA77A9D3BE3FF5D2BB3E2037B238C865F1F957C431F43B4");
+ elements.put("b", "S2-30BA7CD8B4AD93A8B3826CD8D1518790924EEBB930EC04DF7DFB03A50B17D7BC");
+ validate(
+ collection.getSubCollectionMap().get("1").getSubCollectionMap().get("11").getSubCollectionMap().get("111"),
+ Collections.emptyList(),
+ elements
+ );
+ elements.clear();
+
+ // check `12`
+ validate(collection.getSubCollectionMap().get("1").getSubCollectionMap().get("12"), Collections.emptyList(), Collections.emptyMap());
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/test/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionSerializerTest.java b/src/test/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionSerializerTest.java
new file mode 100644
index 0000000..0edfe01
--- /dev/null
+++ b/src/test/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionSerializerTest.java
@@ -0,0 +1,29 @@
+package com.keuin.kbackupfabric.util.backup.incremental;
+
+import com.keuin.kbackupfabric.util.backup.incremental.identifier.Sha256Identifier;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
+import static org.junit.Assert.assertEquals;
+
+public class ObjectCollectionSerializerTest {
+ @Test
+ public void testSerializationConsistency() throws IOException {
+ ObjectCollectionFactory<Sha256Identifier> factory =
+ new ObjectCollectionFactory<>(Sha256Identifier.getFactory());
+ ObjectCollection collection =
+ factory.fromDirectory(new File("./testfile/ObjectCollectionFactoryTest"));
+ File file = new File("./testfile/serialized");
+ if (file.exists()) {
+ Files.delete(file.toPath());
+ }
+ ObjectCollectionSerializer.toFile(collection, file);
+ ObjectCollection collection2 = ObjectCollectionSerializer.fromFile(file);
+ Files.delete(file.toPath());
+ assertEquals(collection, collection2);
+ }
+
+} \ No newline at end of file
diff --git a/src/test/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/Sha256IdentifierTest.java b/src/test/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/Sha256IdentifierTest.java
new file mode 100644
index 0000000..f799a95
--- /dev/null
+++ b/src/test/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/Sha256IdentifierTest.java
@@ -0,0 +1,25 @@
+package com.keuin.kbackupfabric.util.backup.incremental.identifier;
+
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class Sha256IdentifierTest {
+
+ @Test
+ public void fromFile() {
+ try {
+ Sha256Identifier sha256 = Sha256Identifier.fromFile(new File("./testfile/Sha256IdentifierTest"));
+ String str = sha256.getIdentification().toUpperCase();
+ assertEquals("S2-315F5BDB76D078C43B8AC0064E4A0164612B1FCE77C869345BFC94C75894EDD3", str);
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail();
+ }
+ }
+
+} \ No newline at end of file
diff --git a/src/test/java/com/keuin/kbackupfabric/util/backup/name/IncrementalBackupFileNameEncoderTest.java b/src/test/java/com/keuin/kbackupfabric/util/backup/name/IncrementalBackupFileNameEncoderTest.java
new file mode 100644
index 0000000..e5fedd7
--- /dev/null
+++ b/src/test/java/com/keuin/kbackupfabric/util/backup/name/IncrementalBackupFileNameEncoderTest.java
@@ -0,0 +1,38 @@
+package com.keuin.kbackupfabric.util.backup.name;
+
+import org.junit.Test;
+
+import java.time.LocalDateTime;
+
+import static org.junit.Assert.*;
+
+public class IncrementalBackupFileNameEncoderTest {
+ @Test
+ public void testEncode() {
+ LocalDateTime time = LocalDateTime.of(1, 1, 1, 1, 1, 1);
+ String customName = "name";
+ IncrementalBackupFileNameEncoder encoder = new IncrementalBackupFileNameEncoder();
+ assertEquals("incremental-0001-01-01_01-01-01_name.kbi", encoder.encode(customName, time));
+ }
+
+ @Test
+ public void testDecode() {
+ LocalDateTime time = LocalDateTime.of(1, 1, 1, 1, 1, 1);
+ String customName = "name";
+ IncrementalBackupFileNameEncoder encoder = new IncrementalBackupFileNameEncoder();
+ BackupFileNameEncoder.BackupBasicInformation information = encoder.decode("incremental-0001-01-01_01-01-01_name.kbi");
+ assertEquals(time, information.time);
+ assertEquals(customName, information.customName);
+ }
+
+ @Test
+ public void isValid() {
+ IncrementalBackupFileNameEncoder encoder = new IncrementalBackupFileNameEncoder();
+ assertTrue(encoder.isValidFileName("incremental-0001-01-01_01-01-01_name.kbi"));
+ assertTrue(encoder.isValidFileName("incremental-0001-01-01_01-01-01_0001-01-01_01-01-01_name.kbi"));
+ assertFalse(encoder.isValidFileName("incremental-0001-01-01_01-01-01incremental-0001-01-01_01-01-01_name.kbi"));
+ assertFalse(encoder.isValidFileName("incremental-0001-01-01_01-01-01_name"));
+ assertFalse(encoder.isValidFileName("incremental-0001-01-01_01-01-01_name.zip"));
+ assertFalse(encoder.isValidFileName("somefile"));
+ }
+} \ No newline at end of file
diff --git a/src/test/java/com/keuin/kbackupfabric/util/backup/name/PrimitiveBackupFileNameEncoderTest.java b/src/test/java/com/keuin/kbackupfabric/util/backup/name/PrimitiveBackupFileNameEncoderTest.java
new file mode 100644
index 0000000..abc19d5
--- /dev/null
+++ b/src/test/java/com/keuin/kbackupfabric/util/backup/name/PrimitiveBackupFileNameEncoderTest.java
@@ -0,0 +1,50 @@
+package com.keuin.kbackupfabric.util.backup.name;
+
+import org.junit.Test;
+
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+
+import static org.junit.Assert.*;
+
+public class PrimitiveBackupFileNameEncoderTest {
+
+ @Test
+ public void testConsistency() {
+ LocalDateTime time = LocalDateTime.ofEpochSecond(System.currentTimeMillis() / 1000, 0, ZoneOffset.UTC);
+ String name = "Test Na_me";
+ PrimitiveBackupFileNameEncoder encoder = new PrimitiveBackupFileNameEncoder();
+ BackupFileNameEncoder.BackupBasicInformation information = encoder.decode(encoder.encode(name, time));
+ assertEquals(time, information.time);
+ assertEquals(name, information.customName);
+ }
+
+ @Test
+ public void testEncode() {
+ LocalDateTime time = LocalDateTime.of(1, 1, 1, 1, 1, 1);
+ String customName = "name";
+ PrimitiveBackupFileNameEncoder encoder = new PrimitiveBackupFileNameEncoder();
+ assertEquals("kbackup-0001-01-01_01-01-01_name.zip", encoder.encode(customName, time));
+ }
+
+ @Test
+ public void testDecode() {
+ LocalDateTime time = LocalDateTime.of(1, 1, 1, 1, 1, 1);
+ String customName = "name";
+ PrimitiveBackupFileNameEncoder encoder = new PrimitiveBackupFileNameEncoder();
+ BackupFileNameEncoder.BackupBasicInformation information = encoder.decode("kbackup-0001-01-01_01-01-01_name.zip");
+ assertEquals(time, information.time);
+ assertEquals(customName, information.customName);
+ }
+
+ @Test
+ public void isValid() {
+ PrimitiveBackupFileNameEncoder encoder = new PrimitiveBackupFileNameEncoder();
+ assertTrue(encoder.isValidFileName("kbackup-0001-01-01_01-01-01_name.zip"));
+ assertTrue(encoder.isValidFileName("kbackup-0001-01-01_01-01-01_0001-01-01_01-01-01_name.zip"));
+ assertFalse(encoder.isValidFileName("kbackup-0001-01-01_01-01-01kbackup-0001-01-01_01-01-01_name.zip"));
+ assertFalse(encoder.isValidFileName("kbackup-0001-01-01_01-01-01_name"));
+ assertFalse(encoder.isValidFileName("kbackup-0001-01-01_01-01-01_name.kbi"));
+ assertFalse(encoder.isValidFileName("somefile"));
+ }
+} \ No newline at end of file
diff --git a/testfile/ObjectCollectionFactoryTest/1/11/111/a b/testfile/ObjectCollectionFactoryTest/1/11/111/a
new file mode 100644
index 0000000..13a4f6c
--- /dev/null
+++ b/testfile/ObjectCollectionFactoryTest/1/11/111/a
Binary files differ
diff --git a/testfile/ObjectCollectionFactoryTest/1/11/111/b b/testfile/ObjectCollectionFactoryTest/1/11/111/b
new file mode 100644
index 0000000..52755f5
--- /dev/null
+++ b/testfile/ObjectCollectionFactoryTest/1/11/111/b
Binary files differ
diff --git a/testfile/ObjectCollectionFactoryTest/1/a b/testfile/ObjectCollectionFactoryTest/1/a
new file mode 100644
index 0000000..4936a74
--- /dev/null
+++ b/testfile/ObjectCollectionFactoryTest/1/a
Binary files differ
diff --git a/testfile/ObjectCollectionFactoryTest/1/b b/testfile/ObjectCollectionFactoryTest/1/b
new file mode 100644
index 0000000..7c0897e
--- /dev/null
+++ b/testfile/ObjectCollectionFactoryTest/1/b
Binary files differ
diff --git a/testfile/ObjectCollectionFactoryTest/a b/testfile/ObjectCollectionFactoryTest/a
new file mode 100644
index 0000000..210ecec
--- /dev/null
+++ b/testfile/ObjectCollectionFactoryTest/a
Binary files differ
diff --git a/testfile/ObjectCollectionFactoryTest/b b/testfile/ObjectCollectionFactoryTest/b
new file mode 100644
index 0000000..344e596
--- /dev/null
+++ b/testfile/ObjectCollectionFactoryTest/b
Binary files differ
diff --git a/testfile/Sha256IdentifierTest b/testfile/Sha256IdentifierTest
new file mode 100644
index 0000000..5dd01c1
--- /dev/null
+++ b/testfile/Sha256IdentifierTest
@@ -0,0 +1 @@
+Hello, world! \ No newline at end of file