Phase4
This commit is contained in:
159
apps/node-agent/internal/archive/archive.go
Normal file
159
apps/node-agent/internal/archive/archive.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package archive
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CreateTarGz archives sourceDir into outputPath and returns checksum and size.
|
||||
func CreateTarGz(sourceDir, outputPath string) (sha256sum string, size int64, err error) {
|
||||
sourceDir = filepath.Clean(sourceDir)
|
||||
|
||||
info, err := os.Stat(sourceDir)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return "", 0, fmt.Errorf("source is not a directory")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
file, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hasher := sha256.New()
|
||||
writer := io.MultiWriter(file, hasher)
|
||||
gzipWriter := gzip.NewWriter(writer)
|
||||
tarWriter := tar.NewWriter(gzipWriter)
|
||||
|
||||
err = filepath.Walk(sourceDir, func(path string, entry os.FileInfo, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(sourceDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rel == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
rel = filepath.ToSlash(rel)
|
||||
header, err := tar.FileInfoHeader(entry, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = rel
|
||||
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
sourceFile, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
|
||||
_, err = io.Copy(tarWriter, sourceFile)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
if err := tarWriter.Close(); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
if err := gzipWriter.Close(); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
return hex.EncodeToString(hasher.Sum(nil)), stat.Size(), nil
|
||||
}
|
||||
|
||||
// ExtractTarGz extracts archivePath into destDir.
|
||||
func ExtractTarGz(archivePath, destDir string) error {
|
||||
destDir = filepath.Clean(destDir)
|
||||
if err := os.MkdirAll(destDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
gzipReader, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
|
||||
tarReader := tar.NewReader(gzipReader)
|
||||
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target := filepath.Join(destDir, filepath.FromSlash(header.Name))
|
||||
if !strings.HasPrefix(filepath.Clean(target), destDir+string(os.PathSeparator)) && filepath.Clean(target) != destDir {
|
||||
return fmt.Errorf("archive entry escapes destination: %s", header.Name)
|
||||
}
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.MkdirAll(target, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
case tar.TypeReg:
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(out, tarReader); err != nil {
|
||||
out.Close()
|
||||
return err
|
||||
}
|
||||
out.Close()
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -40,6 +40,10 @@ const (
|
||||
TypeServerFilesWrite MessageType = "server.files.write"
|
||||
TypeServerFilesUploadDone MessageType = "server.files.upload.complete"
|
||||
TypeServerWorldValidate MessageType = "server.world.validate"
|
||||
TypeServerWorldArchive MessageType = "server.world.archive"
|
||||
TypeServerWorldReplace MessageType = "server.world.replace"
|
||||
TypeServerStorageUpload MessageType = "server.storage.upload"
|
||||
TypeServerStorageDownload MessageType = "server.storage.download"
|
||||
TypeServerMetricsSub MessageType = "server.metrics.subscribe"
|
||||
TypeServerLogsSubscribe MessageType = "server.logs.subscribe"
|
||||
)
|
||||
@@ -72,6 +76,10 @@ var controlToAgentTypes = map[MessageType]struct{}{
|
||||
TypeServerFilesWrite: {},
|
||||
TypeServerFilesUploadDone: {},
|
||||
TypeServerWorldValidate: {},
|
||||
TypeServerWorldArchive: {},
|
||||
TypeServerWorldReplace: {},
|
||||
TypeServerStorageUpload: {},
|
||||
TypeServerStorageDownload: {},
|
||||
TypeServerMetricsSub: {},
|
||||
TypeServerLogsSubscribe: {},
|
||||
}
|
||||
@@ -178,6 +186,40 @@ type ServerFilesWritePayload struct {
|
||||
Create bool `json:"create"`
|
||||
}
|
||||
|
||||
// ServerWorldValidatePayload validates a Minecraft world directory.
|
||||
type ServerWorldValidatePayload struct {
|
||||
OperationMeta
|
||||
WorldName string `json:"worldName"`
|
||||
}
|
||||
|
||||
// ServerWorldArchivePayload archives a world directory.
|
||||
type ServerWorldArchivePayload struct {
|
||||
OperationMeta
|
||||
WorldName string `json:"worldName"`
|
||||
ArchiveID string `json:"archiveId"`
|
||||
}
|
||||
|
||||
// ServerStorageUploadPayload uploads a local file to a presigned URL.
|
||||
type ServerStorageUploadPayload struct {
|
||||
OperationMeta
|
||||
LocalPath string `json:"localPath"`
|
||||
UploadURL string `json:"uploadUrl"`
|
||||
}
|
||||
|
||||
// ServerStorageDownloadPayload downloads a remote file to a local path.
|
||||
type ServerStorageDownloadPayload struct {
|
||||
OperationMeta
|
||||
DownloadURL string `json:"downloadUrl"`
|
||||
LocalPath string `json:"localPath"`
|
||||
}
|
||||
|
||||
// ServerWorldReplacePayload replaces a world from a local archive.
|
||||
type ServerWorldReplacePayload struct {
|
||||
OperationMeta
|
||||
WorldName string `json:"worldName"`
|
||||
ArchivePath string `json:"archivePath"`
|
||||
}
|
||||
|
||||
// FileEntry describes a file or directory in a list response.
|
||||
type FileEntry struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -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()
|
||||
|
||||
73
apps/node-agent/internal/transfer/transfer.go
Normal file
73
apps/node-agent/internal/transfer/transfer.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package transfer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UploadFile PUTs a local file to a presigned URL.
|
||||
func UploadFile(localPath, uploadURL string) error {
|
||||
file, err := os.Open(localPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(http.MethodPut, uploadURL, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.ContentLength = stat.Size()
|
||||
request.Header.Set("Content-Type", "application/gzip")
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Minute}
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||
body, _ := io.ReadAll(io.LimitReader(response.Body, 4096))
|
||||
return fmt.Errorf("upload failed with status %d: %s", response.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DownloadFile GETs a remote file to a local path.
|
||||
func DownloadFile(downloadURL, localPath string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(localPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Minute}
|
||||
response, err := client.Get(downloadURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||
body, _ := io.ReadAll(io.LimitReader(response.Body, 4096))
|
||||
return fmt.Errorf("download failed with status %d: %s", response.StatusCode, string(body))
|
||||
}
|
||||
|
||||
file, err := os.Create(localPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(file, response.Body)
|
||||
return err
|
||||
}
|
||||
114
apps/node-agent/internal/world/world.go
Normal file
114
apps/node-agent/internal/world/world.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Summary describes a discovered Minecraft world directory.
|
||||
type Summary struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
SizeBytes int64 `json:"sizeBytes"`
|
||||
HasLevelDat bool `json:"hasLevelDat"`
|
||||
}
|
||||
|
||||
// ValidateWorld checks that a world directory contains level.dat.
|
||||
func ValidateWorld(dataPath, worldName string) error {
|
||||
worldName = strings.TrimSpace(worldName)
|
||||
if worldName == "" || strings.Contains(worldName, "..") || strings.ContainsAny(worldName, `/\`) {
|
||||
return fmt.Errorf("invalid world name")
|
||||
}
|
||||
|
||||
worldDir := filepath.Join(dataPath, worldName)
|
||||
levelDat := filepath.Join(worldDir, "level.dat")
|
||||
|
||||
info, err := os.Stat(levelDat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("level.dat not found in world directory")
|
||||
}
|
||||
if info.IsDir() {
|
||||
return fmt.Errorf("level.dat is not a file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListWorlds scans the server data directory for world folders.
|
||||
func ListWorlds(dataPath, activeWorldName string) ([]Summary, error) {
|
||||
entries, err := os.ReadDir(dataPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
worlds := make([]Summary, 0)
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
|
||||
continue
|
||||
}
|
||||
|
||||
worldPath := filepath.Join(dataPath, entry.Name())
|
||||
levelDat := filepath.Join(worldPath, "level.dat")
|
||||
hasLevelDat := false
|
||||
if info, err := os.Stat(levelDat); err == nil && !info.IsDir() {
|
||||
hasLevelDat = true
|
||||
}
|
||||
|
||||
size, err := dirSize(worldPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
worlds = append(worlds, Summary{
|
||||
Name: entry.Name(),
|
||||
Path: entry.Name(),
|
||||
SizeBytes: size,
|
||||
HasLevelDat: hasLevelDat,
|
||||
})
|
||||
}
|
||||
|
||||
if activeWorldName != "" {
|
||||
found := false
|
||||
for _, world := range worlds {
|
||||
if world.Name == activeWorldName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
if err := ValidateWorld(dataPath, activeWorldName); err == nil {
|
||||
size, sizeErr := dirSize(filepath.Join(dataPath, activeWorldName))
|
||||
if sizeErr != nil {
|
||||
return nil, sizeErr
|
||||
}
|
||||
worlds = append(worlds, Summary{
|
||||
Name: activeWorldName,
|
||||
Path: activeWorldName,
|
||||
SizeBytes: size,
|
||||
HasLevelDat: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return worlds, nil
|
||||
}
|
||||
|
||||
func dirSize(root string) (int64, error) {
|
||||
var total int64
|
||||
|
||||
err := filepath.Walk(root, func(_ string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
total += info.Size()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return total, err
|
||||
}
|
||||
@@ -314,6 +314,62 @@ func (c *Client) dispatch(ctx context.Context, env *protocol.Envelope) {
|
||||
}
|
||||
c.runtime.HandleFilesWrite(ctx, env.MessageID, payload)
|
||||
|
||||
case protocol.TypeServerBackupPrepare:
|
||||
var meta protocol.OperationMeta
|
||||
if err := json.Unmarshal(env.Payload, &meta); err != nil {
|
||||
c.log.Warn("decode server.backup.prepare", "error", err)
|
||||
return
|
||||
}
|
||||
c.runtime.HandleBackupPrepare(ctx, env.MessageID, meta)
|
||||
|
||||
case protocol.TypeServerBackupRelease:
|
||||
var meta protocol.OperationMeta
|
||||
if err := json.Unmarshal(env.Payload, &meta); err != nil {
|
||||
c.log.Warn("decode server.backup.release", "error", err)
|
||||
return
|
||||
}
|
||||
c.runtime.HandleBackupRelease(ctx, env.MessageID, meta)
|
||||
|
||||
case protocol.TypeServerWorldValidate:
|
||||
var payload protocol.ServerWorldValidatePayload
|
||||
if err := json.Unmarshal(env.Payload, &payload); err != nil {
|
||||
c.log.Warn("decode server.world.validate", "error", err)
|
||||
return
|
||||
}
|
||||
c.runtime.HandleWorldValidate(ctx, env.MessageID, payload)
|
||||
|
||||
case protocol.TypeServerWorldArchive:
|
||||
var payload protocol.ServerWorldArchivePayload
|
||||
if err := json.Unmarshal(env.Payload, &payload); err != nil {
|
||||
c.log.Warn("decode server.world.archive", "error", err)
|
||||
return
|
||||
}
|
||||
c.runtime.HandleWorldArchive(ctx, env.MessageID, payload)
|
||||
|
||||
case protocol.TypeServerStorageUpload:
|
||||
var payload protocol.ServerStorageUploadPayload
|
||||
if err := json.Unmarshal(env.Payload, &payload); err != nil {
|
||||
c.log.Warn("decode server.storage.upload", "error", err)
|
||||
return
|
||||
}
|
||||
c.runtime.HandleStorageUpload(ctx, env.MessageID, payload)
|
||||
|
||||
case protocol.TypeServerStorageDownload:
|
||||
var payload protocol.ServerStorageDownloadPayload
|
||||
if err := json.Unmarshal(env.Payload, &payload); err != nil {
|
||||
c.log.Warn("decode server.storage.download", "error", err)
|
||||
return
|
||||
}
|
||||
c.runtime.HandleStorageDownload(ctx, env.MessageID, payload)
|
||||
|
||||
case protocol.TypeServerWorldReplace:
|
||||
var payload protocol.ServerWorldReplacePayload
|
||||
if err := json.Unmarshal(env.Payload, &payload); err != nil {
|
||||
c.log.Warn("decode server.world.replace", "error", err)
|
||||
return
|
||||
}
|
||||
c.runtime.HandleWorldReplace(ctx, env.MessageID, payload)
|
||||
|
||||
default:
|
||||
c.log.Debug("ignored control message", "type", env.Type)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user