Implement multi-subject and incremental backup
This commit is contained in:
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
|
||||
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;
|
||||
|
||||
@@ -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<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) {
|
||||
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))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
@@ -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<BundleWriter.BundleWriterBuilder> config
|
||||
) implements Supplier<Backup> {
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
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<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 final Map<String, BackupSubject> 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<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) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -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<String, String> 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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user