Finish adding the snapshots functionality.

This commit is contained in:
zontreck 2024-07-02 00:53:26 -07:00
parent e53fc977bc
commit 633fc37892
6 changed files with 475 additions and 5 deletions

View file

@ -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()
});
}
}

View file

@ -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) {}
@ -300,7 +316,11 @@ class C2SUploadSettingsPacket implements IPacket {
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"}");
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<String, dynamic> js) {}
@override
String getChannelID() {
return "C2SRequestCreateBackup";
}
@override
Future<void> handleClientPacket() async {
// This is not handled on the client at all
}
@override
Future<PacketResponse> 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<String, dynamic> 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<String, dynamic> js) {}
@override
String getChannelID() {
return "C2SRequestSnapshotList";
}
@override
Future<void> handleClientPacket() async {}
@override
Future<PacketResponse> handleServerPacket() async {
// Generate the list of all snapshot files
Settings settings = Settings();
List<String> snapshotFileNames = await settings.getWorldSnapshotFiles();
S2CResponse response = S2CResponse();
S2CSnapshotList snapshots = S2CSnapshotList();
List<String> 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<String, dynamic> toJson() {
return {};
}
}
class S2CSnapshotList implements IPacket {
List<String> 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<String, dynamic> js) {}
@override
String getChannelID() {
return "S2CSnapshotList";
}
@override
Future<void> handleClientPacket() async {
// Oh hey, its us!
// Put the list in the SessionData
SessionData.IE_SNAPSHOTS = snapshotFileNames;
}
@override
Future<PacketResponse> handleServerPacket() {
throw UnimplementedError();
}
@override
Map<String, dynamic> 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<String, dynamic> js) {}
@override
String getChannelID() {
return "C2SRequestSnapshotDeletion";
}
@override
Future<void> handleClientPacket() async {}
@override
Future<PacketResponse> handleServerPacket() async {
Settings settings = Settings();
String correctedName = "$snapshotName.db";
PathHelper ph = PathHelper(pth: settings.getWorldSnapshotFolder())
.resolve(correctedName);
ph.deleteFile();
return PacketResponse.nil;
}
@override
Map<String, dynamic> 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<String, dynamic> js) {}
@override
String getChannelID() {
return "C2SRequestWorldRestore";
}
@override
Future<void> handleClientPacket() async {}
@override
Future<PacketResponse> 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<String, dynamic> toJson() {
return {};
}
}

View file

@ -49,7 +49,9 @@ class GameServerPageState extends State<GameServerPage> {
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"),

View file

@ -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<SnapshotsPage> {
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<SnapshotsPage> {
);
}
}
class SnapshotListPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return SnapshotListState();
}
}
class SnapshotListState extends State<SnapshotListPage> {
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)),
],
),
);
})),
),
);
}
}

View file

@ -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

View file

@ -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<String> IE_SNAPSHOTS = [];
}