Files
Vantz Stockwell e9f9b449b1
All checks were successful
Test Asgard Runner / test (push) Successful in 2s
feat: Add file manager package — VueFinder-compatible NATS request-reply handler
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>
2026-02-21 16:11:59 -05:00

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)
}
}