From ac7e70883c18602e7fd4b525b9e6fb9ea9620a6b Mon Sep 17 00:00:00 2001 From: Keuin Date: Wed, 20 Jan 2021 19:50:42 +0800 Subject: refactor --- .../java/com/keuin/kbackupfabric/KBCommands.java | 16 +- .../keuin/kbackupfabric/KBCommandsRegister.java | 2 +- .../com/keuin/kbackupfabric/KBPluginEvents.java | 4 +- .../kbackupfabric/backup/BackupFilesystemUtil.java | 92 ++++++++++ .../backup/BackupNameTimeFormatter.java | 32 ++++ .../backup/incremental/ObjectCollection.java | 65 +++++++ .../incremental/ObjectCollectionFactory.java | 55 ++++++ .../incremental/ObjectCollectionSerializer.java | 33 ++++ .../backup/incremental/ObjectElement.java | 60 +++++++ .../identifier/FileIdentifierProvider.java | 15 ++ .../incremental/identifier/ObjectIdentifier.java | 13 ++ .../incremental/identifier/Sha256Identifier.java | 88 ++++++++++ .../identifier/SingleHashIdentifier.java | 53 ++++++ .../identifier/StorageObjectLoader.java | 25 +++ .../manager/IncrementalBackupStorageManager.java | 188 +++++++++++++++++++++ .../backup/name/BackupFileNameEncoder.java | 64 +++++++ .../name/IncrementalBackupFileNameEncoder.java | 37 ++++ .../name/PrimitiveBackupFileNameEncoder.java | 33 ++++ .../backup/provider/AvailableBackupProvider.java | 8 + .../provider/IncrementalBackupInformation.java | 13 ++ .../provider/PrimitiveBackupInformation.java | 14 ++ .../suggestion/BackupNameSuggestionProvider.java | 77 +++++++++ .../kbackupfabric/exception/ZipUtilException.java | 4 +- .../kbackupfabric/operation/DeleteOperation.java | 4 +- .../backup/feedback/PrimitiveBackupFeedback.java | 2 +- .../method/ConfiguredIncrementalBackupMethod.java | 10 +- .../method/ConfiguredPrimitiveBackupMethod.java | 6 +- .../util/backup/BackupFilesystemUtil.java | 92 ---------- .../util/backup/BackupNameTimeFormatter.java | 32 ---- .../util/backup/incremental/ObjectCollection.java | 65 ------- .../incremental/ObjectCollectionFactory.java | 55 ------ .../incremental/ObjectCollectionSerializer.java | 33 ---- .../util/backup/incremental/ObjectElement.java | 60 ------- .../identifier/FileIdentifierProvider.java | 15 -- .../incremental/identifier/ObjectIdentifier.java | 13 -- .../incremental/identifier/Sha256Identifier.java | 88 ---------- .../identifier/SingleHashIdentifier.java | 53 ------ .../identifier/StorageObjectLoader.java | 25 --- .../manager/IncrementalBackupStorageManager.java | 188 --------------------- .../util/backup/name/BackupFileNameEncoder.java | 64 ------- .../name/IncrementalBackupFileNameEncoder.java | 37 ---- .../name/PrimitiveBackupFileNameEncoder.java | 33 ---- .../backup/provider/AvailableBackupProvider.java | 8 - .../provider/IncrementalBackupInformation.java | 13 -- .../provider/PrimitiveBackupInformation.java | 14 -- .../suggestion/BackupNameSuggestionProvider.java | 77 --------- .../incremental/ObjectCollectionFactoryTest.java | 80 +++++++++ .../ObjectCollectionSerializerTest.java | 29 ++++ .../identifier/Sha256IdentifierTest.java | 25 +++ .../name/IncrementalBackupFileNameEncoderTest.java | 38 +++++ .../name/PrimitiveBackupFileNameEncoderTest.java | 50 ++++++ .../incremental/ObjectCollectionFactoryTest.java | 80 --------- .../ObjectCollectionSerializerTest.java | 29 ---- .../identifier/Sha256IdentifierTest.java | 25 --- .../name/IncrementalBackupFileNameEncoderTest.java | 38 ----- .../name/PrimitiveBackupFileNameEncoderTest.java | 50 ------ 56 files changed, 1211 insertions(+), 1211 deletions(-) create mode 100644 src/main/java/com/keuin/kbackupfabric/backup/BackupFilesystemUtil.java create mode 100644 src/main/java/com/keuin/kbackupfabric/backup/BackupNameTimeFormatter.java create mode 100644 src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollection.java create mode 100644 src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionFactory.java create mode 100644 src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionSerializer.java create mode 100644 src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectElement.java create mode 100644 src/main/java/com/keuin/kbackupfabric/backup/incremental/identifier/FileIdentifierProvider.java create mode 100644 src/main/java/com/keuin/kbackupfabric/backup/incremental/identifier/ObjectIdentifier.java create mode 100644 src/main/java/com/keuin/kbackupfabric/backup/incremental/identifier/Sha256Identifier.java create mode 100644 src/main/java/com/keuin/kbackupfabric/backup/incremental/identifier/SingleHashIdentifier.java create mode 100644 src/main/java/com/keuin/kbackupfabric/backup/incremental/identifier/StorageObjectLoader.java create mode 100644 src/main/java/com/keuin/kbackupfabric/backup/incremental/manager/IncrementalBackupStorageManager.java create mode 100644 src/main/java/com/keuin/kbackupfabric/backup/name/BackupFileNameEncoder.java create mode 100644 src/main/java/com/keuin/kbackupfabric/backup/name/IncrementalBackupFileNameEncoder.java create mode 100644 src/main/java/com/keuin/kbackupfabric/backup/name/PrimitiveBackupFileNameEncoder.java create mode 100644 src/main/java/com/keuin/kbackupfabric/backup/provider/AvailableBackupProvider.java create mode 100644 src/main/java/com/keuin/kbackupfabric/backup/provider/IncrementalBackupInformation.java create mode 100644 src/main/java/com/keuin/kbackupfabric/backup/provider/PrimitiveBackupInformation.java create mode 100644 src/main/java/com/keuin/kbackupfabric/backup/suggestion/BackupNameSuggestionProvider.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/backup/BackupFilesystemUtil.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/backup/BackupNameTimeFormatter.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollection.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionFactory.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionSerializer.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectElement.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/FileIdentifierProvider.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/ObjectIdentifier.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/Sha256Identifier.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/SingleHashIdentifier.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/StorageObjectLoader.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/backup/incremental/manager/IncrementalBackupStorageManager.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/backup/name/BackupFileNameEncoder.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/backup/name/IncrementalBackupFileNameEncoder.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/backup/name/PrimitiveBackupFileNameEncoder.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/backup/provider/AvailableBackupProvider.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/backup/provider/IncrementalBackupInformation.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/backup/provider/PrimitiveBackupInformation.java delete mode 100644 src/main/java/com/keuin/kbackupfabric/util/backup/suggestion/BackupNameSuggestionProvider.java create mode 100644 src/test/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionFactoryTest.java create mode 100644 src/test/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionSerializerTest.java create mode 100644 src/test/java/com/keuin/kbackupfabric/backup/incremental/identifier/Sha256IdentifierTest.java create mode 100644 src/test/java/com/keuin/kbackupfabric/backup/name/IncrementalBackupFileNameEncoderTest.java create mode 100644 src/test/java/com/keuin/kbackupfabric/backup/name/PrimitiveBackupFileNameEncoderTest.java delete mode 100644 src/test/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionFactoryTest.java delete mode 100644 src/test/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionSerializerTest.java delete mode 100644 src/test/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/Sha256IdentifierTest.java delete mode 100644 src/test/java/com/keuin/kbackupfabric/util/backup/name/IncrementalBackupFileNameEncoderTest.java delete mode 100644 src/test/java/com/keuin/kbackupfabric/util/backup/name/PrimitiveBackupFileNameEncoderTest.java (limited to 'src') diff --git a/src/main/java/com/keuin/kbackupfabric/KBCommands.java b/src/main/java/com/keuin/kbackupfabric/KBCommands.java index 1b52b6b..90a4130 100644 --- a/src/main/java/com/keuin/kbackupfabric/KBCommands.java +++ b/src/main/java/com/keuin/kbackupfabric/KBCommands.java @@ -1,5 +1,9 @@ package com.keuin.kbackupfabric; +import com.keuin.kbackupfabric.backup.BackupFilesystemUtil; +import com.keuin.kbackupfabric.backup.name.IncrementalBackupFileNameEncoder; +import com.keuin.kbackupfabric.backup.name.PrimitiveBackupFileNameEncoder; +import com.keuin.kbackupfabric.backup.suggestion.BackupNameSuggestionProvider; import com.keuin.kbackupfabric.metadata.MetadataHolder; import com.keuin.kbackupfabric.operation.BackupOperation; import com.keuin.kbackupfabric.operation.DeleteOperation; @@ -9,10 +13,6 @@ 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.name.IncrementalBackupFileNameEncoder; -import com.keuin.kbackupfabric.util.backup.name.PrimitiveBackupFileNameEncoder; -import com.keuin.kbackupfabric.util.backup.suggestion.BackupNameSuggestionProvider; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; import net.minecraft.server.MinecraftServer; @@ -26,8 +26,8 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; +import static com.keuin.kbackupfabric.backup.BackupFilesystemUtil.*; import static com.keuin.kbackupfabric.util.PrintUtil.*; -import static com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil.*; public final class KBCommands { @@ -273,12 +273,12 @@ public final class KBCommands { ConfiguredBackupMethod method = !incremental ? new ConfiguredPrimitiveBackupMethod( new PrimitiveBackupFileNameEncoder().encode(customBackupName, LocalDateTime.now()), getLevelPath(server), - getBackupSaveDirectory(server).getAbsolutePath() + getBackupSaveDirectory(server).getCanonicalPath() ) : new ConfiguredIncrementalBackupMethod( new IncrementalBackupFileNameEncoder().encode(customBackupName, LocalDateTime.now()), getLevelPath(server), - getBackupSaveDirectory(server).getAbsolutePath(), - getIncrementalBackupBaseDirectory(server).getAbsolutePath() + getBackupSaveDirectory(server).getCanonicalPath(), + getIncrementalBackupBaseDirectory(server).getCanonicalPath() ); // dispatch to operation worker diff --git a/src/main/java/com/keuin/kbackupfabric/KBCommandsRegister.java b/src/main/java/com/keuin/kbackupfabric/KBCommandsRegister.java index d9ace66..b9baf44 100644 --- a/src/main/java/com/keuin/kbackupfabric/KBCommandsRegister.java +++ b/src/main/java/com/keuin/kbackupfabric/KBCommandsRegister.java @@ -1,7 +1,7 @@ package com.keuin.kbackupfabric; +import com.keuin.kbackupfabric.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; diff --git a/src/main/java/com/keuin/kbackupfabric/KBPluginEvents.java b/src/main/java/com/keuin/kbackupfabric/KBPluginEvents.java index 3cb2dbe..1d90864 100644 --- a/src/main/java/com/keuin/kbackupfabric/KBPluginEvents.java +++ b/src/main/java/com/keuin/kbackupfabric/KBPluginEvents.java @@ -1,9 +1,9 @@ package com.keuin.kbackupfabric; +import com.keuin.kbackupfabric.backup.BackupFilesystemUtil; +import com.keuin.kbackupfabric.backup.suggestion.BackupNameSuggestionProvider; import com.keuin.kbackupfabric.metadata.BackupMetadata; import com.keuin.kbackupfabric.metadata.MetadataHolder; -import com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil; -import com.keuin.kbackupfabric.util.backup.suggestion.BackupNameSuggestionProvider; import com.keuin.kbackupfabric.util.PrintUtil; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.event.server.ServerStartCallback; diff --git a/src/main/java/com/keuin/kbackupfabric/backup/BackupFilesystemUtil.java b/src/main/java/com/keuin/kbackupfabric/backup/BackupFilesystemUtil.java new file mode 100644 index 0000000..49fe6a0 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/backup/BackupFilesystemUtil.java @@ -0,0 +1,92 @@ +package com.keuin.kbackupfabric.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; + +/** + * Functions deal with file name, directory name about Minecraft saves. + */ +public final class BackupFilesystemUtil { + + 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 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 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(), 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) 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 { + File saveDir; + ThreadedAnvilChunkStorage threadedAnvilChunkStorage = (ThreadedAnvilChunkStorage) ReflectionUtils.getPrivateField(world.getChunkManager(), "threadedAnvilChunkStorage"); + saveDir = (File) ReflectionUtils.getPrivateField(threadedAnvilChunkStorage, "saveDir"); + return saveDir.getName(); + } + + @Deprecated + public static long getBackupTimeFromBackupFileName(String backupFileName) { + Matcher matcher = Pattern.compile("[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}").matcher(backupFileName); + if (matcher.find()) { + String timeString = matcher.group(0); + long timeStamp = BackupNameTimeFormatter.timeStringToEpochSeconds(timeString); + System.out.println(backupFileName + " -> " + timeStamp); + return timeStamp; + } else { + System.err.println("Failed to extract time from " + backupFileName); + } + return -1; + } + + public static String 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); + else + //msgInfo(context, String.format("File size: %.2fMB", fileSize)); + return String.format("%.2fMB", fileSize); + } +} diff --git a/src/main/java/com/keuin/kbackupfabric/backup/BackupNameTimeFormatter.java b/src/main/java/com/keuin/kbackupfabric/backup/BackupNameTimeFormatter.java new file mode 100644 index 0000000..bcf5d18 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/backup/BackupNameTimeFormatter.java @@ -0,0 +1,32 @@ +package com.keuin.kbackupfabric.backup; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + +public class BackupNameTimeFormatter { + private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"); + + @Deprecated + public static String getTimeString() { + return LocalDateTime.now().format(formatter); + } + + public static String localDateTimeToString(LocalDateTime localDateTime) { + return localDateTime.format(formatter); + } + + @Deprecated + public static long timeStringToEpochSeconds(String timeString) { + ZoneId systemZone = ZoneId.systemDefault(); // my timezone + LocalDateTime localDateTime = LocalDateTime.parse(timeString, formatter); + ZoneOffset currentOffsetForMyZone = systemZone.getRules().getOffset(localDateTime); + return localDateTime.toEpochSecond(currentOffsetForMyZone); + } + + public static LocalDateTime timeStringToLocalDateTime(String timeString) { + return LocalDateTime.parse(timeString,formatter); + } + +} diff --git a/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollection.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollection.java new file mode 100644 index 0000000..2d07fb4 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollection.java @@ -0,0 +1,65 @@ +package com.keuin.kbackupfabric.backup.incremental; + +import java.io.Serializable; +import java.util.*; + +public class ObjectCollection implements Serializable { + private final String name; + private final Map elements; + private final Map subCollections; + + ObjectCollection(String name, Set elements, Map 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 getElementSet() { + return new HashSet<>(elements.values()); + } + + public Map getElementMap() { + return Collections.unmodifiableMap(elements); + } + + public ObjectElement getElement(String name) { + return elements.get(name); + } + + public Set getSubCollectionSet() { + return new HashSet<>(subCollections.values()); + } + + public Map 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/backup/incremental/ObjectCollectionFactory.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionFactory.java new file mode 100644 index 0000000..0e02606 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionFactory.java @@ -0,0 +1,55 @@ +package com.keuin.kbackupfabric.backup.incremental; + +import com.keuin.kbackupfabric.backup.incremental.identifier.FileIdentifierProvider; +import com.keuin.kbackupfabric.backup.incremental.identifier.ObjectIdentifier; +import com.keuin.kbackupfabric.util.PrintUtil; + +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 { + + private final FileIdentifierProvider identifierFactory; + + public ObjectCollectionFactory(FileIdentifierProvider identifierFactory) { + this.identifierFactory = identifierFactory; + } + + public ObjectCollection fromDirectory(File directory, Set ignoredFiles) throws IOException { + final Set subFiles = new HashSet<>(); + final Map subCollections = new HashMap<>(); + + if (!Objects.requireNonNull(directory).isDirectory()) + throw new IllegalArgumentException("given file is not a directory"); + + for (Iterator 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/backup/incremental/ObjectCollectionSerializer.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionSerializer.java new file mode 100644 index 0000000..f45d4d0 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionSerializer.java @@ -0,0 +1,33 @@ +package com.keuin.kbackupfabric.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/backup/incremental/ObjectElement.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectElement.java new file mode 100644 index 0000000..0bb7873 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectElement.java @@ -0,0 +1,60 @@ +package com.keuin.kbackupfabric.backup.incremental; + +import com.keuin.kbackupfabric.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/backup/incremental/identifier/FileIdentifierProvider.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/identifier/FileIdentifierProvider.java new file mode 100644 index 0000000..e1039d6 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/identifier/FileIdentifierProvider.java @@ -0,0 +1,15 @@ +package com.keuin.kbackupfabric.backup.incremental.identifier; + +import java.io.File; +import java.io.IOException; + +public interface FileIdentifierProvider { + /** + * 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/backup/incremental/identifier/ObjectIdentifier.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/identifier/ObjectIdentifier.java new file mode 100644 index 0000000..07cd390 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/identifier/ObjectIdentifier.java @@ -0,0 +1,13 @@ +package com.keuin.kbackupfabric.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/backup/incremental/identifier/Sha256Identifier.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/identifier/Sha256Identifier.java new file mode 100644 index 0000000..50e6aa4 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/identifier/Sha256Identifier.java @@ -0,0 +1,88 @@ +package com.keuin.kbackupfabric.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 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 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/backup/incremental/identifier/SingleHashIdentifier.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/identifier/SingleHashIdentifier.java new file mode 100644 index 0000000..9fd61c8 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/identifier/SingleHashIdentifier.java @@ -0,0 +1,53 @@ +package com.keuin.kbackupfabric.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/backup/incremental/identifier/StorageObjectLoader.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/identifier/StorageObjectLoader.java new file mode 100644 index 0000000..55dd6bd --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/identifier/StorageObjectLoader.java @@ -0,0 +1,25 @@ +package com.keuin.kbackupfabric.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/backup/incremental/manager/IncrementalBackupStorageManager.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/manager/IncrementalBackupStorageManager.java new file mode 100644 index 0000000..1984362 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/manager/IncrementalBackupStorageManager.java @@ -0,0 +1,188 @@ +package com.keuin.kbackupfabric.backup.incremental.manager; + +import com.keuin.kbackupfabric.backup.incremental.ObjectCollection; +import com.keuin.kbackupfabric.backup.incremental.ObjectElement; +import com.keuin.kbackupfabric.backup.incremental.identifier.ObjectIdentifier; +import com.keuin.kbackupfabric.backup.incremental.identifier.StorageObjectLoader; +import com.keuin.kbackupfabric.util.PrintUtil; + +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 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 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 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 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 entry : collection.getSubCollectionMap().entrySet()) { + File newBase = new File(collectionBasePath, entry.getKey()); + copyCount += restoreObjectCollection(entry.getValue(), newBase); + } + + return copyCount; + } + + public int cleanUnusedObjects(Iterable collectionIterable) { + // construct object list in memory + Set 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 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/backup/name/BackupFileNameEncoder.java b/src/main/java/com/keuin/kbackupfabric/backup/name/BackupFileNameEncoder.java new file mode 100644 index 0000000..8ad91f7 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/backup/name/BackupFileNameEncoder.java @@ -0,0 +1,64 @@ +package com.keuin.kbackupfabric.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/backup/name/IncrementalBackupFileNameEncoder.java b/src/main/java/com/keuin/kbackupfabric/backup/name/IncrementalBackupFileNameEncoder.java new file mode 100644 index 0000000..8910d7f --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/backup/name/IncrementalBackupFileNameEncoder.java @@ -0,0 +1,37 @@ +package com.keuin.kbackupfabric.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/backup/name/PrimitiveBackupFileNameEncoder.java b/src/main/java/com/keuin/kbackupfabric/backup/name/PrimitiveBackupFileNameEncoder.java new file mode 100644 index 0000000..c8e73f9 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/backup/name/PrimitiveBackupFileNameEncoder.java @@ -0,0 +1,33 @@ +package com.keuin.kbackupfabric.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/backup/provider/AvailableBackupProvider.java b/src/main/java/com/keuin/kbackupfabric/backup/provider/AvailableBackupProvider.java new file mode 100644 index 0000000..43a7a01 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/backup/provider/AvailableBackupProvider.java @@ -0,0 +1,8 @@ +package com.keuin.kbackupfabric.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/backup/provider/IncrementalBackupInformation.java b/src/main/java/com/keuin/kbackupfabric/backup/provider/IncrementalBackupInformation.java new file mode 100644 index 0000000..4c4f847 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/backup/provider/IncrementalBackupInformation.java @@ -0,0 +1,13 @@ +package com.keuin.kbackupfabric.backup.provider; + +import com.keuin.kbackupfabric.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/backup/provider/PrimitiveBackupInformation.java b/src/main/java/com/keuin/kbackupfabric/backup/provider/PrimitiveBackupInformation.java new file mode 100644 index 0000000..53f049c --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/backup/provider/PrimitiveBackupInformation.java @@ -0,0 +1,14 @@ +package com.keuin.kbackupfabric.backup.provider; + +import com.keuin.kbackupfabric.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/backup/suggestion/BackupNameSuggestionProvider.java b/src/main/java/com/keuin/kbackupfabric/backup/suggestion/BackupNameSuggestionProvider.java new file mode 100644 index 0000000..9eb6981 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/backup/suggestion/BackupNameSuggestionProvider.java @@ -0,0 +1,77 @@ +package com.keuin.kbackupfabric.backup.suggestion; + +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.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CompletableFuture; + +public class BackupNameSuggestionProvider { + + private static final List candidateCacheList = new ArrayList<>(); + private static final Object syncSetDirectory = new Object(); + private static final Object syncCache = new Object(); + private static final long CACHE_TTL = 8000; + private static String backupSaveDirectory; + private static long cacheUpdateTime = 0; + + public static void setBackupSaveDirectory(String backupSaveDirectory) { + synchronized (syncSetDirectory) { + BackupNameSuggestionProvider.backupSaveDirectory = backupSaveDirectory; + } + // Immediately perform an update + updateCandidateList(); + } + + public static void updateCandidateList() { + synchronized (syncCache) { + try { + File file = new File(backupSaveDirectory); + candidateCacheList.clear(); + File[] files = file.listFiles(); + if (files == null) + return; + for (File f : files) + candidateCacheList.add(f.getName()); + cacheUpdateTime = System.currentTimeMillis(); + } catch (NullPointerException ignored) { + } + } + } + +// private static void updateCandidateList(Collection stringCollection) { +// candidateList.clear(); +// candidateList.addAll(stringCollection); +// } + + public static SuggestionProvider getProvider() { + return (context, builder) -> getCompletableFuture(builder); + } + + private static CompletableFuture getCompletableFuture(SuggestionsBuilder builder) { + if (isCacheExpired()) + updateCandidateList(); + String remaining = builder.getRemaining().toLowerCase(Locale.ROOT); + synchronized (syncCache) { + if (candidateCacheList.isEmpty()) { // If the list is empty then return no suggestions + return Suggestions.empty(); // No suggestions + } + + for (String string : candidateCacheList) { // 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 + } + + private static boolean isCacheExpired() { + return System.currentTimeMillis() - cacheUpdateTime > CACHE_TTL || cacheUpdateTime == 0; + } +} diff --git a/src/main/java/com/keuin/kbackupfabric/exception/ZipUtilException.java b/src/main/java/com/keuin/kbackupfabric/exception/ZipUtilException.java index 88b1d18..24301f0 100644 --- a/src/main/java/com/keuin/kbackupfabric/exception/ZipUtilException.java +++ b/src/main/java/com/keuin/kbackupfabric/exception/ZipUtilException.java @@ -1,7 +1,7 @@ package com.keuin.kbackupfabric.exception; public class ZipUtilException extends Exception { - public ZipUtilException(String string) { - super(string); + public ZipUtilException(String message) { + super(message); } } diff --git a/src/main/java/com/keuin/kbackupfabric/operation/DeleteOperation.java b/src/main/java/com/keuin/kbackupfabric/operation/DeleteOperation.java index 3ae09f5..7ebff0a 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.backup.suggestion.BackupNameSuggestionProvider; import com.keuin.kbackupfabric.operation.abstracts.InvokableAsyncBlockingOperation; 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,9 +10,9 @@ import net.minecraft.server.command.ServerCommandSource; import java.io.File; import java.io.IOException; +import static com.keuin.kbackupfabric.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 { 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 index 3bcd012..e8844a2 100644 --- a/src/main/java/com/keuin/kbackupfabric/operation/backup/feedback/PrimitiveBackupFeedback.java +++ b/src/main/java/com/keuin/kbackupfabric/operation/backup/feedback/PrimitiveBackupFeedback.java @@ -1,6 +1,6 @@ package com.keuin.kbackupfabric.operation.backup.feedback; -import static com.keuin.kbackupfabric.util.backup.BackupFilesystemUtil.getFriendlyFileSizeString; +import static com.keuin.kbackupfabric.backup.BackupFilesystemUtil.getFriendlyFileSizeString; public class PrimitiveBackupFeedback implements BackupFeedback { private final boolean success; 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 index b5d2463..60b825e 100644 --- a/src/main/java/com/keuin/kbackupfabric/operation/backup/method/ConfiguredIncrementalBackupMethod.java +++ b/src/main/java/com/keuin/kbackupfabric/operation/backup/method/ConfiguredIncrementalBackupMethod.java @@ -1,13 +1,13 @@ package com.keuin.kbackupfabric.operation.backup.method; +import com.keuin.kbackupfabric.backup.incremental.ObjectCollection; +import com.keuin.kbackupfabric.backup.incremental.ObjectCollectionFactory; +import com.keuin.kbackupfabric.backup.incremental.ObjectCollectionSerializer; +import com.keuin.kbackupfabric.backup.incremental.identifier.Sha256Identifier; +import com.keuin.kbackupfabric.backup.incremental.manager.IncrementalBackupStorageManager; 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; 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 index 1c3c9f6..87a3043 100644 --- a/src/main/java/com/keuin/kbackupfabric/operation/backup/method/ConfiguredPrimitiveBackupMethod.java +++ b/src/main/java/com/keuin/kbackupfabric/operation/backup/method/ConfiguredPrimitiveBackupMethod.java @@ -1,14 +1,14 @@ package com.keuin.kbackupfabric.operation.backup.method; +import com.keuin.kbackupfabric.backup.BackupFilesystemUtil; +import com.keuin.kbackupfabric.backup.BackupNameTimeFormatter; +import com.keuin.kbackupfabric.backup.name.PrimitiveBackupFileNameEncoder; 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; diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/BackupFilesystemUtil.java b/src/main/java/com/keuin/kbackupfabric/util/backup/BackupFilesystemUtil.java deleted file mode 100644 index 28ede70..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/BackupFilesystemUtil.java +++ /dev/null @@ -1,92 +0,0 @@ -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; - -/** - * Functions deal with file name, directory name about Minecraft saves. - */ -public final class BackupFilesystemUtil { - - 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 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 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(), 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) 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 { - File saveDir; - ThreadedAnvilChunkStorage threadedAnvilChunkStorage = (ThreadedAnvilChunkStorage) ReflectionUtils.getPrivateField(world.getChunkManager(), "threadedAnvilChunkStorage"); - saveDir = (File) ReflectionUtils.getPrivateField(threadedAnvilChunkStorage, "saveDir"); - return saveDir.getName(); - } - - @Deprecated - public static long getBackupTimeFromBackupFileName(String backupFileName) { - Matcher matcher = Pattern.compile("[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}").matcher(backupFileName); - if (matcher.find()) { - String timeString = matcher.group(0); - long timeStamp = BackupNameTimeFormatter.timeStringToEpochSeconds(timeString); - System.out.println(backupFileName + " -> " + timeStamp); - return timeStamp; - } else { - System.err.println("Failed to extract time from " + backupFileName); - } - return -1; - } - - public static String 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); - else - //msgInfo(context, String.format("File size: %.2fMB", fileSize)); - return String.format("%.2fMB", fileSize); - } -} diff --git a/src/main/java/com/keuin/kbackupfabric/util/backup/BackupNameTimeFormatter.java b/src/main/java/com/keuin/kbackupfabric/util/backup/BackupNameTimeFormatter.java deleted file mode 100644 index 3811ae8..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/BackupNameTimeFormatter.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.keuin.kbackupfabric.util.backup; - -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; - -public class BackupNameTimeFormatter { - private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"); - - @Deprecated - public static String getTimeString() { - return LocalDateTime.now().format(formatter); - } - - public static String localDateTimeToString(LocalDateTime localDateTime) { - return localDateTime.format(formatter); - } - - @Deprecated - public static long timeStringToEpochSeconds(String timeString) { - ZoneId systemZone = ZoneId.systemDefault(); // my timezone - LocalDateTime localDateTime = LocalDateTime.parse(timeString, formatter); - ZoneOffset currentOffsetForMyZone = systemZone.getRules().getOffset(localDateTime); - return localDateTime.toEpochSecond(currentOffsetForMyZone); - } - - public static LocalDateTime timeStringToLocalDateTime(String timeString) { - return LocalDateTime.parse(timeString,formatter); - } - -} 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 deleted file mode 100644 index 16d95e6..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollection.java +++ /dev/null @@ -1,65 +0,0 @@ -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 elements; - private final Map subCollections; - - ObjectCollection(String name, Set elements, Map 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 getElementSet() { - return new HashSet<>(elements.values()); - } - - public Map getElementMap() { - return Collections.unmodifiableMap(elements); - } - - public ObjectElement getElement(String name) { - return elements.get(name); - } - - public Set getSubCollectionSet() { - return new HashSet<>(subCollections.values()); - } - - public Map 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 deleted file mode 100644 index 2f3761c..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionFactory.java +++ /dev/null @@ -1,55 +0,0 @@ -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 { - - private final FileIdentifierProvider identifierFactory; - - public ObjectCollectionFactory(FileIdentifierProvider identifierFactory) { - this.identifierFactory = identifierFactory; - } - - public ObjectCollection fromDirectory(File directory, Set ignoredFiles) throws IOException { - final Set subFiles = new HashSet<>(); - final Map subCollections = new HashMap<>(); - - if (!Objects.requireNonNull(directory).isDirectory()) - throw new IllegalArgumentException("given file is not a directory"); - - for (Iterator 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 deleted file mode 100644 index 6f9b792..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionSerializer.java +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index cbb886e..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectElement.java +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index 3fbe284..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/FileIdentifierProvider.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.keuin.kbackupfabric.util.backup.incremental.identifier; - -import java.io.File; -import java.io.IOException; - -public interface FileIdentifierProvider { - /** - * 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 deleted file mode 100644 index aece07d..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/ObjectIdentifier.java +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index c1c87e1..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/Sha256Identifier.java +++ /dev/null @@ -1,88 +0,0 @@ -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 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 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 deleted file mode 100644 index 0f62f2b..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/SingleHashIdentifier.java +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index 96bc295..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/StorageObjectLoader.java +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 6fd339b..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/incremental/manager/IncrementalBackupStorageManager.java +++ /dev/null @@ -1,188 +0,0 @@ -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 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 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 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 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 entry : collection.getSubCollectionMap().entrySet()) { - File newBase = new File(collectionBasePath, entry.getKey()); - copyCount += restoreObjectCollection(entry.getValue(), newBase); - } - - return copyCount; - } - - public int cleanUnusedObjects(Iterable collectionIterable) { - // construct object list in memory - Set 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 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 deleted file mode 100644 index 8ebc7ff..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/name/BackupFileNameEncoder.java +++ /dev/null @@ -1,64 +0,0 @@ -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 deleted file mode 100644 index 926f47c..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/name/IncrementalBackupFileNameEncoder.java +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index ef15ae7..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/name/PrimitiveBackupFileNameEncoder.java +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index caa0e84..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/provider/AvailableBackupProvider.java +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 861d210..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/provider/IncrementalBackupInformation.java +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index d3d2db8..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/provider/PrimitiveBackupInformation.java +++ /dev/null @@ -1,14 +0,0 @@ -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/BackupNameSuggestionProvider.java b/src/main/java/com/keuin/kbackupfabric/util/backup/suggestion/BackupNameSuggestionProvider.java deleted file mode 100644 index 01152c2..0000000 --- a/src/main/java/com/keuin/kbackupfabric/util/backup/suggestion/BackupNameSuggestionProvider.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.keuin.kbackupfabric.util.backup.suggestion; - -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.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.CompletableFuture; - -public class BackupNameSuggestionProvider { - - private static final List candidateCacheList = new ArrayList<>(); - private static final Object syncSetDirectory = new Object(); - private static final Object syncCache = new Object(); - private static final long CACHE_TTL = 8000; - private static String backupSaveDirectory; - private static long cacheUpdateTime = 0; - - public static void setBackupSaveDirectory(String backupSaveDirectory) { - synchronized (syncSetDirectory) { - BackupNameSuggestionProvider.backupSaveDirectory = backupSaveDirectory; - } - // Immediately perform an update - updateCandidateList(); - } - - public static void updateCandidateList() { - synchronized (syncCache) { - try { - File file = new File(backupSaveDirectory); - candidateCacheList.clear(); - File[] files = file.listFiles(); - if (files == null) - return; - for (File f : files) - candidateCacheList.add(f.getName()); - cacheUpdateTime = System.currentTimeMillis(); - } catch (NullPointerException ignored) { - } - } - } - -// private static void updateCandidateList(Collection stringCollection) { -// candidateList.clear(); -// candidateList.addAll(stringCollection); -// } - - public static SuggestionProvider getProvider() { - return (context, builder) -> getCompletableFuture(builder); - } - - private static CompletableFuture getCompletableFuture(SuggestionsBuilder builder) { - if (isCacheExpired()) - updateCandidateList(); - String remaining = builder.getRemaining().toLowerCase(Locale.ROOT); - synchronized (syncCache) { - if (candidateCacheList.isEmpty()) { // If the list is empty then return no suggestions - return Suggestions.empty(); // No suggestions - } - - for (String string : candidateCacheList) { // 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 - } - - private static boolean isCacheExpired() { - return System.currentTimeMillis() - cacheUpdateTime > CACHE_TTL || cacheUpdateTime == 0; - } -} diff --git a/src/test/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionFactoryTest.java b/src/test/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionFactoryTest.java new file mode 100644 index 0000000..9a2a73c --- /dev/null +++ b/src/test/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionFactoryTest.java @@ -0,0 +1,80 @@ +package com.keuin.kbackupfabric.backup.incremental; + +import com.keuin.kbackupfabric.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 subCollections, Map subElements) { + assertEquals(subCollections.size(), collection.getSubCollectionMap().size()); + assertEquals(subElements.size(), collection.getElementSet().size()); + for (Map.Entry c : collection.getSubCollectionMap().entrySet()) { + assertEquals(c.getKey(), c.getValue().getName()); + assertTrue(subCollections.contains(c.getKey())); + } + for (Map.Entry 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 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 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/backup/incremental/ObjectCollectionSerializerTest.java b/src/test/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionSerializerTest.java new file mode 100644 index 0000000..bd25215 --- /dev/null +++ b/src/test/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionSerializerTest.java @@ -0,0 +1,29 @@ +package com.keuin.kbackupfabric.backup.incremental; + +import com.keuin.kbackupfabric.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 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/backup/incremental/identifier/Sha256IdentifierTest.java b/src/test/java/com/keuin/kbackupfabric/backup/incremental/identifier/Sha256IdentifierTest.java new file mode 100644 index 0000000..9cd1762 --- /dev/null +++ b/src/test/java/com/keuin/kbackupfabric/backup/incremental/identifier/Sha256IdentifierTest.java @@ -0,0 +1,25 @@ +package com.keuin.kbackupfabric.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/backup/name/IncrementalBackupFileNameEncoderTest.java b/src/test/java/com/keuin/kbackupfabric/backup/name/IncrementalBackupFileNameEncoderTest.java new file mode 100644 index 0000000..f9695d5 --- /dev/null +++ b/src/test/java/com/keuin/kbackupfabric/backup/name/IncrementalBackupFileNameEncoderTest.java @@ -0,0 +1,38 @@ +package com.keuin.kbackupfabric.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/backup/name/PrimitiveBackupFileNameEncoderTest.java b/src/test/java/com/keuin/kbackupfabric/backup/name/PrimitiveBackupFileNameEncoderTest.java new file mode 100644 index 0000000..bb5f64b --- /dev/null +++ b/src/test/java/com/keuin/kbackupfabric/backup/name/PrimitiveBackupFileNameEncoderTest.java @@ -0,0 +1,50 @@ +package com.keuin.kbackupfabric.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/src/test/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionFactoryTest.java b/src/test/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionFactoryTest.java deleted file mode 100644 index 3f722a4..0000000 --- a/src/test/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionFactoryTest.java +++ /dev/null @@ -1,80 +0,0 @@ -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 subCollections, Map subElements) { - assertEquals(subCollections.size(), collection.getSubCollectionMap().size()); - assertEquals(subElements.size(), collection.getElementSet().size()); - for (Map.Entry c : collection.getSubCollectionMap().entrySet()) { - assertEquals(c.getKey(), c.getValue().getName()); - assertTrue(subCollections.contains(c.getKey())); - } - for (Map.Entry 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 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 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 deleted file mode 100644 index 0edfe01..0000000 --- a/src/test/java/com/keuin/kbackupfabric/util/backup/incremental/ObjectCollectionSerializerTest.java +++ /dev/null @@ -1,29 +0,0 @@ -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 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 deleted file mode 100644 index f799a95..0000000 --- a/src/test/java/com/keuin/kbackupfabric/util/backup/incremental/identifier/Sha256IdentifierTest.java +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index e5fedd7..0000000 --- a/src/test/java/com/keuin/kbackupfabric/util/backup/name/IncrementalBackupFileNameEncoderTest.java +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index abc19d5..0000000 --- a/src/test/java/com/keuin/kbackupfabric/util/backup/name/PrimitiveBackupFileNameEncoderTest.java +++ /dev/null @@ -1,50 +0,0 @@ -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 -- cgit v1.2.3