diff --git a/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleWriter.java b/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleWriter.java index ee6f4b8..84b118c 100644 --- a/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleWriter.java +++ b/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleWriter.java @@ -152,7 +152,7 @@ public class BundleWriter implements Closeable { @SneakyThrows private void writeReassembleInMem(Path path) { - if (!"region".equals(path.getParent().toString()) && !path.toString().endsWith(".mca")) { + if (!"region".equals(path.getParent().toString()) && !path.toString().endsWith(".mca")) { // todo use pattern matcher if(verbose) System.out.println("Mismatch " + path + ", parent: " + path.getParent()); writePlain(path); return; diff --git a/src/main/java/io/ib67/sfcraft/BackupCommands.java b/src/main/java/io/ib67/sfcraft/BackupCommands.java index 7b10391..9a2a39c 100644 --- a/src/main/java/io/ib67/sfcraft/BackupCommands.java +++ b/src/main/java/io/ib67/sfcraft/BackupCommands.java @@ -6,7 +6,6 @@ import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; -import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.minecraft.command.CommandRegistryAccess; import net.minecraft.server.command.CommandManager; import net.minecraft.server.command.ServerCommandSource; @@ -55,23 +54,6 @@ public class BackupCommands { return Command.SINGLE_SUCCESS; } - public int cmdIssueBackup(CommandContext ctx) { - var s = ctx.getSource(); - if (JustBackupMod.PERFORMING_BACKUP.get()) { - s.sendMessage(Text.of("You can't issue a backup because another backup is in progress.")); - return Command.SINGLE_SUCCESS; - } - mod.issueBackup() - .whenComplete((backup, error) -> { - if (error != null) { - s.sendMessage(Text.of("Cannot backup, error: " + error.getMessage())); - } else { - s.sendMessage(Text.of("Backup created successfully! name: " + backup.name())); - } - }); - return Command.SINGLE_SUCCESS; - } - public int cmdSuspend(CommandContext ctx) { mod.suspend = !mod.suspend; ctx.getSource().sendMessage(Text.of("Backup disabled: " + mod.suspend)); @@ -148,7 +130,7 @@ public class BackupCommands { .then(argument("backupName", StringArgumentType.greedyString()) .suggests(this::suggestBackups) .executes(this::cmdRestore))) - .then(literal("create").executes(this::cmdIssueBackup)) +// .then(literal("create").executes(this::cmdIssueBackupFull)) todo .then(literal("suspend").executes(this::cmdSuspend)) ); } diff --git a/src/main/java/io/ib67/sfcraft/BackupSubject.java b/src/main/java/io/ib67/sfcraft/BackupSubject.java new file mode 100644 index 0000000..766d4ef --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/BackupSubject.java @@ -0,0 +1,11 @@ +package io.ib67.sfcraft; + +import java.nio.file.Path; +import java.util.List; + +public record BackupSubject( + String name, + Path root, + List changedFiles +) { +} diff --git a/src/main/java/io/ib67/sfcraft/BackupWorker.java b/src/main/java/io/ib67/sfcraft/BackupWorker.java index ae2f59d..a1399bf 100644 --- a/src/main/java/io/ib67/sfcraft/BackupWorker.java +++ b/src/main/java/io/ib67/sfcraft/BackupWorker.java @@ -1,8 +1,6 @@ package io.ib67.sfcraft; import io.ib67.sfcraft.bundler.BundleWriter; -import io.ib67.sfcraft.config.JustBackupConfig; -import io.ib67.sfcraft.mixin.LevelStorageAccessor; import io.ib67.sfcraft.strategy.BackupStrategy; import lombok.SneakyThrows; import lombok.extern.log4j.Log4j2; @@ -10,20 +8,25 @@ import net.minecraft.server.MinecraftServer; import java.nio.file.Files; import java.nio.file.Path; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.List; import java.util.function.Supplier; +import java.util.function.UnaryOperator; -import static io.ib67.sfcraft.JustBackupMod.BACKUP_LOCK; +import static io.ib67.sfcraft.Globals.BACKUP_LOCK; @Log4j2 public record BackupWorker( MinecraftServer server, BackupStrategy strategy, - JustBackupConfig config + Path tmpDir, + BackupSubject subject, + UnaryOperator config ) implements Supplier { + @SneakyThrows + public BackupWorker { + if (Files.notExists(tmpDir)) Files.createDirectories(tmpDir); + } + @Override @SneakyThrows public Backup get() { @@ -37,29 +40,17 @@ public record BackupWorker( Thread.sleep(2000); } // performing backup is now true - var backupName = "Backup_" + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now()); - var backupParentRoot = Path.of(config.temporaryBackupDir()); - if (Files.notExists(backupParentRoot)) Files.createDirectories(backupParentRoot); - var backupFile = backupParentRoot.resolve(backupName + ".jbp.zst"); + var tmpBundle = tmpDir.resolve("backup_" + subject + "_" + System.currentTimeMillis() + ".swb.zst"); try { if (i == 0) { // not saved yet. server.saveAll(true, true, false); } - var session = ((LevelStorageAccessor) server).getSession(); - var path = session.getDirectory().getRootPath(); - log.info("Creating bundle {}", backupName); - //todo diff - Path worldDir = Path.of(path); - var tree = new WorldDir(worldDir); - BundleWriter.createBundle(backupFile, tree.everything(), c -> c.allowGunzip(config.allowGunzip()) - .relativeRoot(worldDir) - .compressionLevel(config.compressionLevel())); - var backup = strategy.createBackup(worldDir, backupFile); - Files.deleteIfExists(backupFile); - return backup; + // tmp file path + log.info("Creating bundle {}", tmpBundle); + BundleWriter.createBundle(tmpBundle, subject.changedFiles(), config); + return strategy.createBackup(subject.name(), subject.root(), tmpBundle); } catch (Exception e) { - Files.deleteIfExists(backupFile); throw e; } finally { // Though we won't access the state within backup progress, there are certain cases @@ -69,6 +60,7 @@ public record BackupWorker( log.error("Expect BACKUP state but got {}", BACKUP_LOCK.get()); Thread.sleep(2000); } + Files.deleteIfExists(tmpBundle); } } } diff --git a/src/main/java/io/ib67/sfcraft/FileWatcherThread.java b/src/main/java/io/ib67/sfcraft/FileWatcherThread.java new file mode 100644 index 0000000..68c13cf --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/FileWatcherThread.java @@ -0,0 +1,69 @@ +package io.ib67.sfcraft; + +import lombok.SneakyThrows; + +import java.nio.file.*; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class FileWatcherThread extends Thread { + protected final Map watchSubject; + protected final BiConsumer eventListener; + + public FileWatcherThread( + Map watchSubject, + BiConsumer eventListener + ) { + this.watchSubject = watchSubject.entrySet() + .stream() + .flatMap(it -> walkThrough(it.getValue()).map(r -> Map.entry(r, it.getKey()))) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue + )); + this.eventListener = eventListener; + setName("JustBackup/FileWatcher"); + } + + @SneakyThrows + private static Stream walkThrough(Path path) { + return Files.walk(path); + } + + @Override + @SneakyThrows + public void run() { + try (var watcher = FileSystems.getDefault().newWatchService()) { + for (Path value : watchSubject.keySet()) { + value.register( + watcher, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_MODIFY + ); + } + for (; ; ) { + var key = watcher.poll(10, TimeUnit.SECONDS); + if (key == null) continue; + if (Thread.interrupted()) return; + for (var ev : key.pollEvents()) { + var kind = ev.kind(); + if (kind == StandardWatchEventKinds.OVERFLOW) + continue; + var path = ((WatchEvent) ev).context(); + var subject = Objects.requireNonNull(watchSubject.get(path.getParent())); + if (Files.isRegularFile(path)) { + eventListener.accept(subject, path.getParent()); + } else if (Files.isDirectory(path) && kind == StandardWatchEventKinds.ENTRY_CREATE) { + path.register(watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY); + watchSubject.put(path, subject); + } + } + key.reset(); + } + } + } +} diff --git a/src/main/java/io/ib67/sfcraft/Globals.java b/src/main/java/io/ib67/sfcraft/Globals.java new file mode 100644 index 0000000..8062353 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/Globals.java @@ -0,0 +1,27 @@ +package io.ib67.sfcraft; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import io.ib67.sfcraft.config.StorageOption; +import io.ib67.sfcraft.config.serializer.StorageOptionSerializer; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +public class Globals { + /** + * for api users: DO NOT MUTATE THIS STATE IN WHATEVER CASE SINCE THIS IS CAUTIOUSLY ALIGNED WITH THE CURRENT THREAD MODEL + * AND DOING SO WILL BREAK YOUR WORLD + * This state indicates: + * 1. A minecraft auto-save is in-progress. + * 2. The backup is in-progress + * 3. Some chunks are writing to disk + */ + public static final AtomicReference BACKUP_LOCK = new AtomicReference<>(IOState.IDLE); + public static final Gson GSON = new GsonBuilder().setPrettyPrinting() + .registerTypeAdapter(StorageOption.class, new StorageOptionSerializer( + Map.of("s3", StorageOption.S3.class, + "local", StorageOption.Local.class) + )) + .create(); +} diff --git a/src/main/java/io/ib67/sfcraft/JustBackupMod.java b/src/main/java/io/ib67/sfcraft/JustBackupMod.java index 4b7add5..5a4ea2e 100644 --- a/src/main/java/io/ib67/sfcraft/JustBackupMod.java +++ b/src/main/java/io/ib67/sfcraft/JustBackupMod.java @@ -1,79 +1,47 @@ package io.ib67.sfcraft; import com.github.luben.zstd.Zstd; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; -import com.mojang.brigadier.CommandDispatcher; -import com.mojang.brigadier.arguments.StringArgumentType; -import com.mojang.brigadier.context.CommandContext; -import com.mojang.brigadier.suggestion.Suggestions; -import com.mojang.brigadier.suggestion.SuggestionsBuilder; import io.ib67.sfcraft.config.JustBackupConfig; import io.ib67.sfcraft.config.StorageOption; -import io.ib67.sfcraft.config.serializer.StorageOptionSerializer; -import io.ib67.sfcraft.mixin.LevelStorageAccessor; -import io.ib67.sfcraft.mixin.MixinMinecraftServer; import io.ib67.sfcraft.strategy.BackupStrategy; import io.ib67.sfcraft.strategy.BackupTracker; import io.ib67.sfcraft.strategy.LocalBackupStrategy; import io.ib67.sfcraft.strategy.S3BackupStrategy; +import io.netty.buffer.ByteBufAllocator; import lombok.SneakyThrows; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; -import net.jpountz.lz4.LZ4BlockInputStream; -import net.minecraft.command.CommandRegistryAccess; import net.minecraft.server.MinecraftServer; -import net.minecraft.server.command.CommandManager; -import net.minecraft.server.command.ServerCommandSource; -import net.minecraft.server.world.ServerWorld; import net.minecraft.text.Text; import org.jetbrains.annotations.UnknownNullability; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.FileInputStream; import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; +import java.nio.file.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.ReentrantLock; - -import static net.minecraft.server.command.CommandManager.argument; -import static net.minecraft.server.command.CommandManager.literal; +import java.util.stream.Collectors; public class JustBackupMod implements ModInitializer { public static final String MOD_ID = "justbackup"; public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); - /** - * This state indicates: - * 1. A minecraft auto-save is in-progress. - * 2. The backup is in-progress - */ - public static final AtomicReference BACKUP_LOCK = new AtomicReference<>(IOState.IDLE); - public static final Gson GSON = new GsonBuilder().setPrettyPrinting() - .registerTypeAdapter(StorageOption.class, new StorageOptionSerializer( - Map.of("s3", StorageOption.S3.class, - "local", StorageOption.Local.class) - )) - .create(); + protected final Map backupSubjects = new HashMap<>(); + protected volatile MinecraftServer server; + protected volatile boolean suspend; + protected byte[] zstdDict; protected ScheduledExecutorService scheduledBackupExecutor; protected JustBackupConfig config; protected BackupTracker tracker; protected Path backupIndexPath; - protected volatile MinecraftServer server; - protected volatile boolean suspend; @Override @SneakyThrows @@ -89,20 +57,55 @@ public class JustBackupMod implements ModInitializer { if (Files.notExists(backupIndexPath)) Files.createFile(backupIndexPath); - config = GSON.fromJson(Files.readString(configPath), JustBackupConfig.class); + config = Globals.GSON.fromJson(Files.readString(configPath), JustBackupConfig.class); + config.backupSubjects().forEach((k, v) -> { + var subjectPath = Path.of(v); + if (Files.notExists(subjectPath)) { + LOGGER.error("Backup subject {} does not exist.", v); + return; + } + backupSubjects.put(k, new BackupSubject(k, subjectPath, new ArrayList<>())); + }); + if (config.zstdDictPath() != null && !config.zstdDictPath().isEmpty()) { + zstdDict = Files.readAllBytes(Path.of(config.zstdDictPath())); + } var strategy = createStrategy(); - this.tracker = new BackupTracker(strategy, GSON.fromJson(Files.readString(backupIndexPath), new TypeToken<>() { + this.tracker = new BackupTracker(strategy, Globals.GSON.fromJson(Files.readString(backupIndexPath), new TypeToken<>() { })); LOGGER.info("Configuration has been loaded. Using storage option: {}", config.option().type()); scheduledBackupExecutor = Executors.newSingleThreadScheduledExecutor(); + registerWatchService(); registerHandlers(); } + private void registerWatchService() { + var watcherThread = new FileWatcherThread( + config.backupSubjects().entrySet().stream() + .map(it -> Map.entry(it.getKey(), Path.of(it.getValue()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)), + this::handleFileChanges + ); + ServerLifecycleEvents.SERVER_STOPPED.register(server -> { + watcherThread.interrupt(); + }); + watcherThread.start(); + } + + private void handleFileChanges(String subject, Path path) { + synchronized (backupSubjects) { + backupSubjects.get(subject).changedFiles().add(path); + } + } + + @SneakyThrows private void registerHandlers() { ServerLifecycleEvents.SERVER_STARTED.register(server -> { LOGGER.info("Launching background threads..."); this.server = server; - scheduledBackupExecutor.scheduleAtFixedRate(this::issueBackup, + if (config.bundleFullAtStartup()) { + backupAll(false); + } + scheduledBackupExecutor.scheduleAtFixedRate(() -> backupAll(config.incremental()), config.backupIntervalMinutes(), config.backupIntervalMinutes(), TimeUnit.MINUTES); }); ServerLifecycleEvents.SERVER_STOPPED.register(server -> { @@ -123,12 +126,43 @@ public class JustBackupMod implements ModInitializer { }); } - public CompletableFuture issueBackup() { + public CompletableFuture> backupAll(boolean incremental) { + var map = new HashMap>(); + for (var entry : backupSubjects.entrySet()) { + map.put(entry.getKey(), issueBackup(entry.getKey(), incremental)); + } + return CompletableFuture.allOf(map.values().toArray(new CompletableFuture[0])) + .thenApply(it -> { + var _map = new HashMap(); + map.forEach((k, v) -> _map.put(k, v.join())); + return _map; + }); + } + + public CompletableFuture issueBackup(String subjectName, boolean incremental) { if (suspend) { return CompletableFuture.failedFuture(new IllegalStateException("Backup is temporarily disabled")); } - server.getPlayerManager().broadcast(Text.literal("The server will begin the backup shortly, you may experience some lag."), false); - var worker = new BackupWorker(server, tracker, config); + var subject = backupSubjects.get(subjectName); + if (subject == null) + return CompletableFuture.failedFuture(new IllegalArgumentException("Unknown backup subject: " + subjectName)); + if (!incremental) { + try (var s = Files.walk(subject.root())) { + subject = new BackupSubject(subject.name(), subject.root(), s.filter(Files::isRegularFile).toList()); + } catch (IOException e) { + return CompletableFuture.failedFuture(new IllegalArgumentException("Cannot perform full backup for " + subjectName, e)); + } + } + var finalSubject = subject; + var worker = new BackupWorker(server, tracker, Path.of(config.temporaryBackupDir()), + subject, c -> + c.allowGunzip(config.allowGunzip()) + .relativeRoot(finalSubject.root()) + .allocator(ByteBufAllocator.DEFAULT) + .maxWorkers(config.compressionWorkerThreads()) + .compressionLevel(config.compressionLevel()) + .dictionary(zstdDict) + ); return CompletableFuture.supplyAsync(worker, scheduledBackupExecutor).whenComplete(this::onBackupComplete); } @@ -138,7 +172,7 @@ public class JustBackupMod implements ModInitializer { server.getPlayerManager().broadcast(Text.literal("Backup failed. For administrators, please check your server console."), false); LOGGER.error(throwable.getMessage(), throwable); } else { - Files.writeString(backupIndexPath, GSON.toJson(tracker.getTrackedBackups())); + Files.writeString(backupIndexPath, Globals.GSON.toJson(tracker.getTrackedBackups())); server.getPlayerManager().broadcast(Text.literal("Backup success. " + backup), false); } } @@ -154,17 +188,21 @@ public class JustBackupMod implements ModInitializer { private void saveConfig(Path configPath) { LOGGER.info("Saving default configuration"); var cfg = new JustBackupConfig( + true, + true, 60, 5, Zstd.defaultCompressionLevel(), + 4, true, "./.backup_cache", + null, null, new StorageOption.Local("backups", 0) ); var path = Path.of(cfg.temporaryBackupDir()); if (Files.notExists(path)) { Files.createDirectories(path); } - Files.writeString(configPath, GSON.toJson(cfg)); + Files.writeString(configPath, Globals.GSON.toJson(cfg)); } } \ No newline at end of file diff --git a/src/main/java/io/ib67/sfcraft/config/JustBackupConfig.java b/src/main/java/io/ib67/sfcraft/config/JustBackupConfig.java index 8cf4fcc..76e8701 100644 --- a/src/main/java/io/ib67/sfcraft/config/JustBackupConfig.java +++ b/src/main/java/io/ib67/sfcraft/config/JustBackupConfig.java @@ -1,21 +1,39 @@ package io.ib67.sfcraft.config; +import lombok.SneakyThrows; +import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.Validate; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; import java.util.List; +import java.util.Map; +@Log4j2 public record JustBackupConfig( + boolean bundleFullAtStartup, + boolean incremental, long backupIntervalMinutes, int keepBackups, int compressionLevel, + int compressionWorkerThreads, boolean allowGunzip, String temporaryBackupDir, + String zstdDictPath, + Map backupSubjects, StorageOption option ) { + @SneakyThrows public JustBackupConfig { Validate.isTrue(backupIntervalMinutes > 0, "backup interval must be positive"); Validate.isTrue(keepBackups > 0, "keep backups must be positive"); + Validate.isTrue(compressionWorkerThreads >= 0, "compression worker threads must be positive"); Validate.isTrue(compressionLevel > 0, "compression level must be positive"); + if (backupSubjects == null) backupSubjects = new HashMap<>(); + if (backupSubjects.isEmpty()) { + log.warn("Backup subjects are not configured. The mod will not work!"); + } Validate.notNull(option, "option cannot be null"); } } diff --git a/src/main/java/io/ib67/sfcraft/mixin/MixinMinecraftServer.java b/src/main/java/io/ib67/sfcraft/mixin/MixinMinecraftServer.java index cb88f52..98aa602 100644 --- a/src/main/java/io/ib67/sfcraft/mixin/MixinMinecraftServer.java +++ b/src/main/java/io/ib67/sfcraft/mixin/MixinMinecraftServer.java @@ -1,19 +1,14 @@ package io.ib67.sfcraft.mixin; +import io.ib67.sfcraft.Globals; import io.ib67.sfcraft.IOState; -import io.ib67.sfcraft.JustBackupMod; -import lombok.Getter; import net.minecraft.server.MinecraftServer; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import javax.security.auth.callback.Callback; -import java.util.concurrent.atomic.AtomicBoolean; - @Mixin(MinecraftServer.class) public abstract class MixinMinecraftServer { @@ -28,7 +23,7 @@ public abstract class MixinMinecraftServer { // is already backing up this.ticksUntilAutosave = getAutosaveInterval(); IOState witness; - while ((witness = JustBackupMod.BACKUP_LOCK.compareAndExchange(IOState.IDLE, IOState.SAVING_WORLD)) == IOState.STORAGE_SYNC) { + while ((witness = Globals.BACKUP_LOCK.compareAndExchange(IOState.IDLE, IOState.SAVING_WORLD)) == IOState.STORAGE_SYNC) { try { Thread.sleep(1000); } catch (InterruptedException e) { @@ -41,6 +36,6 @@ public abstract class MixinMinecraftServer { @Inject(method = "runAutosave", at = @At("RETURN")) private void backup$disableProtect(CallbackInfo ci) { - JustBackupMod.BACKUP_LOCK.setRelease(IOState.IDLE); + Globals.BACKUP_LOCK.setRelease(IOState.IDLE); } } diff --git a/src/main/java/io/ib67/sfcraft/mixin/MixinRegionBasedStorage.java b/src/main/java/io/ib67/sfcraft/mixin/MixinRegionBasedStorage.java index fc40dbf..db050b9 100644 --- a/src/main/java/io/ib67/sfcraft/mixin/MixinRegionBasedStorage.java +++ b/src/main/java/io/ib67/sfcraft/mixin/MixinRegionBasedStorage.java @@ -1,9 +1,8 @@ package io.ib67.sfcraft.mixin; +import io.ib67.sfcraft.Globals; import io.ib67.sfcraft.IOState; -import io.ib67.sfcraft.JustBackupMod; import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; -import it.unimi.dsi.fastutil.objects.ObjectIterator; import lombok.SneakyThrows; import net.minecraft.world.storage.RegionBasedStorage; import net.minecraft.world.storage.RegionFile; @@ -11,9 +10,6 @@ import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Overwrite; import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(RegionBasedStorage.class) public abstract class MixinRegionBasedStorage { @@ -31,12 +27,12 @@ public abstract class MixinRegionBasedStorage { // allowed states: // STORAGE_SYNC, AUTO_SAVE, IDLE // we try - var witness = JustBackupMod.BACKUP_LOCK.getAndSet(IOState.STORAGE_SYNC); + var witness = Globals.BACKUP_LOCK.getAndSet(IOState.STORAGE_SYNC); if (witness == IOState.BACKUP) { // yield out and keep blocking. // The backup worker will wait for us setting this back to backup. - JustBackupMod.BACKUP_LOCK.set(IOState.BACKUP); - while (JustBackupMod.BACKUP_LOCK.getAcquire() == IOState.BACKUP) { + Globals.BACKUP_LOCK.set(IOState.BACKUP); + while (Globals.BACKUP_LOCK.getAcquire() == IOState.BACKUP) { Thread.sleep(2000); } } @@ -49,8 +45,8 @@ public abstract class MixinRegionBasedStorage { // we asserts that there are no other threads calling sync() // so we can just jump back to the witness value. // witness: AUTO_SAVE or IDLE - JustBackupMod.BACKUP_LOCK.compareAndExchange(IOState.STORAGE_SYNC, witness); - JustBackupMod.BACKUP_LOCK.setRelease(witness); + Globals.BACKUP_LOCK.compareAndExchange(IOState.STORAGE_SYNC, witness); + Globals.BACKUP_LOCK.setRelease(witness); } } } diff --git a/src/main/java/io/ib67/sfcraft/strategy/BackupStrategy.java b/src/main/java/io/ib67/sfcraft/strategy/BackupStrategy.java index c942712..205523b 100644 --- a/src/main/java/io/ib67/sfcraft/strategy/BackupStrategy.java +++ b/src/main/java/io/ib67/sfcraft/strategy/BackupStrategy.java @@ -9,7 +9,7 @@ public interface BackupStrategy { * create a backup * @return a key to this backup */ - Backup createBackup(Path source, Path bundle); + Backup createBackup(String subject, Path source, Path bundle); void recoverBackup(Backup backup, Path restorePath); diff --git a/src/main/java/io/ib67/sfcraft/strategy/LocalBackupStrategy.java b/src/main/java/io/ib67/sfcraft/strategy/LocalBackupStrategy.java index 7afc768..139b4ee 100644 --- a/src/main/java/io/ib67/sfcraft/strategy/LocalBackupStrategy.java +++ b/src/main/java/io/ib67/sfcraft/strategy/LocalBackupStrategy.java @@ -25,12 +25,16 @@ public class LocalBackupStrategy implements BackupStrategy { @Override @SneakyThrows - public Backup createBackup(Path from, Path pathToBundle) { + public Backup createBackup(String subject, Path from, Path pathToBundle) { var result = new Backup( pathToBundle.getFileName().toString(), pathToBundle.getFileName().toString(), "local", from.toString(), false, Files.size(pathToBundle)); - Files.move(pathToBundle, backupParentRoot.resolve(pathToBundle.getFileName())); + var parent = backupParentRoot.resolve(subject); + if(Files.notExists(parent)) { + Files.createDirectories(parent); + } + Files.move(pathToBundle, parent.resolve(pathToBundle.getFileName())); return result; } @@ -40,7 +44,7 @@ public class LocalBackupStrategy implements BackupStrategy { // check if it is compressed var backupFile = backupParentRoot.resolve(backup.backupKey()); if (Files.notExists(backupFile)) throw new IllegalArgumentException("Backup " + backupFile + " does not exist"); - BundleReader.extract(backupFile, restorePath); + BundleReader.builder().build().extract(backupFile, restorePath); } @SneakyThrows diff --git a/src/main/java/io/ib67/sfcraft/strategy/S3BackupStrategy.java b/src/main/java/io/ib67/sfcraft/strategy/S3BackupStrategy.java index 4d5cb55..f14495c 100644 --- a/src/main/java/io/ib67/sfcraft/strategy/S3BackupStrategy.java +++ b/src/main/java/io/ib67/sfcraft/strategy/S3BackupStrategy.java @@ -84,7 +84,7 @@ public class S3BackupStrategy implements BackupStrategy { var target = temporaryDownloadPath.resolve("s3_" + System.currentTimeMillis()); try (var fs = Files.newOutputStream(target, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { resp.transferTo(fs); - BundleReader.extract(target, restorePath); + BundleReader.builder().build().extract(target, restorePath); } finally { Files.deleteIfExists(target); }