Phase4
This commit is contained in:
@@ -5,11 +5,16 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/hexahost/gamecloud/node-agent/internal/archive"
|
||||
"github.com/hexahost/gamecloud/node-agent/internal/docker"
|
||||
"github.com/hexahost/gamecloud/node-agent/internal/files"
|
||||
"github.com/hexahost/gamecloud/node-agent/internal/protocol"
|
||||
"github.com/hexahost/gamecloud/node-agent/internal/transfer"
|
||||
worldpkg "github.com/hexahost/gamecloud/node-agent/internal/world"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -352,6 +357,219 @@ func (m *Manager) HandleFilesWrite(ctx context.Context, correlationID string, pa
|
||||
})
|
||||
}
|
||||
|
||||
// HandleBackupPrepare runs save-off and save-all flush via RCON.
|
||||
func (m *Manager) HandleBackupPrepare(ctx context.Context, correlationID string, meta protocol.OperationMeta) {
|
||||
rec, ok := m.getServer(meta.ServerID)
|
||||
if !ok {
|
||||
m.failOperation(ctx, correlationID, meta, "backup.prepare", "SERVER_NOT_FOUND", "server is not provisioned on this node")
|
||||
return
|
||||
}
|
||||
|
||||
if rec.State != ServerStateRunning {
|
||||
m.completeOperation(ctx, correlationID, meta, "backup.prepare", map[string]string{"skipped": "not_running"})
|
||||
return
|
||||
}
|
||||
|
||||
for _, command := range []string{"save-off", "save-all flush"} {
|
||||
if _, _, err := m.docker.ExecRcon(ctx, rec.ContainerID, command); err != nil {
|
||||
m.failOperation(ctx, correlationID, meta, "backup.prepare", "BACKUP_PREPARE_FAILED", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
m.completeOperation(ctx, correlationID, meta, "backup.prepare", map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
// HandleBackupRelease runs save-on via RCON.
|
||||
func (m *Manager) HandleBackupRelease(ctx context.Context, correlationID string, meta protocol.OperationMeta) {
|
||||
rec, ok := m.getServer(meta.ServerID)
|
||||
if !ok {
|
||||
m.failOperation(ctx, correlationID, meta, "backup.release", "SERVER_NOT_FOUND", "server is not provisioned on this node")
|
||||
return
|
||||
}
|
||||
|
||||
if rec.State != ServerStateRunning {
|
||||
m.completeOperation(ctx, correlationID, meta, "backup.release", map[string]string{"skipped": "not_running"})
|
||||
return
|
||||
}
|
||||
|
||||
if _, _, err := m.docker.ExecRcon(ctx, rec.ContainerID, "save-on"); err != nil {
|
||||
m.failOperation(ctx, correlationID, meta, "backup.release", "BACKUP_RELEASE_FAILED", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
m.completeOperation(ctx, correlationID, meta, "backup.release", map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
// HandleWorldValidate validates a world directory and lists worlds.
|
||||
func (m *Manager) HandleWorldValidate(ctx context.Context, correlationID string, payload protocol.ServerWorldValidatePayload) {
|
||||
meta := payload.OperationMeta
|
||||
rec, ok := m.getServer(meta.ServerID)
|
||||
if !ok {
|
||||
m.failOperation(ctx, correlationID, meta, "world.validate", "SERVER_NOT_FOUND", "server is not provisioned on this node")
|
||||
return
|
||||
}
|
||||
|
||||
worldName := payload.WorldName
|
||||
if worldName == "" {
|
||||
worldName = "world"
|
||||
}
|
||||
|
||||
if err := worldpkg.ValidateWorld(rec.DataPath, worldName); err != nil {
|
||||
m.failOperation(ctx, correlationID, meta, "world.validate", "WORLD_INVALID", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
worlds, err := worldpkg.ListWorlds(rec.DataPath, worldName)
|
||||
if err != nil {
|
||||
m.failOperation(ctx, correlationID, meta, "world.validate", "WORLD_LIST_FAILED", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
m.completeOperation(ctx, correlationID, meta, "world.validate", map[string]any{
|
||||
"valid": true,
|
||||
"worldName": worldName,
|
||||
"worlds": worlds,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleWorldArchive creates a tar.gz archive of a world directory.
|
||||
func (m *Manager) HandleWorldArchive(ctx context.Context, correlationID string, payload protocol.ServerWorldArchivePayload) {
|
||||
meta := payload.OperationMeta
|
||||
rec, ok := m.getServer(meta.ServerID)
|
||||
if !ok {
|
||||
m.failOperation(ctx, correlationID, meta, "world.archive", "SERVER_NOT_FOUND", "server is not provisioned on this node")
|
||||
return
|
||||
}
|
||||
|
||||
worldName := payload.WorldName
|
||||
if worldName == "" {
|
||||
worldName = "world"
|
||||
}
|
||||
|
||||
if err := worldpkg.ValidateWorld(rec.DataPath, worldName); err != nil {
|
||||
m.failOperation(ctx, correlationID, meta, "world.archive", "WORLD_INVALID", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
archiveDir := filepath.Join(rec.DataPath, ".hgc", "archives")
|
||||
archiveID := payload.ArchiveID
|
||||
if archiveID == "" {
|
||||
archiveID = meta.ServerID
|
||||
}
|
||||
outputPath := filepath.Join(archiveDir, archiveID+".tar.gz")
|
||||
sourceDir := filepath.Join(rec.DataPath, worldName)
|
||||
|
||||
checksum, size, err := archive.CreateTarGz(sourceDir, outputPath)
|
||||
if err != nil {
|
||||
m.failOperation(ctx, correlationID, meta, "world.archive", "ARCHIVE_FAILED", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
m.completeOperation(ctx, correlationID, meta, "world.archive", map[string]any{
|
||||
"localPath": outputPath,
|
||||
"sha256": checksum,
|
||||
"sizeBytes": size,
|
||||
"worldName": worldName,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleStorageUpload uploads a local archive to object storage.
|
||||
func (m *Manager) HandleStorageUpload(ctx context.Context, correlationID string, payload protocol.ServerStorageUploadPayload) {
|
||||
meta := payload.OperationMeta
|
||||
|
||||
if err := transfer.UploadFile(payload.LocalPath, payload.UploadURL); err != nil {
|
||||
m.failOperation(ctx, correlationID, meta, "storage.upload", "UPLOAD_FAILED", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
m.completeOperation(ctx, correlationID, meta, "storage.upload", map[string]string{
|
||||
"localPath": payload.LocalPath,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleStorageDownload downloads an archive from object storage.
|
||||
func (m *Manager) HandleStorageDownload(ctx context.Context, correlationID string, payload protocol.ServerStorageDownloadPayload) {
|
||||
meta := payload.OperationMeta
|
||||
|
||||
if err := transfer.DownloadFile(payload.DownloadURL, payload.LocalPath); err != nil {
|
||||
m.failOperation(ctx, correlationID, meta, "storage.download", "DOWNLOAD_FAILED", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
m.completeOperation(ctx, correlationID, meta, "storage.download", map[string]string{
|
||||
"localPath": payload.LocalPath,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleWorldReplace replaces a world directory from a local archive.
|
||||
func (m *Manager) HandleWorldReplace(ctx context.Context, correlationID string, payload protocol.ServerWorldReplacePayload) {
|
||||
meta := payload.OperationMeta
|
||||
rec, ok := m.getServer(meta.ServerID)
|
||||
if !ok {
|
||||
m.failOperation(ctx, correlationID, meta, "world.replace", "SERVER_NOT_FOUND", "server is not provisioned on this node")
|
||||
return
|
||||
}
|
||||
|
||||
if rec.State == ServerStateRunning {
|
||||
m.failOperation(ctx, correlationID, meta, "world.replace", "SERVER_RUNNING", "server must be stopped before world replace")
|
||||
return
|
||||
}
|
||||
|
||||
worldName := payload.WorldName
|
||||
if worldName == "" {
|
||||
worldName = "world"
|
||||
}
|
||||
|
||||
worldDir := filepath.Join(rec.DataPath, worldName)
|
||||
tempDir := filepath.Join(rec.DataPath, ".hgc", "restore", meta.ServerID)
|
||||
if err := os.RemoveAll(tempDir); err != nil {
|
||||
m.failOperation(ctx, correlationID, meta, "world.replace", "RESTORE_PREP_FAILED", err.Error())
|
||||
return
|
||||
}
|
||||
if err := os.MkdirAll(tempDir, 0o755); err != nil {
|
||||
m.failOperation(ctx, correlationID, meta, "world.replace", "RESTORE_PREP_FAILED", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := archive.ExtractTarGz(payload.ArchivePath, tempDir); err != nil {
|
||||
m.failOperation(ctx, correlationID, meta, "world.replace", "EXTRACT_FAILED", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
extractedWorld := filepath.Join(tempDir, worldName)
|
||||
if info, err := os.Stat(extractedWorld); err != nil || !info.IsDir() {
|
||||
extractedWorld = tempDir
|
||||
}
|
||||
|
||||
if err := worldpkg.ValidateWorld(filepath.Dir(extractedWorld), filepath.Base(extractedWorld)); err != nil {
|
||||
m.failOperation(ctx, correlationID, meta, "world.replace", "WORLD_INVALID", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
backupDir := worldDir + ".bak"
|
||||
_ = os.RemoveAll(backupDir)
|
||||
if _, err := os.Stat(worldDir); err == nil {
|
||||
if err := os.Rename(worldDir, backupDir); err != nil {
|
||||
m.failOperation(ctx, correlationID, meta, "world.replace", "REPLACE_FAILED", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.Rename(extractedWorld, worldDir); err != nil {
|
||||
_ = os.Rename(backupDir, worldDir)
|
||||
m.failOperation(ctx, correlationID, meta, "world.replace", "REPLACE_FAILED", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_ = os.RemoveAll(tempDir)
|
||||
_ = os.RemoveAll(backupDir)
|
||||
|
||||
m.completeOperation(ctx, correlationID, meta, "world.replace", map[string]string{
|
||||
"worldName": worldName,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) getServer(serverID string) (*serverRecord, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
Reference in New Issue
Block a user