diff --git a/src/main/java/cloud/kubelet/foundation/Foundation.java b/src/main/java/cloud/kubelet/foundation/Foundation.java index b3a02d8..3997706 100644 --- a/src/main/java/cloud/kubelet/foundation/Foundation.java +++ b/src/main/java/cloud/kubelet/foundation/Foundation.java @@ -1,18 +1,38 @@ package cloud.kubelet.foundation; +import cloud.kubelet.foundation.command.BackupCommand; import io.papermc.paper.event.player.ChatEvent; +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Objects; import net.kyori.adventure.text.Component; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.plugin.java.JavaPlugin; public final class Foundation extends JavaPlugin implements Listener { + public static final boolean BACKUP_ENABLED = true; + private static final String BACKUPS_DIRECTORY = "backups"; @Override public void onEnable() { + Path dataPath = getDataFolder().toPath(); + Path backupPath = dataPath.resolve(BACKUPS_DIRECTORY); + + // Create Foundation plugin directories. + dataPath.toFile().mkdir(); + backupPath.toFile().mkdir(); + + // Register this as an event listener. getServer().getPluginManager().registerEvents(this, this); - getDataFolder().mkdir(); + // Set up commands. + Objects.requireNonNull(getCommand("fbackup")).setExecutor(new BackupCommand(backupPath)); + + final var log = getSLF4JLogger(); + log.info("Features:"); + Util.printFeatureStatus(log, "Backup: ", BACKUP_ENABLED); } @Override diff --git a/src/main/java/cloud/kubelet/foundation/Util.java b/src/main/java/cloud/kubelet/foundation/Util.java new file mode 100644 index 0000000..3058bd8 --- /dev/null +++ b/src/main/java/cloud/kubelet/foundation/Util.java @@ -0,0 +1,15 @@ +package cloud.kubelet.foundation; + +import java.io.IOException; +import java.util.List; +import org.slf4j.Logger; + +public class Util { + public static int runProcess(List command) throws IOException, InterruptedException { + return new ProcessBuilder(command).start().waitFor(); + } + + public static void printFeatureStatus(Logger logger, String feature, boolean state) { + logger.info("{}: {}", feature, state ? "Enabled" : "Disabled"); + } +} diff --git a/src/main/java/cloud/kubelet/foundation/command/BackupCommand.java b/src/main/java/cloud/kubelet/foundation/command/BackupCommand.java new file mode 100644 index 0000000..ee323f4 --- /dev/null +++ b/src/main/java/cloud/kubelet/foundation/command/BackupCommand.java @@ -0,0 +1,105 @@ +package cloud.kubelet.foundation.command; + +import cloud.kubelet.foundation.Foundation; +import java.io.BufferedOutputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import org.bukkit.World; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +public class BackupCommand implements CommandExecutor { + + private static final AtomicBoolean RUNNING = new AtomicBoolean(); + private final Path backupPath; + + public BackupCommand(Path backupPath) { + this.backupPath = backupPath; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, + @NotNull String label, @NotNull String[] args) { + if (!Foundation.BACKUP_ENABLED) { + sender.sendMessage( + Component + .text("Backup is not enabled.") + .color(TextColor.fromHexString("#FF0000")) + ); + return true; + } + if (RUNNING.get()) { + sender.sendMessage( + Component + .text("Backup is already running.") + .color(TextColor.fromHexString("#FF0000")) + ); + } else { + try { + runBackup(sender); + } catch (Exception e) { + sender.sendMessage(String.format("Failed to backup: %s", e.getMessage())); + } + } + + return true; + } + + private void runBackup(CommandSender sender) throws IOException { + RUNNING.set(true); + + final var backupFile = backupPath.resolve( + String.format("backup-%s.zip", Instant.now().toString())).toFile(); + final var zipFileStream = new FileOutputStream(backupFile); + + try (zipFileStream; var zipStream = new ZipOutputStream( + new BufferedOutputStream(zipFileStream))) { + final var worlds = sender.getServer().getWorlds(); + for (World world : worlds) { + final var name = world.getName(); + final var worldPath = world.getWorldFolder().toPath().toString(); + sender.sendMessage(String.format("%s: %s", name, worldPath)); + + world.save(); + world.setAutoSave(false); + + try { + final var paths = Files.walk(Paths.get(worldPath)) + .filter(Files::isRegularFile) + .toList(); + + for (Path path : paths) { + try (InputStream fileStream = new FileInputStream(path.toFile())) { + final var entry = new ZipEntry(path.toString()); + zipStream.putNextEntry(entry); + int n; + byte[] buffer = new byte[1024]; + while ((n = fileStream.read(buffer)) > -1) { + zipStream.write(buffer, 0, n); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + + world.setAutoSave(true); + } + } finally { + RUNNING.set(false); + } + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index d79d460..3cc4343 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -6,4 +6,8 @@ prefix: Foundation load: STARTUP authors: - kubelet -commands: {} +commands: + fbackup: + description: Foundation Backup + usage: /fbackup + permission: foundation.backup