From 7e3cd92742383c43f2741449c551208e6487154e Mon Sep 17 00:00:00 2001 From: Keuin Date: Sat, 20 Jan 2024 13:20:41 +0800 Subject: feature: configurable CoW incremental backup --- .../keuin/kbackupfabric/KBCommandsRegister.java | 4 ++ .../com/keuin/kbackupfabric/KBPluginEvents.java | 11 ++++- .../manager/IncrementalBackupStorageManager.java | 26 ++++++++-- .../keuin/kbackupfabric/config/KBackupConfig.java | 56 ++++++++++++++++++++++ .../method/ConfiguredIncrementalBackupMethod.java | 1 + .../com/keuin/kbackupfabric/ui/KBCommands.java | 11 +++++ .../keuin/kbackupfabric/util/cow/FileCopier.java | 9 ++++ .../kbackupfabric/util/cow/FileCowCopier.java | 44 +++++++++++++++++ .../kbackupfabric/util/cow/FileEagerCopier.java | 17 +++++++ 9 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/keuin/kbackupfabric/config/KBackupConfig.java create mode 100644 src/main/java/com/keuin/kbackupfabric/util/cow/FileCopier.java create mode 100644 src/main/java/com/keuin/kbackupfabric/util/cow/FileCowCopier.java create mode 100644 src/main/java/com/keuin/kbackupfabric/util/cow/FileEagerCopier.java (limited to 'src/main/java/com/keuin/kbackupfabric') diff --git a/src/main/java/com/keuin/kbackupfabric/KBCommandsRegister.java b/src/main/java/com/keuin/kbackupfabric/KBCommandsRegister.java index 5bf92e4..7e454d2 100644 --- a/src/main/java/com/keuin/kbackupfabric/KBCommandsRegister.java +++ b/src/main/java/com/keuin/kbackupfabric/KBCommandsRegister.java @@ -58,6 +58,10 @@ public final class KBCommandsRegister { .requires(Permissions.require("kb.list", DEFAULT_REQUIRED_LEVEL)) .executes(KBCommands::list))); + dispatcher.register(CommandManager.literal("kb") + .then(CommandManager.literal("cow-info") + .executes(KBCommands::cowInfo))); + // register /kb delete [name] for deleting an existing backup. OP is required. dispatcher.register(CommandManager.literal("kb") .then(CommandManager.literal("delete") diff --git a/src/main/java/com/keuin/kbackupfabric/KBPluginEvents.java b/src/main/java/com/keuin/kbackupfabric/KBPluginEvents.java index 362abb8..20e9058 100644 --- a/src/main/java/com/keuin/kbackupfabric/KBPluginEvents.java +++ b/src/main/java/com/keuin/kbackupfabric/KBPluginEvents.java @@ -2,6 +2,7 @@ package com.keuin.kbackupfabric; import com.keuin.kbackupfabric.backup.BackupFilesystemUtil; import com.keuin.kbackupfabric.backup.suggestion.BackupNameSuggestionProvider; +import com.keuin.kbackupfabric.config.KBackupConfig; import com.keuin.kbackupfabric.event.OnPlayerConnect; import com.keuin.kbackupfabric.metadata.BackupMetadata; import com.keuin.kbackupfabric.metadata.MetadataHolder; @@ -32,7 +33,15 @@ public final class KBPluginEvents implements ModInitializer { @Override public void onInitialize() { - System.out.println("Binding events and commands ..."); + PrintUtil.info("Reading config..."); + try { + KBackupConfig.load(); + } catch (IOException e) { + PrintUtil.error("Failed to read config file, using default values: " + e + e.getMessage()); + } + boolean incCow = KBackupConfig.getInstance().getIncbakCow(); + PrintUtil.info("Incremental backup CoW: " + (incCow ? "enabled" : "disabled")); + PrintUtil.info("Binding events and commands..."); CommandRegistrationCallback.EVENT.register(KBCommandsRegister::registerCommands); OnPlayerConnect.ON_PLAYER_CONNECT.register((connection, player) -> NotificationManager.INSTANCE.notifyPlayer(DistinctNotifiable.fromServerPlayerEntity(player))); 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 index 7870620..53143f8 100644 --- a/src/main/java/com/keuin/kbackupfabric/backup/incremental/manager/IncrementalBackupStorageManager.java +++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/manager/IncrementalBackupStorageManager.java @@ -4,15 +4,18 @@ import com.keuin.kbackupfabric.backup.incremental.ObjectCollection2; import com.keuin.kbackupfabric.backup.incremental.ObjectCollectionIterator; import com.keuin.kbackupfabric.backup.incremental.ObjectElement; import com.keuin.kbackupfabric.backup.incremental.identifier.ObjectIdentifier; +import com.keuin.kbackupfabric.config.KBackupConfig; import com.keuin.kbackupfabric.util.FilesystemUtil; import com.keuin.kbackupfabric.util.PrintUtil; +import com.keuin.kbackupfabric.util.cow.FileCopier; +import com.keuin.kbackupfabric.util.cow.FileCowCopier; +import com.keuin.kbackupfabric.util.cow.FileEagerCopier; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.logging.Logger; @@ -27,9 +30,26 @@ public class IncrementalBackupStorageManager { private final Logger logger = Logger.getLogger(IncrementalBackupStorageManager.class.getName()); private final Path backupStorageBase; private final Logger LOGGER = Logger.getLogger(IncrementalBackupStorageManager.class.getName()); + private FileCopier copier; public IncrementalBackupStorageManager(Path backupStorageBase) { this.backupStorageBase = backupStorageBase; + if (KBackupConfig.getInstance().getIncbakCow()) { + // try to use cow copier, if failed, fallback to normal copier + try { + this.copier = FileCowCopier.getInstance(); + } catch (Exception | UnsatisfiedLinkError ex) { + PrintUtil.error("Failed to initialize kbackup-cow: " + ex + ex.getMessage()); + this.copier = new FileEagerCopier(); + } + } else { + this.copier = new FileEagerCopier(); + } + if (this.copier.isCow()) { + PrintUtil.info("Copy-on-write is enabled"); + } else { + PrintUtil.info("Copy-on-write is disabled"); + } } /** @@ -85,7 +105,7 @@ public class IncrementalBackupStorageManager { if (!contains(entry.getValue())) { // element does not exist. copy. logger.fine("Copy new file `" + copySourceFile.getName() + "`."); - Files.copy(copySourceFile.toPath(), copyDestination.toPath()); + copier.copy(copyDestination.getAbsolutePath(), copySourceFile.getAbsolutePath()); copyCount = copyCount.addWith(new IncCopyResult(1, 1, fileBytes, fileBytes)); } else { // element exists (file reused). Just update the stat info @@ -198,7 +218,7 @@ public class IncrementalBackupStorageManager { } } - Files.copy(copySource.toPath(), copyTarget.toPath()); + copier.copy(copyTarget.getAbsolutePath(), copySource.getAbsolutePath()); ++copyCount; } diff --git a/src/main/java/com/keuin/kbackupfabric/config/KBackupConfig.java b/src/main/java/com/keuin/kbackupfabric/config/KBackupConfig.java new file mode 100644 index 0000000..8ece8d2 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/config/KBackupConfig.java @@ -0,0 +1,56 @@ +package com.keuin.kbackupfabric.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.keuin.kbackupfabric.util.PrintUtil; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +public class KBackupConfig { + + private static KBackupConfig instance = getDefault(); + private static final String CONFIG_FILE = "kbackup.json"; + + @JsonProperty("incbak_cow") + private Boolean incbakCow; + + public static KBackupConfig getInstance() { + return instance; + } + + private static KBackupConfig getDefault() { + return new KBackupConfig(false); + } + + public static void load() throws IOException { + File file = new File(CONFIG_FILE); + ObjectMapper om = new ObjectMapper(); + try { + instance = om.readValue(file, KBackupConfig.class); + } catch (FileNotFoundException ignored) { + // generate default config file + PrintUtil.info("Config file does not exist. Creating default config: " + CONFIG_FILE); + instance = getDefault(); + ObjectWriter w = om.writerWithDefaultPrettyPrinter(); + w.writeValue(file, instance); + } + } + + public KBackupConfig() { + } + + public KBackupConfig(Boolean incbakCow) { + this.incbakCow = incbakCow; + } + + public Boolean getIncbakCow() { + return this.incbakCow; + } + + public void setIncbakCow(Boolean incbakCow) { + this.incbakCow = incbakCow; + } +} \ No newline at end of file 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 cfc9b81..3d9007d 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 @@ -99,6 +99,7 @@ public class ConfiguredIncrementalBackupMethod implements ConfiguredBackupMethod PrintUtil.info("Incremental backup finished."); feedback = new IncrementalBackupFeedback(true, copyResult); } catch (IOException e) { + PrintUtil.error("Incremental backup failed: " + e + e.getMessage()); feedback = new IncrementalBackupFeedback(e); } diff --git a/src/main/java/com/keuin/kbackupfabric/ui/KBCommands.java b/src/main/java/com/keuin/kbackupfabric/ui/KBCommands.java index d23a6fd..0dff99a 100644 --- a/src/main/java/com/keuin/kbackupfabric/ui/KBCommands.java +++ b/src/main/java/com/keuin/kbackupfabric/ui/KBCommands.java @@ -14,6 +14,7 @@ import com.keuin.kbackupfabric.operation.backup.method.ConfiguredIncrementalBack import com.keuin.kbackupfabric.operation.backup.method.ConfiguredPrimitiveBackupMethod; import com.keuin.kbackupfabric.util.DateUtil; import com.keuin.kbackupfabric.util.PrintUtil; +import com.keuin.kbackupfabric.util.cow.FileCowCopier; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; import net.minecraft.server.MinecraftServer; @@ -75,6 +76,7 @@ public final class KBCommands { msgInfo(context, "/kb restore - Delete the whole current level and restore from given backup. /kb restore is identical with /kb list."); msgInfo(context, "/kb confirm - Confirm and start restoring."); msgInfo(context, "/kb cancel - Cancel the restoration to be confirmed. If cancelled, /kb confirm will not run."); + msgInfo(context, "/kb cow-info - Display copy-on-write support info (Experimental)"); return SUCCESS; } @@ -135,6 +137,15 @@ public final class KBCommands { return SUCCESS; } + public static int cowInfo(CommandContext context) { + try { + msgInfo(context, "KBackup-cow library version: " + FileCowCopier.getVersion()); + } catch (Exception | UnsatisfiedLinkError ignored) { + msgErr(context, "KBackup-cow library is not loaded"); + } + return SUCCESS; + } + /** * Print backup information. * diff --git a/src/main/java/com/keuin/kbackupfabric/util/cow/FileCopier.java b/src/main/java/com/keuin/kbackupfabric/util/cow/FileCopier.java new file mode 100644 index 0000000..eb21af7 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/util/cow/FileCopier.java @@ -0,0 +1,9 @@ +package com.keuin.kbackupfabric.util.cow; + +import java.io.IOException; + +public interface FileCopier { + void copy(String dst, String src) throws IOException; + + boolean isCow(); +} diff --git a/src/main/java/com/keuin/kbackupfabric/util/cow/FileCowCopier.java b/src/main/java/com/keuin/kbackupfabric/util/cow/FileCowCopier.java new file mode 100644 index 0000000..e5819de --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/util/cow/FileCowCopier.java @@ -0,0 +1,44 @@ +package com.keuin.kbackupfabric.util.cow; + +import com.keuin.kbackupfabric.util.PrintUtil; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class FileCowCopier { + private static final AtomicBoolean initialized = new AtomicBoolean(false); + + static { + try { + System.loadLibrary("kbackup_cow"); + } catch (SecurityException | UnsatisfiedLinkError ignored) { + } + } + + public static native void init(); + + public static native void copy(String dst, String src) throws IOException; + + public static native String getVersion(); + + public static FileCopier getInstance() { + if (initialized.compareAndSet(false, true)) { + FileCowCopier.init(); + PrintUtil.info("kbackup-cow version: " + FileCowCopier.getVersion()); + } + // call a native method to ensure the dynamic library is correctly loaded, JVM will throw if failed + // so the outside fallback logic could work + FileCowCopier.getVersion(); + return new FileCopier() { + @Override + public void copy(String dst, String src) throws IOException { + FileCowCopier.copy(dst, src); + } + + @Override + public boolean isCow() { + return true; + } + }; + } +} diff --git a/src/main/java/com/keuin/kbackupfabric/util/cow/FileEagerCopier.java b/src/main/java/com/keuin/kbackupfabric/util/cow/FileEagerCopier.java new file mode 100644 index 0000000..0f12374 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/util/cow/FileEagerCopier.java @@ -0,0 +1,17 @@ +package com.keuin.kbackupfabric.util.cow; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class FileEagerCopier implements FileCopier { + @Override + public void copy(String dst, String src) throws IOException { + Files.copy(Paths.get(src), Paths.get(dst)); + } + + @Override + public boolean isCow() { + return false; + } +} -- cgit v1.2.3