refactor: Restructure project into multi-module setup and improve bundler

- Convert project to multi-module structure with 'bundler' subproject
- Extract bundler functionality into separate module with proper dependencies
- Improve IO state management with atomic reference for better synchronization
- Enhance bundler performance with parallel processing support
- Add CLI interface to bundler with various compression options
- Fix file handling issues in bundle reader/writer
- Update compression configuration and remove redundant options
- Improve region file synchronization with proper state management
- Add dictionary support for better compression ratios
- Refactor backup strategies to use new bundler API
- Update dependencies and build configuration
- Fix path resolution issues in bundle operations
`
This commit is contained in:
iceBear67 committed 2025-09-26 16:55:36 +08:00
1 parent 3a4256cee1
commit 28280bc162
26 files changed
+605 -536

No files matched your search

+24 -13
View File
@@ -1,6 +1,6 @@
package io.ib67.sfcraft;
import io.ib67.sfcraft.bundler.Bundle;
import io.ib67.sfcraft.bundler.BundleWriter;
import io.ib67.sfcraft.config.JustBackupConfig;
import io.ib67.sfcraft.mixin.LevelStorageAccessor;
import io.ib67.sfcraft.strategy.BackupStrategy;
@@ -12,9 +12,10 @@ 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 static io.ib67.sfcraft.JustBackupMod.PERFORMING_BACKUP;
import static io.ib67.sfcraft.JustBackupMod.BACKUP_LOCK;
@Log4j2
public record BackupWorker(
@@ -30,11 +31,16 @@ public record BackupWorker(
throw new IllegalStateException("MinecraftServer is not initialized yet, not doing backup");
}
int i = 0;
while (!PERFORMING_BACKUP.compareAndSet(false, true)) {
if (i++ % 10 == 0) log.info("Server is saving world while performing backup! waiting...");
while (!BACKUP_LOCK.compareAndSet(IOState.IDLE, IOState.BACKUP)) {
log.info("Server is saving world while performing backup! waiting...");
i++;
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 + ".jpack");
try {
if (i == 0) {
// not saved yet.
@@ -42,22 +48,27 @@ public record BackupWorker(
}
var session = ((LevelStorageAccessor) server).getSession();
var path = session.getDirectory().getRootPath();
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 + ".jpack");
log.info("Creating bundle {}", backupName);
//todo diff
Path worldDir = Path.of(path);
new Bundle(new WorldDir(worldDir))
.compress(config.compress())
.allowGunzip(config.allowGunzip())
.buildBundle(backupFile);
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) {
Files.deleteIfExists(backupFile);
throw e;
} finally {
PERFORMING_BACKUP.set(false);
// Though we won't access the state within backup progress, there are certain cases
// that this state will be changed to STORAGE_SYNC.
// Also take a look at MixinRegionBasedStorage.
while (!BACKUP_LOCK.compareAndSet(IOState.BACKUP, IOState.IDLE)) {
log.error("Expect BACKUP state but got {}", BACKUP_LOCK.get());
Thread.sleep(2000);
}
}
}
}
@@ -0,0 +1,8 @@
package io.ib67.sfcraft;
public enum IOState {
SAVING_WORLD,
BACKUP,
STORAGE_SYNC,
IDLE
}
@@ -1,5 +1,6 @@
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;
@@ -45,6 +46,8 @@ 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;
@@ -57,7 +60,7 @@ public class JustBackupMod implements ModInitializer {
* 1. A minecraft auto-save is in-progress.
* 2. The backup is in-progress
*/
public static final AtomicBoolean PERFORMING_BACKUP = new AtomicBoolean(false);
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,
@@ -75,8 +78,9 @@ public class JustBackupMod implements ModInitializer {
@SneakyThrows
public void onInitialize() {
LOGGER.info("Loading backup configuration");
backupIndexPath = Path.of("config").resolve("just_backup_index.json");
var configPath = Path.of("config").resolve("just_backup.json");
Path config1 = Path.of("config");
backupIndexPath = config1.resolve("just_backup_index.json");
var configPath = config1.resolve("just_backup.json");
if (Files.notExists(configPath.getParent()))
Files.createDirectories(configPath.getParent());
if (Files.notExists(configPath))
@@ -131,7 +135,7 @@ public class JustBackupMod implements ModInitializer {
private void onBackupComplete(@UnknownNullability Backup backup, @UnknownNullability Throwable throwable) {
if (throwable != null) {
server.getPlayerManager().broadcast(Text.literal("Backup failed. For administrators, please check your server console."), false);
throwable.printStackTrace();
LOGGER.error(throwable.getMessage(), throwable);
} else {
Files.writeString(backupIndexPath, GSON.toJson(tracker.getTrackedBackups()));
server.getPlayerManager().broadcast(Text.literal("Backup success. " + backup), false);
@@ -151,7 +155,7 @@ public class JustBackupMod implements ModInitializer {
var cfg = new JustBackupConfig(
60,
5,
true,
Zstd.defaultCompressionLevel(),
true,
"./.backup_cache",
new StorageOption.Local("backups", 0)
@@ -1,131 +0,0 @@
package io.ib67.sfcraft.bundler;
import com.github.luben.zstd.ZstdOutputStreamNoFinalizer;
import io.ib67.sfcraft.WorldDir;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.experimental.Accessors;
import org.apache.commons.lang3.Validate;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;
@Accessors(fluent = true)
@Data
public class Bundle {
public static final int MAGIC = 0xBACEBACE;
public static final int COMPRESSED_MAGIC = 0xBACEAAAA;
protected final WorldDir dir;
protected boolean compress = true;
protected boolean allowGunzip = true;
protected boolean allowParallel = false;
public Bundle(WorldDir dir) {
this.dir = dir;
}
@SneakyThrows
public static void unbundleFile(Path bundle, Path out) {
try (var bundlerReader = new SerialBundlerReader(bundle)) {
while (bundlerReader.hasEntry()) {
var entry = bundlerReader.readEntry();
var target = out.resolve(entry.fileName());
Files.createDirectories(target.getParent());
bundlerReader.writeTo(FileChannel.open(target, StandardOpenOption.WRITE,
StandardOpenOption.CREATE_NEW), entry);
}
}
}
@SneakyThrows
public void buildBundle(Path backupFile) {
checkCondition();
var sortedEntries = dir.everything().stream().sorted((a, b) -> {
boolean aMatch = a.endsWith(".mca") || a.endsWith(".dat");
boolean bMatch = b.endsWith(".mca") || b.endsWith(".dat");
if (aMatch && !bMatch) return -1;
if (!aMatch && bMatch) return 1;
return a.compareTo(b);
}).map(dir.root()::relativize).toList();
if (Files.notExists(backupFile.getParent()))
Files.createDirectories(backupFile.getParent());
SerialBundlerWriter writer;
if (compress) {
var fout = Files.newOutputStream(backupFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
writeMagic(fout, null, true);
var out = new ZstdOutputStreamNoFinalizer(fout);
writer = new SerialBundlerWriter(out);
} else {
var chan = FileChannel.open(backupFile);
writer = new SerialBundlerWriter(chan, 0);
writeMagic(null, chan, false);
}
// todo parallelism
writeEntries(sortedEntries, writer, allowGunzip && compress);
writer.close();
try (var backup = FileChannel.open(backupFile, StandardOpenOption.WRITE, StandardOpenOption.APPEND);
var bos = new ByteArrayOutputStream();
var metadata = new EntryOutputStream(new ZstdOutputStreamNoFinalizer(bos))) {
var entries = writer.getEntries();
for (BundlerEntry entry : entries) {
metadata.write(entry);
}
metadata.flush();
var result = bos.toByteArray();
var buffer = ByteBuffer.allocate(result.length +4 + 4);
buffer.put(result);
buffer.putInt(entries.size());
buffer.putInt(result.length);
writeFully(backup, buffer);
}
}
private static void writeMagic(OutputStream out, FileChannel channel, boolean compress) throws IOException {
var magic = compress ? Bundle.COMPRESSED_MAGIC : Bundle.MAGIC;
if (channel != null) {
channel.write(ByteBuffer.allocate(4).putInt(magic).flip());
} else {
out.write(ByteBuffer.allocate(4).putInt(magic).array());
}
}
private void writeFully(FileChannel chan, ByteBuffer buf) throws IOException {
buf.flip();
while (buf.hasRemaining()) {
chan.write(buf);
}
}
private void checkCondition() {
if (allowParallel) {
Validate.isTrue(!compress && !allowGunzip);
}
}
private void writeEntries(
List<Path> sortedEntries,
SerialBundlerWriter writer,
boolean allowGunzip) throws IOException {
for (Path sortedEntry : sortedEntries) {
var fileName = sortedEntry.getFileName();
var shouldGunzip = allowGunzip && (fileName.endsWith("mca") || fileName.endsWith("dat"));
if (shouldGunzip) {
writer.writeGunzip(dir.root().resolve(sortedEntry), sortedEntry.toString());
} else {
writer.writePlain(dir.root().resolve(sortedEntry), sortedEntry.toString());
}
}
}
}
@@ -1,9 +0,0 @@
package io.ib67.sfcraft.bundler;
public record BundlerEntry(
String fileName,
boolean unGzipped,
long fileLength,
long offset
) {
}
@@ -1,33 +0,0 @@
package io.ib67.sfcraft.bundler;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
public class EntryInputStream extends FilterInputStream {
/**
* Creates a {@code FilterInputStream}
* by assigning the argument {@code in}
* to the field {@code this.in} so as
* to remember it for later use.
*
* @param in the underlying input stream, or {@code null} if
* this instance is to be created without an underlying stream.
*/
protected EntryInputStream(InputStream in) {
super(in);
}
public BundlerEntry readEntry() throws IOException {
var len = ByteBuffer.wrap(readNBytes(4)).getInt();
var gunzipped = len < 0;
len = Math.abs(len);
var name = new String(readNBytes(len)).intern();
var buf = ByteBuffer.wrap(readNBytes(8 + 8));
var fileLen = buf.getLong();
var fileOffset = buf.getLong();
return new BundlerEntry(name, gunzipped, fileLen, fileOffset);
}
}
@@ -1,33 +0,0 @@
package io.ib67.sfcraft.bundler;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class EntryOutputStream extends FilterOutputStream {
/**
* Creates an output stream filter built on top of the specified
* underlying output stream.
*
* @param out the underlying output stream to be assigned to
* the field {@code this.out} for later use, or
* {@code null} if this instance is to be
* created without an underlying stream.
*/
public EntryOutputStream(OutputStream out) {
super(out);
}
public void write(BundlerEntry entry) throws IOException {
var fileName = entry.fileName().getBytes(StandardCharsets.UTF_8);
var buf = ByteBuffer.allocate(4).putInt(entry.unGzipped() ? fileName.length * -1 : fileName.length);
out.write(buf.array());
out.write(fileName);
buf = ByteBuffer.allocate(8 + 8);
buf.putLong(entry.fileLength());
buf.putLong(entry.offset());
out.write(buf.array());
}
}
@@ -1,56 +0,0 @@
package io.ib67.sfcraft.bundler;
import io.ib67.kiwi.ArgOpts;
import io.ib67.sfcraft.WorldDir;
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) {
var opts = ArgOpts.builder()
.args(args).description("Tools for .jpack format bundle").programName("jpack").build();
var in = opts.string("in", "Input. Can be directory or jpack bundle", null);
var out = opts.string("out", "Output. Can be jpack bundle or directory", null);
var help = opts.bool("help", false);
var allowGunzip = opts.bool("gunzip",
"Should we recompress NBT files for maximum compression rate (depends on compress)", true);
var compress = opts.bool("compress", "Should we compress?", true);
var parallel = opts.bool("parallel", "Use parallelized bundler. This conflicts with compress options", false);
if (help) {
opts.printHelp(System.out);
return;
}
if (allowGunzip && !compress) {
System.out.println("allowGunzip depends on compress feature.");
return;
}
if (in == null || out == null) {
System.out.println("Both in and out should be provided");
return;
}
var inPath = Path.of(in);
var outPath = Path.of(out);
if (!Files.exists(inPath)) {
System.out.println("The input isn't exists.");
return;
}
if (Files.exists(outPath)) {
System.out.println("The output is already exists. Please move it to other places");
return;
}
if (in.endsWith(".jpack")) {
Bundle.unbundleFile(inPath, outPath);
} else if (out.endsWith(".jpack")) {
if (!Files.isDirectory(inPath))
throw new IllegalArgumentException("Your input should be a world directory.");
new Bundle(new WorldDir(inPath))
.allowGunzip(allowGunzip)
.compress(compress)
.allowParallel(parallel)
.buildBundle(outPath);
} else {
System.out.println("Invalid arguments. You should provide a pair of jpack and directory to act as in/output.");
}
}
}
@@ -1,102 +0,0 @@
package io.ib67.sfcraft.bundler;
import com.github.luben.zstd.Zstd;
import com.github.luben.zstd.ZstdInputStreamNoFinalizer;
import lombok.SneakyThrows;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
public class SerialBundlerReader implements Closeable {
protected final InputStream in;
protected final FileChannel inChannel; // for direct copy
protected final EntryInputStream metadataIn;
protected final byte[] buffer = new byte[4096];
protected final RandomAccessFile raf;
protected boolean entryRead;
protected int entries;
// public SerialBundlerReader(InputStream in, InputStream metadataIn) {
// this.in = Objects.requireNonNull(in);
// this.metadataIn = new EntryInputStream(Objects.requireNonNull(metadataIn));
// inChannel = null;
// }
public SerialBundlerReader(Path path) throws IOException {
raf = new RandomAccessFile(path.toFile(), "r");
var totalLen = Files.size(path);
var magic = raf.readInt();
var compressed = magic == Bundle.COMPRESSED_MAGIC;
if (!compressed && magic != Bundle.MAGIC) throw new IOException("Invalid bundle file");
// read length
raf.seek(totalLen - 4);
var metadataLength = raf.readInt();
raf.seek(totalLen - 8);
entries = raf.readInt();
// read metadata
var metadataBegin = totalLen - 8 - metadataLength;
var metadataBytes = new byte[metadataLength];
raf.seek(metadataBegin);
raf.readFully(metadataBytes);
this.metadataIn = new EntryInputStream(new ZstdInputStreamNoFinalizer(new ByteArrayInputStream(metadataBytes)));
// check compressed
raf.seek(4);
var fis = Channels.newInputStream(raf.getChannel());
if (compressed) {
in = new ZstdInputStreamNoFinalizer(fis);
inChannel = null;
} else {
in = fis;
inChannel = raf.getChannel();
}
}
@SneakyThrows
public boolean hasEntry() {
return entries > 0;
}
public BundlerEntry readEntry() throws IOException {
if (!hasEntry()) throw new IOException("All entries are read");
var entry = metadataIn.readEntry();
entryRead = true;
entries--;
return entry;
}
public void writeTo(FileChannel channel, BundlerEntry entry) throws IOException {
if (inChannel != null) {
inChannel.transferTo(entry.offset(), entry.fileLength(), channel);
} else {
writeTo(Channels.newOutputStream(channel), entry);
}
}
public void writeTo(OutputStream out, BundlerEntry entry) throws IOException {
Objects.requireNonNull(entry);
var remaining = entry.fileLength();
var buffer = this.buffer;
var totalRead = 0;
do {
var read = in.read(buffer, 0, (int) Math.min(remaining - totalRead, buffer.length));
out.write(buffer, 0, read);
totalRead += read;
} while (totalRead < remaining);
}
@Override
public void close() throws IOException {
in.close();
metadataIn.close();
raf.close();
}
}
@@ -1,83 +0,0 @@
package io.ib67.sfcraft.bundler;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.zip.GZIPInputStream;
@AllArgsConstructor
public class SerialBundlerWriter implements Closeable {
protected final FileChannel channel;
protected final OutputStream out;
protected long offset;
@Getter
protected List<BundlerEntry> entries;
public SerialBundlerWriter(OutputStream out) {
Objects.requireNonNull(out);
this.out = out;
this.entries = new ArrayList<>();
channel = null;
}
public SerialBundlerWriter(FileChannel channel, long offset) {
Objects.requireNonNull(channel);
this.offset = offset;
this.entries = new ArrayList<>();
this.channel = channel;
out = null;
}
public void writeGunzip(Path path, String fileName) throws IOException {
try (var gzip = new GZIPInputStream(Files.newInputStream(path, StandardOpenOption.READ))) {
var len = channel != null ? gzip.transferTo(Channels.newOutputStream(channel)) :
gzip.transferTo(out);
entries.add(new BundlerEntry(fileName, true, len, offset));
offset += len;
}
}
public void writePlain(Path path, String fileName) throws IOException {
if (channel != null) {
writeZC(path, fileName);
} else {
var current = offset;
try (var in = Files.newInputStream(path)) {
in.transferTo(out);
}
entries.add(new BundlerEntry(fileName, false, Files.size(path), current));
}
}
private void writeZC(Path path, String fileName) throws IOException {
try (var in = FileChannel.open(path, StandardOpenOption.READ)) {
var begin = offset;
var currentPosition = offset;
long transferred = 0;
while ((transferred = in.transferTo(currentPosition, in.size() - transferred, channel)) != 0) {
currentPosition += transferred;
}
offset = currentPosition;
entries.add(new BundlerEntry(
fileName, false, offset - begin, begin
));
}
}
@Override
public void close() throws IOException {
if (out != null) out.close();
if (channel != null) channel.close();
}
}
@@ -7,14 +7,15 @@ import java.util.List;
public record JustBackupConfig(
long backupIntervalMinutes,
int keepBackups,
int compressionLevel,
boolean allowGunzip,
boolean compress,
String temporaryBackupDir,
StorageOption option
) {
public JustBackupConfig {
Validate.isTrue(backupIntervalMinutes > 0, "backup interval must be positive");
Validate.isTrue(keepBackups > 0, "keep backups must be positive");
Validate.isTrue(compressionLevel > 0, "compression level must be positive");
Validate.notNull(option, "option cannot be null");
}
}
@@ -1,5 +1,6 @@
package io.ib67.sfcraft.mixin;
import io.ib67.sfcraft.IOState;
import io.ib67.sfcraft.JustBackupMod;
import lombok.Getter;
import net.minecraft.server.MinecraftServer;
@@ -16,18 +17,30 @@ import java.util.concurrent.atomic.AtomicBoolean;
@Mixin(MinecraftServer.class)
public abstract class MixinMinecraftServer {
@Shadow private int ticksUntilAutosave;
@Shadow
private int ticksUntilAutosave;
@Shadow protected abstract int getAutosaveInterval();
@Shadow
protected abstract int getAutosaveInterval();
@Inject(method = "runAutosave", at = @At("HEAD"), cancellable = true)
private void backup$checkSaving(CallbackInfo ci) {
// is already backing up
this.ticksUntilAutosave = getAutosaveInterval();
if (!JustBackupMod.PERFORMING_BACKUP.compareAndSet(false, true)) ci.cancel();
IOState witness;
while ((witness = JustBackupMod.BACKUP_LOCK.compareAndExchange(IOState.IDLE, IOState.SAVING_WORLD)) == IOState.STORAGE_SYNC) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.err.println("Error while waiting for chunk sync. " + e);
ci.cancel();
}
}
if (witness == IOState.BACKUP) ci.cancel();
}
@Inject(method="runAutosave", at=@At("RETURN"))
private void backup$disableProtect(CallbackInfo ci){
JustBackupMod.PERFORMING_BACKUP.set(false);
@Inject(method = "runAutosave", at = @At("RETURN"))
private void backup$disableProtect(CallbackInfo ci) {
JustBackupMod.BACKUP_LOCK.setRelease(IOState.IDLE);
}
}
@@ -0,0 +1,56 @@
package io.ib67.sfcraft.mixin;
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;
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 {
@Shadow
@Final
private Long2ObjectLinkedOpenHashMap<RegionFile> cachedRegionFiles;
/**
* @author iceBear67
* @reason to wrap the saving process with an exception handler.
*/
@Overwrite
@SneakyThrows
public void sync() {
// allowed states:
// STORAGE_SYNC, AUTO_SAVE, IDLE
// we try
var witness = JustBackupMod.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) {
Thread.sleep(2000);
}
}
// program point: IDLE, AUTOSAVE, STORAGESYNC
try {
for (RegionFile value : this.cachedRegionFiles.values()) {
value.sync();
}
} finally {
// 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);
}
}
}
@@ -1,8 +1,6 @@
package io.ib67.sfcraft.strategy;
import io.ib67.sfcraft.Backup;
import io.ib67.sfcraft.WorldDir;
import io.ib67.sfcraft.bundler.Bundle;
import java.nio.file.Path;
@@ -1,26 +1,15 @@
package io.ib67.sfcraft.strategy;
import com.github.luben.zstd.Zstd;
import com.github.luben.zstd.ZstdOutputStreamNoFinalizer;
import io.ib67.sfcraft.Backup;
import io.ib67.sfcraft.WorldDir;
import io.ib67.sfcraft.bundler.*;
import io.ib67.sfcraft.bundler.BundleReader;
import io.ib67.sfcraft.config.StorageOption;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.Validate;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
@Log4j2
public class LocalBackupStrategy implements BackupStrategy {
@@ -51,7 +40,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");
Bundle.unbundleFile(backupFile, restorePath);
BundleReader.extract(backupFile, restorePath);
}
@SneakyThrows
@@ -1,7 +1,7 @@
package io.ib67.sfcraft.strategy;
import io.ib67.sfcraft.Backup;
import io.ib67.sfcraft.bundler.Bundle;
import io.ib67.sfcraft.bundler.BundleReader;
import io.ib67.sfcraft.config.StorageOption;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
@@ -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);
Bundle.unbundleFile(target, restorePath);
BundleReader.extract(target, restorePath);
} finally {
Files.deleteIfExists(target);
}