From 633fc3789264acc44c080f5a8ed31d6946332f60 Mon Sep 17 00:00:00 2001 From: zontreck Date: Tue, 2 Jul 2024 00:53:26 -0700 Subject: [PATCH] Finish adding the snapshots functionality. --- lib/main.dart | 3 +- lib/packets/ClientPackets.dart | 344 ++++++++++++++++++++++++++++++++- lib/pages/GameServerPage.dart | 4 +- lib/pages/snapshots.dart | 104 ++++++++++ lib/statemachine.dart | 13 ++ lib/structs/SessionData.dart | 12 ++ 6 files changed, 475 insertions(+), 5 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 395ca41..5257122 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -36,7 +36,8 @@ class MyApp extends StatelessWidget { "/server/mods": (context) => ModManager(settings: appSettings), "/server/mods/edit": (context) => ModPage(), "/server/discord": (context) => DiscordConfigPage(), - "/server/snapshots": (context) => SnapshotsPage() + "/server/snapshots": (context) => SnapshotsPage(), + "/server/snapshots/restore": (context) => SnapshotListPage() }); } } diff --git a/lib/packets/ClientPackets.dart b/lib/packets/ClientPackets.dart index daa1141..4b87eec 100644 --- a/lib/packets/ClientPackets.dart +++ b/lib/packets/ClientPackets.dart @@ -6,9 +6,11 @@ import 'package:libac_dart/nbt/NbtUtils.dart'; import 'package:libac_dart/nbt/Stream.dart'; import 'package:libac_dart/nbt/Tag.dart'; import 'package:libac_dart/nbt/impl/CompoundTag.dart'; +import 'package:libac_dart/nbt/impl/ListTag.dart'; import 'package:libac_dart/nbt/impl/StringTag.dart'; import 'package:libac_dart/packets/packets.dart'; import 'package:libac_dart/utils/Hashing.dart'; +import 'package:libac_dart/utils/IOTools.dart'; import 'package:libac_dart/utils/uuid/NbtUUID.dart'; import 'package:libac_dart/utils/uuid/UUID.dart'; import 'package:servermanager/statemachine.dart'; @@ -31,6 +33,21 @@ class ClientPackets { reg.register(C2SUploadSettingsPacket(), () { return C2SUploadSettingsPacket(); }); + reg.register(C2SRequestCreateBackup(), () { + return C2SRequestCreateBackup(); + }); + reg.register(C2SRequestSnapshotList(), () { + return C2SRequestSnapshotList(); + }); + reg.register(S2CSnapshotList(), () { + return S2CSnapshotList(); + }); + reg.register(C2SRequestSnapshotDeletion(), () { + return C2SRequestSnapshotDeletion(); + }); + reg.register(C2SRequestWorldRestore(), () { + return C2SRequestWorldRestore(); + }); } } @@ -249,7 +266,6 @@ class C2SUploadSettingsPacket implements IPacket { CompoundTag srvSettings = CompoundTag(); bool performRestart = false; - @override void decodeJson(String params) {} @@ -299,8 +315,12 @@ class C2SUploadSettingsPacket implements IPacket { settings.deserialize(srvSettings); settings.Write(); - if(!performRestart){ - DiscordHookHelper.sendWebHook(settings.inst!.discord, DiscordHookProps.ONLINE_ALERT, "Server Wrapper Settings", "Server wrapper settings have been updated.\n\n${performRestart ? "A restart has been requested" : "A restart is not needed"}"); + if (!performRestart) { + DiscordHookHelper.sendWebHook( + settings.inst!.discord, + DiscordHookProps.ONLINE_ALERT, + "Server Wrapper Settings", + "Server wrapper settings have been updated.\n\n${performRestart ? "A restart has been requested" : "A restart is not needed"}"); return PacketResponse.nil; } @@ -333,3 +353,321 @@ class C2SUploadSettingsPacket implements IPacket { return {}; } } + +class C2SRequestCreateBackup implements IPacket { + String fileName = ""; // File name of the backup + + @override + void decodeJson(String params) { + return; // Json not supported by this packet + } + + @override + void decodeTag(Tag tag) { + if (tag is CompoundTag) { + CompoundTag ct = tag.asCompoundTag(); + fileName = ct.get("name")!.asString(); + } + } + + @override + NetworkDirection direction() { + return NetworkDirection.ClientToServer; + } + + @override + String encodeJson() { + return "{}"; + } + + @override + Tag encodeTag() { + CompoundTag ct = CompoundTag(); + ct.put("name", StringTag.valueOf(fileName)); + + return ct; + } + + @override + void fromJson(Map js) {} + + @override + String getChannelID() { + return "C2SRequestCreateBackup"; + } + + @override + Future handleClientPacket() async { + // This is not handled on the client at all + } + + @override + Future handleServerPacket() async { + // Copy the world file to the new destination + Settings settings = Settings(); + + File world = File(settings.getWorldGameDB()); + if (world.existsSync()) { + // We're good! + // Begin copy operations + String destinationFile = "$fileName.db"; + PathHelper pth = PathHelper(pth: settings.getWorldSnapshotFolder()) + .resolve(destinationFile); + world.copy(pth.build()); + } + + return PacketResponse.nil; + } + + @override + Map toJson() { + return {}; + } +} + +class C2SRequestSnapshotList implements IPacket { + @override + void decodeJson(String params) {} + + @override + void decodeTag(Tag tag) {} + + @override + NetworkDirection direction() { + return NetworkDirection.ClientToServer; + } + + @override + String encodeJson() { + return "{}"; + } + + @override + Tag encodeTag() { + return CompoundTag(); + } + + @override + void fromJson(Map js) {} + + @override + String getChannelID() { + return "C2SRequestSnapshotList"; + } + + @override + Future handleClientPacket() async {} + + @override + Future handleServerPacket() async { + // Generate the list of all snapshot files + Settings settings = Settings(); + + List snapshotFileNames = await settings.getWorldSnapshotFiles(); + S2CResponse response = S2CResponse(); + S2CSnapshotList snapshots = S2CSnapshotList(); + + List strippedFiles = []; + for (String str in snapshotFileNames) { + if (str.endsWith(".db")) { + // add file name without db extension to the list + + String trimmedFileName = str.trim().substring(0, str.trim().length - 3); + strippedFiles.add(trimmedFileName); + } else + strippedFiles.add(str); + } + + snapshots.snapshotFileNames = strippedFiles; + + response.contents = snapshots.encodeTag().asCompoundTag(); + return PacketResponse(replyDataTag: response.encodeTag().asCompoundTag()); + } + + @override + Map toJson() { + return {}; + } +} + +class S2CSnapshotList implements IPacket { + List snapshotFileNames = []; + + @override + void decodeJson(String params) {} + + @override + void decodeTag(Tag tag) { + CompoundTag ct = tag.asCompoundTag(); + + ListTag lst = ct.get("items") as ListTag; + for (int i = 0; i < lst.size(); i++) { + snapshotFileNames.add(lst.get(i).asString()); + } + } + + @override + NetworkDirection direction() { + return NetworkDirection.ServerToClient; + } + + @override + String encodeJson() { + return "[]"; + } + + @override + Tag encodeTag() { + CompoundTag ct = CompoundTag(); + + ListTag lst = ListTag(); + for (String str in snapshotFileNames) { + lst.add(StringTag.valueOf(str)); + } + + ct.put("items", lst); + return ct; + } + + @override + void fromJson(Map js) {} + + @override + String getChannelID() { + return "S2CSnapshotList"; + } + + @override + Future handleClientPacket() async { + // Oh hey, its us! + // Put the list in the SessionData + SessionData.IE_SNAPSHOTS = snapshotFileNames; + } + + @override + Future handleServerPacket() { + throw UnimplementedError(); + } + + @override + Map toJson() { + return {}; + } +} + +class C2SRequestSnapshotDeletion implements IPacket { + String snapshotName = ""; + + @override + void decodeJson(String params) {} + + @override + void decodeTag(Tag tag) { + CompoundTag ct = tag.asCompoundTag(); + snapshotName = ct.get("name")!.asString(); + } + + @override + NetworkDirection direction() { + return NetworkDirection.ClientToServer; + } + + @override + String encodeJson() { + return "{}"; + } + + @override + Tag encodeTag() { + CompoundTag ct = CompoundTag(); + ct.put("name", StringTag.valueOf(snapshotName)); + + return ct; + } + + @override + void fromJson(Map js) {} + + @override + String getChannelID() { + return "C2SRequestSnapshotDeletion"; + } + + @override + Future handleClientPacket() async {} + + @override + Future handleServerPacket() async { + Settings settings = Settings(); + + String correctedName = "$snapshotName.db"; + PathHelper ph = PathHelper(pth: settings.getWorldSnapshotFolder()) + .resolve(correctedName); + ph.deleteFile(); + + return PacketResponse.nil; + } + + @override + Map toJson() { + return {}; + } +} + +class C2SRequestWorldRestore implements IPacket { + String snapshot = ""; + + @override + void decodeJson(String params) {} + + @override + void decodeTag(Tag tag) { + CompoundTag ct = tag.asCompoundTag(); + snapshot = ct.get("name")!.asString(); + } + + @override + NetworkDirection direction() { + return NetworkDirection.ClientToServer; + } + + @override + String encodeJson() { + return "{}"; + } + + @override + Tag encodeTag() { + CompoundTag ct = CompoundTag(); + ct.put("name", StringTag.valueOf(snapshot)); + + return ct; + } + + @override + void fromJson(Map js) {} + + @override + String getChannelID() { + return "C2SRequestWorldRestore"; + } + + @override + Future handleClientPacket() async {} + + @override + Future handleServerPacket() async { + SessionData.isWorldRestore = true; + SessionData.snapshotToRestore = snapshot; + SessionData.shutdownMessage = "A backup restore has been requested"; + SessionData.timer.apply(30); + SessionData.CURRENT_INTERVAL = WarnIntervals.NONE; + + return PacketResponse.nil; + } + + @override + Map toJson() { + return {}; + } +} diff --git a/lib/pages/GameServerPage.dart b/lib/pages/GameServerPage.dart index e706c00..50afce4 100644 --- a/lib/pages/GameServerPage.dart +++ b/lib/pages/GameServerPage.dart @@ -49,7 +49,9 @@ class GameServerPageState extends State { title: Text("Server Snapshots"), leading: Icon(Icons.photo), subtitle: Text("Manage server database snapshots"), - onTap: () {}, + onTap: () { + Navigator.pushNamed(context, "/server/snapshots"); + }, ), ListTile( title: Text("Configure AutoRestart"), diff --git a/lib/pages/snapshots.dart b/lib/pages/snapshots.dart index 86589c2..a59ef1f 100644 --- a/lib/pages/snapshots.dart +++ b/lib/pages/snapshots.dart @@ -1,5 +1,10 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:libac_dart/packets/packets.dart'; +import 'package:servermanager/packets/ClientPackets.dart'; import 'package:servermanager/pages/Constants.dart'; +import 'package:servermanager/pages/dialogbox.dart'; +import 'package:servermanager/structs/SessionData.dart'; import 'package:servermanager/structs/settings.dart'; class SnapshotsPage extends StatefulWidget { @@ -31,6 +36,50 @@ class SnapshotsState extends State { title: Text("Server DB File"), subtitle: Text(settings.gameServerDBFile), ), + ListTile( + title: Text("Create Backup"), + subtitle: Text( + "This action will prompt for a filename, then contact the server, which will then take a immediate snapshot of the world under that name."), + leading: Icon(Icons.backup), + onTap: () { + String finalValue = ""; + InputBox ib = + InputBox("", promptText: "Backup Name?", changed: (V) { + setState(() { + finalValue = V; + }); + }, onSubmit: () { + // Send the filename to the server and request backup creation + C2SRequestCreateBackup rcb = C2SRequestCreateBackup(); + rcb.fileName = finalValue; + + settings.client!.send(rcb, true); + }, onCancel: () {}, isDefault: true); + + showCupertinoDialog( + context: context, + builder: (ctx) { + return ib; + }); + }, + ), + ListTile( + title: Text("Restore"), + subtitle: Text("Restore a backup file to active status"), + leading: Icon(Icons.restore), + onTap: () async { + // Request the snapshot list, then present the restore list to the end user + C2SRequestSnapshotList rsl = C2SRequestSnapshotList(); + S2CResponse reply = await settings.client!.send(rsl, true); + S2CSnapshotList snaps = S2CSnapshotList(); + snaps.decodeTag(reply.contents); + await snaps.handleClientPacket(); + + if (SessionData.IE_SNAPSHOTS.isNotEmpty) { + // Show the list! + } + }, + ) ], ), ), @@ -38,3 +87,58 @@ class SnapshotsState extends State { ); } } + +class SnapshotListPage extends StatefulWidget { + @override + State createState() { + return SnapshotListState(); + } +} + +class SnapshotListState extends State { + Settings settings = Settings(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + "Snapshot Manager - Restore - ${SessionData.IE_SNAPSHOTS.length} backups"), + backgroundColor: Constants.TITLEBAR_COLOR, + ), + body: Padding( + padding: EdgeInsets.all(8), + child: SingleChildScrollView( + child: ListView.builder(itemBuilder: (ctx, index) { + String filename = SessionData.IE_SNAPSHOTS[index]; + return ListTile( + title: Text(filename), + leading: Icon(Icons.photo), + subtitle: Text( + "No information is known about this snapshot at this time"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () { + // Send restore packet + }, + icon: Icon(Icons.restore)), + IconButton( + onPressed: () { + // Send deletion packet + C2SRequestSnapshotDeletion rsd = + C2SRequestSnapshotDeletion(); + rsd.snapshotName = filename; + + settings.client!.send(rsd, true); + }, + icon: Icon(Icons.delete)), + ], + ), + ); + })), + ), + ); + } +} diff --git a/lib/statemachine.dart b/lib/statemachine.dart index 0479be0..264be0b 100644 --- a/lib/statemachine.dart +++ b/lib/statemachine.dart @@ -236,6 +236,19 @@ class StateMachine { { timer.cancel(); + // Check if we should perform a world restore + if (SessionData.isWorldRestore) { + await DiscordHookHelper.sendWebHook( + settings.inst!.discord, + DiscordHookProps.OFFLINE_ALERT, + "RESTORE", + "Restoring backup file: ${SessionData.snapshotToRestore}"); + + // Now restore the backup file + File backup = File(SessionData.snapshotToRestore); + await backup.copy(settings.getWorldGameDB()); + } + //Settings settings = Settings(); if (settings.inst!.pterodactylMode) { // Shut down the server processes now diff --git a/lib/structs/SessionData.dart b/lib/structs/SessionData.dart index 0451acf..1f55719 100644 --- a/lib/structs/SessionData.dart +++ b/lib/structs/SessionData.dart @@ -19,6 +19,9 @@ class SessionData { static Time mod_update_check_tracker = Time(hours: 0, minutes: 0, seconds: 0); static bool enableRestartTimer = false; static bool canPingServer = false; + static bool isWorldRestore = false; + static String snapshotToRestore = + ""; // This is the absolute path to the snapshot being restored static Time timeSinceLastPing = Time(hours: 0, minutes: 0, seconds: 0); @@ -33,4 +36,13 @@ class SessionData { static bool shouldCheckModUpdates() { return mod_update_check_tracker.minutes >= 30; } + + /// Interactive Editor - Snapshots + /// + /// This contains the list of snapshot files for the editor client GUI. + /// + /// DO NOT USE ON SERVER + /// + /// Use the [Settings.getWorldSnapshotFiles] function instead + static List IE_SNAPSHOTS = []; }