feat: Add file manager package — VueFinder-compatible NATS request-reply handler
All checks were successful
Test Asgard Runner / test (push) Successful in 2s
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>
This commit is contained in:
209
companion-agent/internal/filemanager/handler.go
Normal file
209
companion-agent/internal/filemanager/handler.go
Normal file
@@ -0,0 +1,209 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user