Implement multi-subject and incremental backup

This commit is contained in:
iceBear67 committed 2025-09-28 10:48:45 +08:00
1 parent 82ae81c7af
commit 641fe1ac0a
13 files changed
+249 -117

No files matched your search

@@ -152,7 +152,7 @@ public class BundleWriter implements Closeable {
@SneakyThrows @SneakyThrows
private void writeReassembleInMem(Path path) { 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()); if(verbose) System.out.println("Mismatch " + path + ", parent: " + path.getParent());
writePlain(path); writePlain(path);
return; return;
@@ -6,7 +6,6 @@ import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder; import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.minecraft.command.CommandRegistryAccess; import net.minecraft.command.CommandRegistryAccess;
import net.minecraft.server.command.CommandManager; import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.command.ServerCommandSource;
@@ -55,23 +54,6 @@ public class BackupCommands {
return Command.SINGLE_SUCCESS; return Command.SINGLE_SUCCESS;
} }
public int cmdIssueBackup(CommandContext<ServerCommandSource> 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<ServerCommandSource> ctx) { public int cmdSuspend(CommandContext<ServerCommandSource> ctx) {
mod.suspend = !mod.suspend; mod.suspend = !mod.suspend;
ctx.getSource().sendMessage(Text.of("Backup disabled: " + mod.suspend)); ctx.getSource().sendMessage(Text.of("Backup disabled: " + mod.suspend));
@@ -148,7 +130,7 @@ public class BackupCommands {
.then(argument("backupName", StringArgumentType.greedyString()) .then(argument("backupName", StringArgumentType.greedyString())
.suggests(this::suggestBackups) .suggests(this::suggestBackups)
.executes(this::cmdRestore))) .executes(this::cmdRestore)))
.then(literal("create").executes(this::cmdIssueBackup)) // .then(literal("create").executes(this::cmdIssueBackupFull)) todo
.then(literal("suspend").executes(this::cmdSuspend)) .then(literal("suspend").executes(this::cmdSuspend))
); );
} }
@@ -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<Path> changedFiles
) {
}
+16 -24
View File
@@ -1,8 +1,6 @@
package io.ib67.sfcraft; package io.ib67.sfcraft;
import io.ib67.sfcraft.bundler.BundleWriter; 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 io.ib67.sfcraft.strategy.BackupStrategy;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
@@ -10,20 +8,25 @@ import net.minecraft.server.MinecraftServer;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; 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.Supplier;
import java.util.function.UnaryOperator;
import static io.ib67.sfcraft.JustBackupMod.BACKUP_LOCK; import static io.ib67.sfcraft.Globals.BACKUP_LOCK;
@Log4j2 @Log4j2
public record BackupWorker( public record BackupWorker(
MinecraftServer server, MinecraftServer server,
BackupStrategy strategy, BackupStrategy strategy,
JustBackupConfig config Path tmpDir,
BackupSubject subject,
UnaryOperator<BundleWriter.BundleWriterBuilder> config
) implements Supplier<Backup> { ) implements Supplier<Backup> {
@SneakyThrows
public BackupWorker {
if (Files.notExists(tmpDir)) Files.createDirectories(tmpDir);
}
@Override @Override
@SneakyThrows @SneakyThrows
public Backup get() { public Backup get() {
@@ -37,29 +40,17 @@ public record BackupWorker(
Thread.sleep(2000); Thread.sleep(2000);
} }
// performing backup is now true // performing backup is now true
var backupName = "Backup_" + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now()); var tmpBundle = tmpDir.resolve("backup_" + subject + "_" + System.currentTimeMillis() + ".swb.zst");
var backupParentRoot = Path.of(config.temporaryBackupDir());
if (Files.notExists(backupParentRoot)) Files.createDirectories(backupParentRoot);
var backupFile = backupParentRoot.resolve(backupName + ".jbp.zst");
try { try {
if (i == 0) { if (i == 0) {
// not saved yet. // not saved yet.
server.saveAll(true, true, false); server.saveAll(true, true, false);
} }
var session = ((LevelStorageAccessor) server).getSession(); // tmp file path
var path = session.getDirectory().getRootPath(); log.info("Creating bundle {}", tmpBundle);
log.info("Creating bundle {}", backupName); BundleWriter.createBundle(tmpBundle, subject.changedFiles(), config);
//todo diff return strategy.createBackup(subject.name(), subject.root(), tmpBundle);
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;
} catch (Exception e) { } catch (Exception e) {
Files.deleteIfExists(backupFile);
throw e; throw e;
} finally { } finally {
// Though we won't access the state within backup progress, there are certain cases // 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()); log.error("Expect BACKUP state but got {}", BACKUP_LOCK.get());
Thread.sleep(2000); Thread.sleep(2000);
} }
Files.deleteIfExists(tmpBundle);
} }
} }
} }
@@ -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<Path, String> watchSubject;
protected final BiConsumer<String, Path> eventListener;
public FileWatcherThread(
Map<String, Path> watchSubject,
BiConsumer<String, Path> 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<Path> 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<Path>) 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();
}
}
}
}
@@ -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<IOState> 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();
}
@@ -1,79 +1,47 @@
package io.ib67.sfcraft; package io.ib67.sfcraft;
import com.github.luben.zstd.Zstd; import com.github.luben.zstd.Zstd;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken; 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.JustBackupConfig;
import io.ib67.sfcraft.config.StorageOption; 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.BackupStrategy;
import io.ib67.sfcraft.strategy.BackupTracker; import io.ib67.sfcraft.strategy.BackupTracker;
import io.ib67.sfcraft.strategy.LocalBackupStrategy; import io.ib67.sfcraft.strategy.LocalBackupStrategy;
import io.ib67.sfcraft.strategy.S3BackupStrategy; import io.ib67.sfcraft.strategy.S3BackupStrategy;
import io.netty.buffer.ByteBufAllocator;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import net.fabricmc.api.ModInitializer; import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; 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.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 net.minecraft.text.Text;
import org.jetbrains.annotations.UnknownNullability; import org.jetbrains.annotations.UnknownNullability;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.RandomAccessFile; import java.nio.file.*;
import java.nio.file.Files; import java.util.ArrayList;
import java.nio.file.Path; import java.util.HashMap;
import java.time.LocalDateTime; import java.util.Map;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors;
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;
public class JustBackupMod implements ModInitializer { public class JustBackupMod implements ModInitializer {
public static final String MOD_ID = "justbackup"; public static final String MOD_ID = "justbackup";
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
/** protected final Map<String, BackupSubject> backupSubjects = new HashMap<>();
* This state indicates: protected volatile MinecraftServer server;
* 1. A minecraft auto-save is in-progress. protected volatile boolean suspend;
* 2. The backup is in-progress protected byte[] zstdDict;
*/
public static final AtomicReference<IOState> 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 ScheduledExecutorService scheduledBackupExecutor; protected ScheduledExecutorService scheduledBackupExecutor;
protected JustBackupConfig config; protected JustBackupConfig config;
protected BackupTracker tracker; protected BackupTracker tracker;
protected Path backupIndexPath; protected Path backupIndexPath;
protected volatile MinecraftServer server;
protected volatile boolean suspend;
@Override @Override
@SneakyThrows @SneakyThrows
@@ -89,20 +57,55 @@ public class JustBackupMod implements ModInitializer {
if (Files.notExists(backupIndexPath)) if (Files.notExists(backupIndexPath))
Files.createFile(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(); 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()); LOGGER.info("Configuration has been loaded. Using storage option: {}", config.option().type());
scheduledBackupExecutor = Executors.newSingleThreadScheduledExecutor(); scheduledBackupExecutor = Executors.newSingleThreadScheduledExecutor();
registerWatchService();
registerHandlers(); 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() { private void registerHandlers() {
ServerLifecycleEvents.SERVER_STARTED.register(server -> { ServerLifecycleEvents.SERVER_STARTED.register(server -> {
LOGGER.info("Launching background threads..."); LOGGER.info("Launching background threads...");
this.server = server; this.server = server;
scheduledBackupExecutor.scheduleAtFixedRate(this::issueBackup, if (config.bundleFullAtStartup()) {
backupAll(false);
}
scheduledBackupExecutor.scheduleAtFixedRate(() -> backupAll(config.incremental()),
config.backupIntervalMinutes(), config.backupIntervalMinutes(), TimeUnit.MINUTES); config.backupIntervalMinutes(), config.backupIntervalMinutes(), TimeUnit.MINUTES);
}); });
ServerLifecycleEvents.SERVER_STOPPED.register(server -> { ServerLifecycleEvents.SERVER_STOPPED.register(server -> {
@@ -123,12 +126,43 @@ public class JustBackupMod implements ModInitializer {
}); });
} }
public CompletableFuture<Backup> issueBackup() { public CompletableFuture<Map<String, Backup>> backupAll(boolean incremental) {
var map = new HashMap<String, CompletableFuture<Backup>>();
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<String, Backup>();
map.forEach((k, v) -> _map.put(k, v.join()));
return _map;
});
}
public CompletableFuture<Backup> issueBackup(String subjectName, boolean incremental) {
if (suspend) { if (suspend) {
return CompletableFuture.failedFuture(new IllegalStateException("Backup is temporarily disabled")); 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 subject = backupSubjects.get(subjectName);
var worker = new BackupWorker(server, tracker, config); 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); 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); server.getPlayerManager().broadcast(Text.literal("Backup failed. For administrators, please check your server console."), false);
LOGGER.error(throwable.getMessage(), throwable); LOGGER.error(throwable.getMessage(), throwable);
} else { } 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); server.getPlayerManager().broadcast(Text.literal("Backup success. " + backup), false);
} }
} }
@@ -154,17 +188,21 @@ public class JustBackupMod implements ModInitializer {
private void saveConfig(Path configPath) { private void saveConfig(Path configPath) {
LOGGER.info("Saving default configuration"); LOGGER.info("Saving default configuration");
var cfg = new JustBackupConfig( var cfg = new JustBackupConfig(
true,
true,
60, 60,
5, 5,
Zstd.defaultCompressionLevel(), Zstd.defaultCompressionLevel(),
4,
true, true,
"./.backup_cache", "./.backup_cache",
null, null,
new StorageOption.Local("backups", 0) new StorageOption.Local("backups", 0)
); );
var path = Path.of(cfg.temporaryBackupDir()); var path = Path.of(cfg.temporaryBackupDir());
if (Files.notExists(path)) { if (Files.notExists(path)) {
Files.createDirectories(path); Files.createDirectories(path);
} }
Files.writeString(configPath, GSON.toJson(cfg)); Files.writeString(configPath, Globals.GSON.toJson(cfg));
} }
} }
@@ -1,21 +1,39 @@
package io.ib67.sfcraft.config; package io.ib67.sfcraft.config;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.Validate; 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.List;
import java.util.Map;
@Log4j2
public record JustBackupConfig( public record JustBackupConfig(
boolean bundleFullAtStartup,
boolean incremental,
long backupIntervalMinutes, long backupIntervalMinutes,
int keepBackups, int keepBackups,
int compressionLevel, int compressionLevel,
int compressionWorkerThreads,
boolean allowGunzip, boolean allowGunzip,
String temporaryBackupDir, String temporaryBackupDir,
String zstdDictPath,
Map<String, String> backupSubjects,
StorageOption option StorageOption option
) { ) {
@SneakyThrows
public JustBackupConfig { public JustBackupConfig {
Validate.isTrue(backupIntervalMinutes > 0, "backup interval must be positive"); Validate.isTrue(backupIntervalMinutes > 0, "backup interval must be positive");
Validate.isTrue(keepBackups > 0, "keep backups 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"); 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"); Validate.notNull(option, "option cannot be null");
} }
} }
@@ -1,19 +1,14 @@
package io.ib67.sfcraft.mixin; package io.ib67.sfcraft.mixin;
import io.ib67.sfcraft.Globals;
import io.ib67.sfcraft.IOState; import io.ib67.sfcraft.IOState;
import io.ib67.sfcraft.JustBackupMod;
import lombok.Getter;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; 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.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import javax.security.auth.callback.Callback;
import java.util.concurrent.atomic.AtomicBoolean;
@Mixin(MinecraftServer.class) @Mixin(MinecraftServer.class)
public abstract class MixinMinecraftServer { public abstract class MixinMinecraftServer {
@@ -28,7 +23,7 @@ public abstract class MixinMinecraftServer {
// is already backing up // is already backing up
this.ticksUntilAutosave = getAutosaveInterval(); this.ticksUntilAutosave = getAutosaveInterval();
IOState witness; 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 { try {
Thread.sleep(1000); Thread.sleep(1000);
} catch (InterruptedException e) { } catch (InterruptedException e) {
@@ -41,6 +36,6 @@ public abstract class MixinMinecraftServer {
@Inject(method = "runAutosave", at = @At("RETURN")) @Inject(method = "runAutosave", at = @At("RETURN"))
private void backup$disableProtect(CallbackInfo ci) { private void backup$disableProtect(CallbackInfo ci) {
JustBackupMod.BACKUP_LOCK.setRelease(IOState.IDLE); Globals.BACKUP_LOCK.setRelease(IOState.IDLE);
} }
} }
@@ -1,9 +1,8 @@
package io.ib67.sfcraft.mixin; package io.ib67.sfcraft.mixin;
import io.ib67.sfcraft.Globals;
import io.ib67.sfcraft.IOState; import io.ib67.sfcraft.IOState;
import io.ib67.sfcraft.JustBackupMod;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectIterator;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import net.minecraft.world.storage.RegionBasedStorage; import net.minecraft.world.storage.RegionBasedStorage;
import net.minecraft.world.storage.RegionFile; 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.Mixin;
import org.spongepowered.asm.mixin.Overwrite; import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow; 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) @Mixin(RegionBasedStorage.class)
public abstract class MixinRegionBasedStorage { public abstract class MixinRegionBasedStorage {
@@ -31,12 +27,12 @@ public abstract class MixinRegionBasedStorage {
// allowed states: // allowed states:
// STORAGE_SYNC, AUTO_SAVE, IDLE // STORAGE_SYNC, AUTO_SAVE, IDLE
// we try // we try
var witness = JustBackupMod.BACKUP_LOCK.getAndSet(IOState.STORAGE_SYNC); var witness = Globals.BACKUP_LOCK.getAndSet(IOState.STORAGE_SYNC);
if (witness == IOState.BACKUP) { if (witness == IOState.BACKUP) {
// yield out and keep blocking. // yield out and keep blocking.
// The backup worker will wait for us setting this back to backup. // The backup worker will wait for us setting this back to backup.
JustBackupMod.BACKUP_LOCK.set(IOState.BACKUP); Globals.BACKUP_LOCK.set(IOState.BACKUP);
while (JustBackupMod.BACKUP_LOCK.getAcquire() == IOState.BACKUP) { while (Globals.BACKUP_LOCK.getAcquire() == IOState.BACKUP) {
Thread.sleep(2000); Thread.sleep(2000);
} }
} }
@@ -49,8 +45,8 @@ public abstract class MixinRegionBasedStorage {
// we asserts that there are no other threads calling sync() // we asserts that there are no other threads calling sync()
// so we can just jump back to the witness value. // so we can just jump back to the witness value.
// witness: AUTO_SAVE or IDLE // witness: AUTO_SAVE or IDLE
JustBackupMod.BACKUP_LOCK.compareAndExchange(IOState.STORAGE_SYNC, witness); Globals.BACKUP_LOCK.compareAndExchange(IOState.STORAGE_SYNC, witness);
JustBackupMod.BACKUP_LOCK.setRelease(witness); Globals.BACKUP_LOCK.setRelease(witness);
} }
} }
} }
@@ -9,7 +9,7 @@ public interface BackupStrategy {
* create a backup * create a backup
* @return a key to this 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); void recoverBackup(Backup backup, Path restorePath);
@@ -25,12 +25,16 @@ public class LocalBackupStrategy implements BackupStrategy {
@Override @Override
@SneakyThrows @SneakyThrows
public Backup createBackup(Path from, Path pathToBundle) { public Backup createBackup(String subject, Path from, Path pathToBundle) {
var result = new Backup( var result = new Backup(
pathToBundle.getFileName().toString(), pathToBundle.getFileName().toString(),
pathToBundle.getFileName().toString(), pathToBundle.getFileName().toString(),
"local", from.toString(), false, Files.size(pathToBundle)); "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; return result;
} }
@@ -40,7 +44,7 @@ public class LocalBackupStrategy implements BackupStrategy {
// check if it is compressed // check if it is compressed
var backupFile = backupParentRoot.resolve(backup.backupKey()); var backupFile = backupParentRoot.resolve(backup.backupKey());
if (Files.notExists(backupFile)) throw new IllegalArgumentException("Backup " + backupFile + " does not exist"); if (Files.notExists(backupFile)) throw new IllegalArgumentException("Backup " + backupFile + " does not exist");
BundleReader.extract(backupFile, restorePath); BundleReader.builder().build().extract(backupFile, restorePath);
} }
@SneakyThrows @SneakyThrows
@@ -84,7 +84,7 @@ public class S3BackupStrategy implements BackupStrategy {
var target = temporaryDownloadPath.resolve("s3_" + System.currentTimeMillis()); var target = temporaryDownloadPath.resolve("s3_" + System.currentTimeMillis());
try (var fs = Files.newOutputStream(target, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { try (var fs = Files.newOutputStream(target, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
resp.transferTo(fs); resp.transferTo(fs);
BundleReader.extract(target, restorePath); BundleReader.builder().build().extract(target, restorePath);
} finally { } finally {
Files.deleteIfExists(target); Files.deleteIfExists(target);
} }