From 720e8ec8eeb69d24afbb6b14068563cde2d470f0 Mon Sep 17 00:00:00 2001 From: Keuin Date: Thu, 23 Apr 2020 14:51:29 +0800 Subject: Version 1.1.0-dev: - Optimized backup lag (using async I/O). - Added twice confirmation /kb confirm and cancellation /kb cancel, to avoid mistake. - Added countdown before restoring. - Adjusted some text. - Code optimization. --- .../keuin/kbackupfabric/worker/BackupWorker.java | 92 ++++++++++++++++++++++ .../keuin/kbackupfabric/worker/RestoreWorker.java | 90 +++++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 src/main/java/com/keuin/kbackupfabric/worker/BackupWorker.java create mode 100644 src/main/java/com/keuin/kbackupfabric/worker/RestoreWorker.java (limited to 'src/main/java/com/keuin/kbackupfabric/worker') diff --git a/src/main/java/com/keuin/kbackupfabric/worker/BackupWorker.java b/src/main/java/com/keuin/kbackupfabric/worker/BackupWorker.java new file mode 100644 index 0000000..30fd8b5 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/worker/BackupWorker.java @@ -0,0 +1,92 @@ +package com.keuin.kbackupfabric.worker; + +import com.keuin.kbackupfabric.util.ZipUtil; +import com.keuin.kbackupfabric.util.ZipUtilException; +import com.mojang.brigadier.context.CommandContext; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.world.World; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static com.keuin.kbackupfabric.util.BackupFilesystemUtil.*; +import static com.keuin.kbackupfabric.util.PrintUtil.debug; +import static com.keuin.kbackupfabric.util.PrintUtil.message; + +/** + * The backup worker + * To invoke this worker, simply call invoke() method. + */ +public final class BackupWorker implements Runnable { + private final CommandContext context; + private final String backupName; + private final Map oldWorldsSavingDisabled; + + + private BackupWorker(CommandContext context, String backupName, Map oldWorldsSavingDisabled) { + this.context = context; + this.backupName = backupName; + this.oldWorldsSavingDisabled = oldWorldsSavingDisabled; + } + + public static void invoke(CommandContext context, String backupName) { + //// Save world, save old autosave configs + + message(context, String.format("Making backup %s, please wait ...", backupName), true); + Map oldWorldsSavingDisabled = new HashMap<>(); // old switch stat + + // Get server + MinecraftServer server = context.getSource().getMinecraftServer(); + + // Save old autosave switch stat temporally + server.getWorlds().forEach(world -> { + oldWorldsSavingDisabled.put(world, world.savingDisabled); + world.savingDisabled = true; + }); + + // Force to save all player data and worlds + debug("Saving players ..."); + server.getPlayerManager().saveAllPlayerData(); + debug("Saving worlds ..."); + server.save(true, true, true); + + // Start threaded worker + BackupWorker worker = new BackupWorker(context, backupName, oldWorldsSavingDisabled); + Thread workerThread = new Thread(worker, "BackupWorker"); + workerThread.start(); + } + + @Override + public void run() { + String destPathFolderName = ""; + MinecraftServer server = context.getSource().getMinecraftServer(); + try { + //// Do our main backup logic + + // Create backup saving directory + File destPathFile = getBackupSaveDirectory(server); + destPathFolderName = destPathFile.getName(); + if (!destPathFile.isDirectory() && !destPathFile.mkdir()) { + message(context, String.format("Failed to create backup saving directory: %s. Failed to backup.", destPathFolderName)); + return; + } + + // Make zip + String levelPath = getLevelPath(server); + debug(String.format("zip(srcPath=%s, destPath=%s)", levelPath, destPathFile.toString())); + ZipUtil.zip(levelPath, destPathFile.toString(), getBackupFileName(backupName)); + + // Restore old autosave switch stat + server.getWorlds().forEach(world -> world.savingDisabled = oldWorldsSavingDisabled.getOrDefault(world, true)); + + message(context, "Done.", true); + } catch (SecurityException e) { + message(context, String.format("Failed to create backup saving directory: %s. Failed to backup.", destPathFolderName)); + } catch (IOException | ZipUtilException e) { + message(context, "Failed to make zip: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/keuin/kbackupfabric/worker/RestoreWorker.java b/src/main/java/com/keuin/kbackupfabric/worker/RestoreWorker.java new file mode 100644 index 0000000..9e424d7 --- /dev/null +++ b/src/main/java/com/keuin/kbackupfabric/worker/RestoreWorker.java @@ -0,0 +1,90 @@ +package com.keuin.kbackupfabric.worker; + +import com.keuin.kbackupfabric.util.ZipUtil; +import com.keuin.kbackupfabric.util.ZipUtilException; +import net.minecraft.server.MinecraftServer; + +import java.io.File; +import java.io.IOException; + +import static com.keuin.kbackupfabric.util.PrintUtil.*; +import static org.apache.commons.io.FileUtils.forceDelete; + +/** + * The restore worker + * To invoke this worker, simply call invoke() method. + */ +public final class RestoreWorker implements Runnable { + private final Thread serverThread; + private final String backupFilePath; + private final String levelDirectory; + + private RestoreWorker(Thread serverThread, String backupFilePath, String levelDirectory) { + this.serverThread = serverThread; + this.backupFilePath = backupFilePath; + this.levelDirectory = levelDirectory; + } + + public static void invoke(MinecraftServer server, String backupFilePath, String levelDirectory) { + RestoreWorker worker = new RestoreWorker(server.getThread(), backupFilePath, levelDirectory); + Thread workerThread = new Thread(worker, "RestoreWorker"); + workerThread.start(); + try { + Thread.sleep(500); + } catch (InterruptedException ignored) { + } + server.stop(false); + } + + @Override + public void run() { + try { + // Wait server thread die + debug("Waiting server thread stopping ..."); + while (serverThread.isAlive()) { + try { + serverThread.join(); + } catch (InterruptedException ignored) { + } + } + + debug("Wait for 5 seconds ..."); + try { + Thread.sleep(5000); + } catch (InterruptedException ignored) { + } + + // Delete old level + debug("Server stopped. Deleting old level ..."); + File levelDirFile = new File(levelDirectory); + + int failedCounter = 0; + final int MAX_RETRY_TIMES = 20; + while (failedCounter < MAX_RETRY_TIMES) { + System.gc(); + if (!levelDirFile.delete() && levelDirFile.exists()) { + System.gc(); + forceDelete(levelDirFile); // Try to force delete. + } + if (!levelDirFile.exists()) + break; + ++failedCounter; + try { + Thread.sleep(500); + } catch (InterruptedException ignored) { + } + } + if (levelDirFile.exists()) { + error(String.format("Cannot restore: failed to delete old level %s .", levelDirFile.getName())); + return; + } + + // Decompress archive + debug("Decompressing archived level"); + ZipUtil.unzip(backupFilePath, levelDirectory, false); + info("Restore complete! Please restart the server manually."); + } catch (SecurityException | IOException | ZipUtilException e) { + error("An exception occurred while restoring: " + e.getMessage()); + } + } +} -- cgit v1.2.3