Phase5
Some checks failed
CI / Node — lint, typecheck, test, build (push) Failing after 9s
CI / Go — node-agent tests (push) Failing after 9s
CI / Go — edge-gateway build (push) Successful in 13s

This commit is contained in:
smueller
2026-06-26 13:00:01 +02:00
parent 4262464cd5
commit d29a02f2a4
93 changed files with 1875 additions and 55 deletions

View File

@@ -189,6 +189,10 @@ func mapSoftwareFamily(family string) string {
switch strings.ToUpper(strings.TrimSpace(family)) {
case "PAPER":
return "PAPER"
case "FABRIC":
return "FABRIC"
case "PURPUR":
return "PURPUR"
default:
return "VANILLA"
}

View File

@@ -229,6 +229,30 @@ func WriteFile(dataRoot, relativePath, content string, create bool) error {
return nil
}
// DeleteFile removes a file relative to dataRoot.
func DeleteFile(dataRoot, relativePath string) error {
filePath, err := ResolvePath(dataRoot, relativePath)
if err != nil {
return err
}
if err := rejectSymlinks(dataRoot, filePath); err != nil {
return err
}
info, err := os.Lstat(filePath)
if err != nil {
if os.IsNotExist(err) {
return ErrNotFound
}
return err
}
if info.IsDir() {
return ErrIsDirectory
}
return os.Remove(filePath)
}
func rejectSymlinks(dataRoot, target string) error {
root, err := filepath.Abs(filepath.Clean(dataRoot))
if err != nil {

View File

@@ -44,6 +44,8 @@ const (
TypeServerWorldReplace MessageType = "server.world.replace"
TypeServerStorageUpload MessageType = "server.storage.upload"
TypeServerStorageDownload MessageType = "server.storage.download"
TypeServerAddonInstall MessageType = "server.addon.install"
TypeServerAddonRemove MessageType = "server.addon.remove"
TypeServerMetricsSub MessageType = "server.metrics.subscribe"
TypeServerLogsSubscribe MessageType = "server.logs.subscribe"
)
@@ -80,6 +82,8 @@ var controlToAgentTypes = map[MessageType]struct{}{
TypeServerWorldReplace: {},
TypeServerStorageUpload: {},
TypeServerStorageDownload: {},
TypeServerAddonInstall: {},
TypeServerAddonRemove: {},
TypeServerMetricsSub: {},
TypeServerLogsSubscribe: {},
}
@@ -220,6 +224,20 @@ type ServerWorldReplacePayload struct {
ArchivePath string `json:"archivePath"`
}
// ServerAddonInstallPayload downloads and installs an addon file.
type ServerAddonInstallPayload struct {
OperationMeta
DownloadURL string `json:"downloadUrl"`
RelativePath string `json:"relativePath"`
Sha512 string `json:"sha512,omitempty"`
}
// ServerAddonRemovePayload removes an installed addon file.
type ServerAddonRemovePayload struct {
OperationMeta
RelativePath string `json:"relativePath"`
}
// FileEntry describes a file or directory in a list response.
type FileEntry struct {
Name string `json:"name"`

View File

@@ -570,6 +570,66 @@ func (m *Manager) HandleWorldReplace(ctx context.Context, correlationID string,
})
}
// HandleAddonInstall downloads and installs an addon JAR.
func (m *Manager) HandleAddonInstall(ctx context.Context, correlationID string, payload protocol.ServerAddonInstallPayload) {
meta := payload.OperationMeta
rec, ok := m.getServer(meta.ServerID)
if !ok {
m.failOperation(ctx, correlationID, meta, "addon.install", "SERVER_NOT_FOUND", "server is not provisioned on this node")
return
}
if rec.State == ServerStateRunning {
m.failOperation(ctx, correlationID, meta, "addon.install", "SERVER_RUNNING", "server must be stopped before installing addons")
return
}
localPath, err := files.ResolvePath(rec.DataPath, payload.RelativePath)
if err != nil {
m.failOperation(ctx, correlationID, meta, "addon.install", filesErrorCode(err), err.Error())
return
}
if err := transfer.DownloadFile(payload.DownloadURL, localPath); err != nil {
m.failOperation(ctx, correlationID, meta, "addon.install", "DOWNLOAD_FAILED", err.Error())
return
}
if err := transfer.VerifySha512File(localPath, payload.Sha512); err != nil {
_ = os.Remove(localPath)
m.failOperation(ctx, correlationID, meta, "addon.install", "HASH_MISMATCH", err.Error())
return
}
m.completeOperation(ctx, correlationID, meta, "addon.install", map[string]string{
"path": payload.RelativePath,
})
}
// HandleAddonRemove deletes an installed addon file.
func (m *Manager) HandleAddonRemove(ctx context.Context, correlationID string, payload protocol.ServerAddonRemovePayload) {
meta := payload.OperationMeta
rec, ok := m.getServer(meta.ServerID)
if !ok {
m.failOperation(ctx, correlationID, meta, "addon.remove", "SERVER_NOT_FOUND", "server is not provisioned on this node")
return
}
if rec.State == ServerStateRunning {
m.failOperation(ctx, correlationID, meta, "addon.remove", "SERVER_RUNNING", "server must be stopped before removing addons")
return
}
if err := files.DeleteFile(rec.DataPath, payload.RelativePath); err != nil {
m.failOperation(ctx, correlationID, meta, "addon.remove", filesErrorCode(err), err.Error())
return
}
m.completeOperation(ctx, correlationID, meta, "addon.remove", map[string]string{
"path": payload.RelativePath,
})
}
func (m *Manager) getServer(serverID string) (*serverRecord, bool) {
m.mu.RLock()
defer m.mu.RUnlock()

View File

@@ -0,0 +1,34 @@
package transfer
import (
"crypto/sha512"
"encoding/hex"
"fmt"
"io"
"os"
)
// VerifySha512File checks that a file matches the expected sha512 hash.
func VerifySha512File(path, expected string) error {
if expected == "" {
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
hasher := sha512.New()
if _, err := io.Copy(hasher, file); err != nil {
return err
}
actual := hex.EncodeToString(hasher.Sum(nil))
if actual != expected {
return fmt.Errorf("sha512 mismatch")
}
return nil
}

View File

@@ -370,6 +370,22 @@ func (c *Client) dispatch(ctx context.Context, env *protocol.Envelope) {
}
c.runtime.HandleWorldReplace(ctx, env.MessageID, payload)
case protocol.TypeServerAddonInstall:
var payload protocol.ServerAddonInstallPayload
if err := json.Unmarshal(env.Payload, &payload); err != nil {
c.log.Warn("decode server.addon.install", "error", err)
return
}
c.runtime.HandleAddonInstall(ctx, env.MessageID, payload)
case protocol.TypeServerAddonRemove:
var payload protocol.ServerAddonRemovePayload
if err := json.Unmarshal(env.Payload, &payload); err != nil {
c.log.Warn("decode server.addon.remove", "error", err)
return
}
c.runtime.HandleAddonRemove(ctx, env.MessageID, payload)
default:
c.log.Debug("ignored control message", "type", env.Type)
}