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
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
) {
}
+16 -24
View File
@@ -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);
}