All checks were successful
Test Asgard Runner / test (push) Successful in 2s
Implements companion-agent/internal/filemanager with full installDir jail
enforcement (Clean + EvalSymlinks + HasPrefix on every path). Handles all
VueFinder operations: list, delete, rename, copy, move, mkdir, mkfile,
search, preview, save, upload. Wires into daemon.go as a 6th NATS
subscription on corrosion.{license_id}.files.cmd.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
210 lines
6.4 KiB
Go
210 lines
6.4 KiB
Go
package filemanager
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
)
|
|
|
|
// HandleNatsRequest is the NATS message handler for the file manager command
|
|
// subject (corrosion.{license_id}.files.cmd). It deserialises the request,
|
|
// routes to the correct FileManager operation, and calls msg.Respond with a
|
|
// NatsResponse JSON payload — either success with data or a structured error.
|
|
func (fm *FileManager) HandleNatsRequest(msg *nats.Msg) {
|
|
var req NatsRequest
|
|
if err := json.Unmarshal(msg.Data, &req); err != nil {
|
|
log.Printf("filemanager: invalid NATS request payload: %v", err)
|
|
respondError(msg, "invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
log.Printf("filemanager: handling %s path=%q", req.Func, req.Path)
|
|
|
|
switch req.Func {
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Directory listing
|
|
// -----------------------------------------------------------------------
|
|
case FuncList:
|
|
result, err := fm.List(req.Path)
|
|
if err != nil {
|
|
respondError(msg, err.Error())
|
|
return
|
|
}
|
|
respondJSON(msg, result)
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Delete
|
|
// -----------------------------------------------------------------------
|
|
case FuncDelete:
|
|
result, err := fm.Delete(req.Path, req.Items)
|
|
if err != nil {
|
|
respondError(msg, err.Error())
|
|
return
|
|
}
|
|
respondJSON(msg, result)
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Rename — req.Name holds the new basename
|
|
// -----------------------------------------------------------------------
|
|
case FuncRename:
|
|
if len(req.Items) == 0 {
|
|
respondError(msg, "rename requires at least one item")
|
|
return
|
|
}
|
|
result, err := fm.Rename(req.Path, req.Items[0], req.Name)
|
|
if err != nil {
|
|
respondError(msg, err.Error())
|
|
return
|
|
}
|
|
respondJSON(msg, result)
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Copy
|
|
// -----------------------------------------------------------------------
|
|
case FuncCopy:
|
|
if req.Destination == "" {
|
|
respondError(msg, "copy requires a destination path")
|
|
return
|
|
}
|
|
result, err := fm.Copy(req.Path, req.Items, req.Destination)
|
|
if err != nil {
|
|
respondError(msg, err.Error())
|
|
return
|
|
}
|
|
respondJSON(msg, result)
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Move
|
|
// -----------------------------------------------------------------------
|
|
case FuncMove:
|
|
if req.Destination == "" {
|
|
respondError(msg, "move requires a destination path")
|
|
return
|
|
}
|
|
result, err := fm.Move(req.Path, req.Items, req.Destination)
|
|
if err != nil {
|
|
respondError(msg, err.Error())
|
|
return
|
|
}
|
|
respondJSON(msg, result)
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Create directory
|
|
// -----------------------------------------------------------------------
|
|
case FuncCreateFolder:
|
|
if req.Name == "" {
|
|
respondError(msg, "mkdir requires a folder name")
|
|
return
|
|
}
|
|
result, err := fm.CreateFolder(req.Path, req.Name)
|
|
if err != nil {
|
|
respondError(msg, err.Error())
|
|
return
|
|
}
|
|
respondJSON(msg, result)
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Create empty file
|
|
// -----------------------------------------------------------------------
|
|
case FuncCreateFile:
|
|
if req.Name == "" {
|
|
respondError(msg, "mkfile requires a file name")
|
|
return
|
|
}
|
|
result, err := fm.CreateFile(req.Path, req.Name)
|
|
if err != nil {
|
|
respondError(msg, err.Error())
|
|
return
|
|
}
|
|
respondJSON(msg, result)
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Search
|
|
// -----------------------------------------------------------------------
|
|
case FuncSearch:
|
|
result, err := fm.Search(req.Path, req.Filter)
|
|
if err != nil {
|
|
respondError(msg, err.Error())
|
|
return
|
|
}
|
|
respondJSON(msg, result)
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Preview / read file content (VueFinder uses "fm_preview" for text files)
|
|
// -----------------------------------------------------------------------
|
|
case FuncPreview:
|
|
content, err := fm.GetContent(req.Path)
|
|
if err != nil {
|
|
respondError(msg, err.Error())
|
|
return
|
|
}
|
|
respondJSON(msg, map[string]string{"content": content})
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Save file content
|
|
// -----------------------------------------------------------------------
|
|
case FuncSave:
|
|
if err := fm.SaveContent(req.Path, req.Content); err != nil {
|
|
respondError(msg, err.Error())
|
|
return
|
|
}
|
|
respondJSON(msg, map[string]string{"status": "saved"})
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Upload — content arrives as a base64-encoded string
|
|
// -----------------------------------------------------------------------
|
|
case FuncUpload:
|
|
if req.Filename == "" {
|
|
respondError(msg, "upload requires a filename")
|
|
return
|
|
}
|
|
data, err := DecodeBase64(req.Content)
|
|
if err != nil {
|
|
respondError(msg, err.Error())
|
|
return
|
|
}
|
|
result, err := fm.Upload(req.Path, req.Filename, data)
|
|
if err != nil {
|
|
respondError(msg, err.Error())
|
|
return
|
|
}
|
|
respondJSON(msg, result)
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Unknown function
|
|
// -----------------------------------------------------------------------
|
|
default:
|
|
log.Printf("filemanager: unknown function %q", req.Func)
|
|
respondError(msg, "unknown function: "+req.Func)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Response helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// respondJSON sends a successful NatsResponse wrapping data.
|
|
func respondJSON(msg *nats.Msg, data interface{}) {
|
|
resp := NatsResponse{Success: true, Data: data}
|
|
bytes, err := json.Marshal(resp)
|
|
if err != nil {
|
|
log.Printf("filemanager: failed to marshal success response: %v", err)
|
|
respondError(msg, "internal: failed to marshal response")
|
|
return
|
|
}
|
|
if err := msg.Respond(bytes); err != nil {
|
|
log.Printf("filemanager: failed to send response: %v", err)
|
|
}
|
|
}
|
|
|
|
// respondError sends a failed NatsResponse with the given error message.
|
|
func respondError(msg *nats.Msg, errMsg string) {
|
|
resp := NatsResponse{Success: false, Error: errMsg}
|
|
bytes, _ := json.Marshal(resp)
|
|
if err := msg.Respond(bytes); err != nil {
|
|
log.Printf("filemanager: failed to send error response: %v", err)
|
|
}
|
|
}
|