Phase5
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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()
|
||||
|
||||
34
apps/node-agent/internal/transfer/hash.go
Normal file
34
apps/node-agent/internal/transfer/hash.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user