diff --git a/build.gradle b/build.gradle index 8b3145b..867efaf 100644 --- a/build.gradle +++ b/build.gradle @@ -67,13 +67,15 @@ dependencies { shadow project(":bundler") shadow 'software.amazon.awssdk:s3:2.34.0' } - configurations.implementation.extendsFrom(configurations.shadow) shadowJar { configurations = [project.configurations.shadow] exclude("META-INF") dependencies { exclude(dependency("software.amazon.awssdk:aws-xml-protocol:.*")) + exclude(dependency("org.lz4:lz4-java:.*")) + exclude(dependency("it.unimi.dsi:fastutil:.*")) + exclude(dependency("io.netty:netty-all:.*")) } minimize() //relocate 'com.google', 'sfcraft.relocation.google' diff --git a/bundler/build.gradle b/bundler/build.gradle index 2ef49dc..88fb217 100644 --- a/bundler/build.gradle +++ b/bundler/build.gradle @@ -8,18 +8,19 @@ plugins { dependencies { api 'com.github.luben:zstd-jni:1.5.7-4' - compileOnly 'io.netty:netty-all:4.2.6.Final' + testCompileOnly 'io.netty:netty-all:4.2.6.Final' api "io.ib67.kiwi:lang:1.2.3-SNAPSHOT" - compileOnly 'io.netty:netty-all:4.2.6.Final' - compileOnly 'it.unimi.dsi:fastutil:8.5.15' - compileOnly 'org.lz4:lz4-java:1.8.0' - compileOnly 'org.lz4:lz4-java:1.8.0' + implementation 'io.netty:netty-all:4.2.6.Final' + implementation 'it.unimi.dsi:fastutil:8.5.15' + implementation 'org.lz4:lz4-java:1.8.0' testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } -shadowJar { - minimize() +configurations { + testRuntimeOnly { + extendsFrom compileOnly + } } test { diff --git a/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleReader.java b/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleReader.java index 72e9c90..534037e 100644 --- a/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleReader.java +++ b/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleReader.java @@ -62,12 +62,18 @@ public class BundleReader { entryData.writeBytes(buffer, 0, read); } try (var reassembler = new RegionParser(entryData, allocator)) { - var result = reassembler.writeReassembled(RegionFile.CompressType.GZIP); + var result = reassembler.writeReassembled(RegionFile.CompressType.ZLIB); try (var fc = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) { result.readBytes(fc, 0, result.readableBytes()); } finally { result.release(); } + } catch (Exception e) { + try (var ch = FileChannel.open(Path.of("dump.mca"), StandardOpenOption.CREATE, StandardOpenOption.WRITE)) { + entryData.readerIndex(0); + entryData.readBytes(ch, 0, entryData.readableBytes()); + } + throw new IOException("Error while reassembling " + entry.name(), e); } } else { writePlain(path, entry, in); diff --git a/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleWriter.java b/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleWriter.java index 670b4a5..ff22d1f 100644 --- a/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleWriter.java +++ b/bundler/src/main/java/io/ib67/sfcraft/bundler/BundleWriter.java @@ -75,7 +75,7 @@ public class BundleWriter implements Closeable { var futures = new ArrayList>(); for (int i = 0; i < parted.size(); i++) { var pathList = parted.get(i); - var output = parentOfBundle.resolve("Worker_" + System.currentTimeMillis() + "_" + i + ".jbp.zst"); + var output = parentOfBundle.resolve("Worker_" + System.currentTimeMillis() + "_" + i + ".swb.zst"); var future = CompletableFuture.supplyAsync(() -> createBundleWorker(pathList, output, config), executor); futures.add(future); } @@ -142,7 +142,7 @@ public class BundleWriter implements Closeable { } public void write(Path path, boolean suggestGunzip) throws IOException { - if (suggestGunzip) { + if (suggestGunzip && allowReassemble) { //todo size limit writeReassembleInMem(path); return; @@ -158,22 +158,26 @@ public class BundleWriter implements Closeable { return; } if(verbose) System.out.println("Reassembling " + path); - var uncompressed = allocator.buffer(); + var finalResult = allocator.buffer(); var rPath = relativeRoot.toAbsolutePath().relativize(path.toAbsolutePath()); - try (var out = new EntryOutputStream(new ByteBufOutputStream(uncompressed)); + try (var out = new EntryOutputStream(new ByteBufOutputStream(finalResult)); var rf = new RegionParser(path, allocator)) { + // uncompressed result var result = rf.writeReassembled(RegionFile.CompressType.NONE); var length = result.readableBytes(); out.writeEntryHeader(rPath.toString(), BundleEntry.ATTR_REASSEMBLE, length, System.currentTimeMillis()); result.readBytes(out, length); result.release(); - uncompressed.readBytes(outputStream, uncompressed.readableBytes()); + // all done } catch (IOException e) { System.err.println("Failed to parse region " + path + ": " + e); + finalResult.release(); writePlain(path); - } finally { - uncompressed.release(); + return; } + finalResult.readBytes(outputStream, finalResult.readableBytes()); + finalResult.release(); + } private void writePlain(Path path) throws IOException { @@ -186,6 +190,9 @@ public class BundleWriter implements Closeable { while ((read = fs.read(buffer)) > 0) { outputStream.write(buffer, 0, read); } + }catch(IOException e){ + System.err.println("Error while bundling "+path); + throw e; } } diff --git a/bundler/src/main/java/io/ib67/sfcraft/bundler/Main.java b/bundler/src/main/java/io/ib67/sfcraft/bundler/Main.java index 3654ee5..fb8e501 100644 --- a/bundler/src/main/java/io/ib67/sfcraft/bundler/Main.java +++ b/bundler/src/main/java/io/ib67/sfcraft/bundler/Main.java @@ -26,7 +26,7 @@ public class Main { @SneakyThrows public static void main(String[] args) { var opts = ArgOpts.builder() - .args(args).description("Tools for .jbp.zst format bundle").programName("jpack").build(); + .args(args).description("Tools for .swb.zst 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); @@ -81,9 +81,9 @@ public class Main { return; } - if (in.endsWith(".jbp.zst")) { + if (in.endsWith(".swb.zst")) { BundleReader.builder().build().extract(inPath, outPath); - } else if (out.endsWith(".jbp.zst")) { + } else if (out.endsWith(".swb.zst")) { UnaryOperator cfg = it -> it .allowGunzip(allowGunzip) .relativeRoot(inPath) diff --git a/bundler/src/main/java/io/ib67/sfcraft/bundler/region/RegionFile.java b/bundler/src/main/java/io/ib67/sfcraft/bundler/region/RegionFile.java index 9c8d875..378b91c 100644 --- a/bundler/src/main/java/io/ib67/sfcraft/bundler/region/RegionFile.java +++ b/bundler/src/main/java/io/ib67/sfcraft/bundler/region/RegionFile.java @@ -89,11 +89,14 @@ public class RegionFile implements Closeable { } public ByteBuf writeOutput() { - var sect = allocator.buffer(8192); + var sect = allocator.buffer(4096); + sect.setZero(0, 4096); + sect.writerIndex(4096); var compositeByteBuf = allocator.compositeBuffer(2 + regionData.size()); var beginSector = 2; compositeByteBuf.addComponent(true, sect); timestamp.retain(); + timestamp.readerIndex(0); compositeByteBuf.addComponent(true, timestamp); for (var entry : regionData.short2ObjectEntrySet()) { var chunkKey = entry.getShortKey(); @@ -102,12 +105,21 @@ public class RegionFile implements Closeable { var chunk = entry.getValue(); var size = chunk.readableBytes(); var sizeSectors = Math.ceilDiv(size, 4096); + var zeros = 4096 - (size % 4096); + for (int i = 0; i < zeros; i++) { + chunk.writeByte(0); + } if (sizeSectors > 255) { throw new IllegalArgumentException("MCC not supported."); } - var indexEntry = Integer.reverseBytes(beginSector) << 1 | (sizeSectors & 0xFF); - // sect = int[1024] - sect.setInt((x + 32 * z) * 4, indexEntry); + if(sizeSectors < 0) { + throw new IllegalArgumentException("Invalid sizeSectors: "+sizeSectors); + } + var begin = (x + 32 * z) * 4; + sect.setByte(begin, (beginSector >>> 16) & 0xFF); + sect.setByte(begin+1, (beginSector >>> 8) & 0xFF); + sect.setByte(begin+2, beginSector & 0xFF); + sect.setByte(begin+3, sizeSectors & 0xFF); chunk.retain(); // retain a local reference compositeByteBuf.addComponent(true, chunk); beginSector += sizeSectors; diff --git a/bundler/src/main/java/io/ib67/sfcraft/bundler/region/RegionParser.java b/bundler/src/main/java/io/ib67/sfcraft/bundler/region/RegionParser.java index b9063e0..ea527d9 100644 --- a/bundler/src/main/java/io/ib67/sfcraft/bundler/region/RegionParser.java +++ b/bundler/src/main/java/io/ib67/sfcraft/bundler/region/RegionParser.java @@ -22,12 +22,12 @@ public class RegionParser implements Closeable { this.allocator = allocator; var size = (int) Files.size(file); if (size < 8192) throw new IOException("The region file " + file + " has been truncated"); - if(size > 8388608 * 2) throw new IOException("The region file " + file + " is too large"); //todo bug + if (size > 8388608 * 2) throw new IOException("The region file " + file + " is too large"); //todo bug source = allocator.buffer(size); try (var fc = FileChannel.open(file, StandardOpenOption.READ)) { var toRead = size; - while(toRead > 0){ + while (toRead > 0) { toRead -= source.writeBytes(fc, 0L, (int) size); } } @@ -66,6 +66,7 @@ public class RegionParser implements Closeable { if (compressType < 0) { // mcc todo } + if (sectorLen == 0) return compressType; buf.writeBytes(source, sectorLen - 1); return compressType; } diff --git a/bundler/src/test/java/io/ib67/bundler/TestRegionReassembler.java b/bundler/src/test/java/io/ib67/bundler/TestRegionReassembler.java index dfde0c6..e90cf8a 100644 --- a/bundler/src/test/java/io/ib67/bundler/TestRegionReassembler.java +++ b/bundler/src/test/java/io/ib67/bundler/TestRegionReassembler.java @@ -7,16 +7,32 @@ import io.netty.buffer.Unpooled; import org.junit.jupiter.api.Test; import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; public class TestRegionReassembler { @Test public void testReassembler() throws IOException { + testMethod("region/r.9.-5.mca"); + testMethod("region/r.-1.-1.mca"); + } + + private static void testMethod(String key) throws IOException { byte[] bytes; - try(var in = TestRegionReassembler.class.getClassLoader().getResourceAsStream("region/r.9.-5.mca")){ + try(var in = TestRegionReassembler.class.getClassLoader().getResourceAsStream(key)){ bytes = in.readAllBytes(); } try(var reasm = new RegionParser(Unpooled.wrappedBuffer(bytes), ByteBufAllocator.DEFAULT)) { var result = reasm.writeReassembled(RegionFile.CompressType.NONE); + try(var foc = FileChannel.open(Path.of("./test.mca"), StandardOpenOption.CREATE, StandardOpenOption.WRITE)) { + result.readBytes(foc, 0, result.readableBytes()); + } + result.readerIndex(0); + try(var reader = new RegionFile(ByteBufAllocator.DEFAULT)) { + result = reasm.writeReassembled(RegionFile.CompressType.ZLIB); + } } } } diff --git a/bundler/src/test/resources/region/r.-1.-1.mca b/bundler/src/test/resources/region/r.-1.-1.mca new file mode 100644 index 0000000..ae1df05 Binary files /dev/null and b/bundler/src/test/resources/region/r.-1.-1.mca differ diff --git a/src/main/java/io/ib67/sfcraft/Backup.java b/src/main/java/io/ib67/sfcraft/Backup.java index 306f484..28982c5 100644 --- a/src/main/java/io/ib67/sfcraft/Backup.java +++ b/src/main/java/io/ib67/sfcraft/Backup.java @@ -11,7 +11,7 @@ public record Backup( long sizeTotal ) { public Text toText(){ - return Text.of("sizeTotal: "+((sizeTotal / 1024.0) / 1024.0) + "GiB, "+ + return Text.of("sizeTotal: "+Math.round(((sizeTotal / 1024.0) / 1024.0) * 100) * 0.01 + "MiB, "+ (incremental ? "incremental" :"") + ", type: "+type+"." ); } diff --git a/src/main/java/io/ib67/sfcraft/BackupCommands.java b/src/main/java/io/ib67/sfcraft/BackupCommands.java index f5b9b51..50b5fde 100644 --- a/src/main/java/io/ib67/sfcraft/BackupCommands.java +++ b/src/main/java/io/ib67/sfcraft/BackupCommands.java @@ -7,6 +7,7 @@ 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.bundler.BundleReader; import lombok.extern.log4j.Log4j2; import net.minecraft.command.CommandRegistryAccess; import net.minecraft.server.command.CommandManager; @@ -15,7 +16,10 @@ import net.minecraft.text.ClickEvent; import net.minecraft.text.Text; import java.awt.*; +import java.io.IOException; import java.lang.ref.WeakReference; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -33,10 +37,10 @@ public class BackupCommands { " -- Show this message", " /backup list ", " -- List backups", - " /backup restore ", + " /backup restore ", " -- Restore the backup on next server restart.", " Can be cancelled by /backup cancelrestore", - " /backup delete ", + " /backup delete ", " -- Delete a backup. This does not ask for a confirmation.", " /backup create ", " -- Create a backup.", @@ -61,11 +65,11 @@ public class BackupCommands { .then(literal("list").executes(this::cmdList)) .then(literal("cancelrestore").executes(this::cmdCancelRestore)) .then(literal("delete") - .then(argument("backupName", StringArgumentType.greedyString()) + .then(argument("backupKey", StringArgumentType.greedyString()) .suggests(this::suggestBackups) .executes(this::cmdDelete))) .then(literal("restore") - .then(argument("backupName", StringArgumentType.greedyString()) + .then(argument("backupKey", StringArgumentType.greedyString()) .suggests(this::suggestBackups) .executes(this::cmdRestore))) .then(literal("create") @@ -128,19 +132,49 @@ public class BackupCommands { s.sendMessage(Text.of("TIP: Use `/backup cancelrestore` to cancel that request.")); return 0; } - var backupName = StringArgumentType.getString(context, "backupName"); + var backupKey = StringArgumentType.getString(context, "backupKey"); + var backup = mod.tracker.getTrackedBackups().get(backupKey); + if (backup == null) { + s.sendMessage(Text.of("Invalid backup " + backupKey + ". Note: Use backupKey instead of backupName!")); + return 0; + } restoreIssued = () -> { - //todo implementation + log.warn("EXTRACTING BACKUP " + backup + " TO " + backup.from()); + log.warn(" == STEP 1 == Make a backup for the destination."); + var destination = Path.of(backup.from()); + var movedDst = destination.resolveSibling(destination.getFileName().toString()+"_bak_"+System.currentTimeMillis()); + try{ + Files.move(destination, movedDst); + log.info("{} has been moved to {}", destination, movedDst); + } catch (IOException e) { + log.error(e); + log.error("CANNOT MOVE "+destination+" TO "+movedDst); + log.error("BACKUP INTERRUPTED. Here are some tips helping you out:"); + log.error(" -- A. Merge already moved files"); + log.error(" Try this command in your server directory: (linux)"); + log.error(" $ cp ./"+movedDst+"/* ./"+destination); + log.error(" -- B. Remove them all and use external unbundler tools"); + log.error(" You may want to copy them elsewhere first (see kind A)"); + log.error(" $ rm -r ./"+destination+" ./"+movedDst); + log.error(" Visit https://github.com/saltedfishclub/justbackup for the usage of bundler tools"); + log.error(" ------- ERROR END -------"); + return; + } + log.info(" == STEP 2 == Recovering backup "+backup); + mod.tracker.recoverBackup(backup, destination); + log.info("Backup has been recovered."); }; + s.sendMessage(Text.of("Backup restoration task has been scheduled!")); + s.sendMessage(Text.of("Restart your server to take changes.")); return Command.SINGLE_SUCCESS; } private int cmdDelete(CommandContext context) { - var backupName = StringArgumentType.getString(context, "backupName"); - var backup = mod.tracker.getTrackedBackups().get(backupName); + var backupKey = StringArgumentType.getString(context, "backupKey"); + var backup = mod.tracker.getTrackedBackups().get(backupKey); var source = context.getSource(); if (backup == null) { - source.sendMessage(Text.of("Invalid backup. No backups are named '" + backupName + "'")); + source.sendMessage(Text.of("Invalid backup. No backups are named '" + backupKey + "'")); return 0; } try { @@ -194,6 +228,8 @@ public class BackupCommands { for (String s : helpMessage) { if (s.startsWith(" /")) { source.sendMessage(Text.literal(s).withColor(Color.CYAN.getRGB())); + } else { + source.sendMessage(Text.of(s)); } } return Command.SINGLE_SUCCESS; diff --git a/src/main/java/io/ib67/sfcraft/BackupWorker.java b/src/main/java/io/ib67/sfcraft/BackupWorker.java index a1399bf..1bb680a 100644 --- a/src/main/java/io/ib67/sfcraft/BackupWorker.java +++ b/src/main/java/io/ib67/sfcraft/BackupWorker.java @@ -33,19 +33,14 @@ public record BackupWorker( if (server == null) { throw new IllegalStateException("MinecraftServer is not initialized yet, not doing backup"); } - int i = 0; + server.saveAll(true, true, false); 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 tmpBundle = tmpDir.resolve("backup_" + subject + "_" + System.currentTimeMillis() + ".swb.zst"); + var tmpBundle = tmpDir.resolve("backup_" + subject.name() + "_" + System.currentTimeMillis() + ".swb.zst"); try { - if (i == 0) { - // not saved yet. - server.saveAll(true, true, false); - } // tmp file path log.info("Creating bundle {}", tmpBundle); BundleWriter.createBundle(tmpBundle, subject.changedFiles(), config); diff --git a/src/main/java/io/ib67/sfcraft/FileWatcherThread.java b/src/main/java/io/ib67/sfcraft/FileWatcherThread.java index 68c13cf..4ea8b28 100644 --- a/src/main/java/io/ib67/sfcraft/FileWatcherThread.java +++ b/src/main/java/io/ib67/sfcraft/FileWatcherThread.java @@ -1,8 +1,10 @@ package io.ib67.sfcraft; import lombok.SneakyThrows; +import lombok.extern.log4j.Log4j2; import java.nio.file.*; +import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -10,6 +12,7 @@ import java.util.function.BiConsumer; import java.util.stream.Collectors; import java.util.stream.Stream; +@Log4j2 public class FileWatcherThread extends Thread { protected final Map watchSubject; protected final BiConsumer eventListener; @@ -20,17 +23,24 @@ public class FileWatcherThread extends Thread { ) { this.watchSubject = watchSubject.entrySet() .stream() - .flatMap(it -> walkThrough(it.getValue()).map(r -> Map.entry(r, it.getKey()))) + // todo support for nonexist dir + .flatMap(it -> walkThrough(it.getValue()).map(r -> Map.entry(r.toAbsolutePath(), it.getKey()))) + .filter(it->Files.isDirectory(it.getKey())) .collect(Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue )); this.eventListener = eventListener; setName("JustBackup/FileWatcher"); + setDaemon(true); } @SneakyThrows private static Stream walkThrough(Path path) { + if(Files.notExists(path)){ + log.error("{} not found. Creating directory for it..", path); + Files.createDirectories(path); + } return Files.walk(path); } @@ -38,12 +48,14 @@ public class FileWatcherThread extends Thread { @SneakyThrows public void run() { try (var watcher = FileSystems.getDefault().newWatchService()) { + var keyToParent = new HashMap(); for (Path value : watchSubject.keySet()) { - value.register( + var key = value.register( watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY ); + keyToParent.put(key, value); } for (; ; ) { var key = watcher.poll(10, TimeUnit.SECONDS); @@ -53,17 +65,25 @@ public class FileWatcherThread extends Thread { var kind = ev.kind(); if (kind == StandardWatchEventKinds.OVERFLOW) continue; - var path = ((WatchEvent) ev).context(); - var subject = Objects.requireNonNull(watchSubject.get(path.getParent())); + var parent = keyToParent.get(key); + var subject = watchSubject.get(parent); + if(subject == null) { + log.warn("Ignoring changes in "+parent); + continue; + } + var path = parent.resolve(((WatchEvent) ev).context()); if (Files.isRegularFile(path)) { - eventListener.accept(subject, path.getParent()); + eventListener.accept(subject, parent); } else if (Files.isDirectory(path) && kind == StandardWatchEventKinds.ENTRY_CREATE) { - path.register(watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY); + var nK = path.register(watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY); watchSubject.put(path, subject); + keyToParent.put(nK, path); } } key.reset(); } + }catch (InterruptedException interruptedException){ + log.info("FileWatcherThread is quitting!"); } } } diff --git a/src/main/java/io/ib67/sfcraft/JustBackupMod.java b/src/main/java/io/ib67/sfcraft/JustBackupMod.java index 48bb07b..9f4d6bc 100644 --- a/src/main/java/io/ib67/sfcraft/JustBackupMod.java +++ b/src/main/java/io/ib67/sfcraft/JustBackupMod.java @@ -30,13 +30,13 @@ 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.stream.Collectors; public class JustBackupMod implements ModInitializer { public static final String MOD_ID = "justbackup"; public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); protected final Map backupSubjects = new HashMap<>(); + protected int lastBackupServerTicks; protected volatile MinecraftServer server; protected volatile boolean suspend; protected byte[] zstdDict; @@ -63,7 +63,7 @@ public class JustBackupMod implements ModInitializer { config.backupSubjects().forEach((k, v) -> { var subjectPath = Path.of(v); if (Files.notExists(subjectPath)) { - LOGGER.error("Backup subject {} does not exist.", v); + LOGGER.error("Backup _subject {} does not exist.", v); return; } backupSubjects.put(k, new BackupSubject(k, subjectPath, new ArrayList<>())); @@ -93,9 +93,11 @@ public class JustBackupMod implements ModInitializer { watcherThread.start(); } - private void handleFileChanges(String subject, Path path) { + private void handleFileChanges(String _subject, Path path) { synchronized (backupSubjects) { - backupSubjects.get(subject).changedFiles().add(path); + var subject = backupSubjects.get(_subject); + if (subject != null) + subject.changedFiles().add(path); // check null } } @@ -108,6 +110,12 @@ public class JustBackupMod implements ModInitializer { backupAll(false); } scheduledBackupExecutor.scheduleAtFixedRate(() -> { + if (lastBackupServerTicks > 0 && lastBackupServerTicks == server.getTicks()) { + // the server is idling + LOGGER.debug("Server is idling, not backing up."); + return; + } + lastBackupServerTicks = server.getTicks(); if (!suspend) backupAll(config.incremental()).thenAccept(it -> { LOGGER.info("Backup success!"); it.forEach((k, v) -> LOGGER.info(k + ": " + v)); @@ -125,6 +133,7 @@ public class JustBackupMod implements ModInitializer { var command = new BackupCommands(this); CommandRegistrationCallback.EVENT.register(command::registerCommand); ServerLifecycleEvents.SERVER_STOPPED.register(server -> { + LOGGER.info("Shutting down the executor"); scheduledBackupExecutor.shutdown(); if (command.restoreIssued != null) { command.restoreIssued.run(); @@ -134,6 +143,7 @@ public class JustBackupMod implements ModInitializer { } public CompletableFuture> backupAll(boolean incremental) { + if (backupSubjects.isEmpty()) return CompletableFuture.completedFuture(Map.of()); var map = new HashMap>(); server.getPlayerManager().broadcast( Text.literal(" BACKUP >> ").withColor(Color.RED.getRGB()).styled(it -> it.withBold(true)) @@ -146,7 +156,11 @@ public class JustBackupMod implements ModInitializer { return CompletableFuture.allOf(map.values().toArray(new CompletableFuture[0])) .thenApply(it -> { var _map = new HashMap(); - map.forEach((k, v) -> _map.put(k, v.join())); + map.forEach((k, v) -> { + var result = v.join(); + LOGGER.info("Backup information of {}: {}", k, result); + _map.put(k, result); + }); return _map; }); } @@ -154,15 +168,18 @@ public class JustBackupMod implements ModInitializer { public CompletableFuture issueBackup(String subjectName, boolean incremental) { var subject = backupSubjects.get(subjectName); if (subject == null) - return CompletableFuture.failedFuture(new IllegalArgumentException("Unknown backup subject: " + subjectName)); + 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()); + subject = new BackupSubject(subject.name(), subject.root(), s.filter(Files::isRegularFile) + .filter(it -> !it.getFileName().toString().equals("session.lock")) + .toList()); } catch (IOException e) { return CompletableFuture.failedFuture(new IllegalArgumentException("Cannot perform full backup for " + subjectName, e)); } } var finalSubject = subject; + if (config.allowGunzip()) LOGGER.info("Chunk reassembler is enabled!"); var worker = new BackupWorker(server, tracker, Path.of(config.temporaryBackupDir()), subject, c -> c.allowGunzip(config.allowGunzip()) diff --git a/src/main/java/io/ib67/sfcraft/strategy/LocalBackupStrategy.java b/src/main/java/io/ib67/sfcraft/strategy/LocalBackupStrategy.java index 139b4ee..5e104ee 100644 --- a/src/main/java/io/ib67/sfcraft/strategy/LocalBackupStrategy.java +++ b/src/main/java/io/ib67/sfcraft/strategy/LocalBackupStrategy.java @@ -26,14 +26,15 @@ public class LocalBackupStrategy implements BackupStrategy { @Override @SneakyThrows public Backup createBackup(String subject, Path from, Path pathToBundle) { + var parent = backupParentRoot.resolve(subject); var result = new Backup( pathToBundle.getFileName().toString(), - pathToBundle.getFileName().toString(), + Path.of(subject).resolve(pathToBundle.getFileName()).normalize().toString(), "local", from.toString(), false, Files.size(pathToBundle)); - var parent = backupParentRoot.resolve(subject); if(Files.notExists(parent)) { Files.createDirectories(parent); } + //todo new name Files.move(pathToBundle, parent.resolve(pathToBundle.getFileName())); return result; } diff --git a/src/main/resources/justbackup.mixins.json b/src/main/resources/justbackup.mixins.json index 6d4047c..50ccb18 100644 --- a/src/main/resources/justbackup.mixins.json +++ b/src/main/resources/justbackup.mixins.json @@ -3,7 +3,6 @@ "package": "io.ib67.sfcraft.mixin", "compatibilityLevel": "JAVA_21", "mixins": [ - "LevelStorageAccessor", "MixinMinecraftServer", "MixinRegionBasedStorage" ],