forked from wrenn/wrenn
237 lines
6.2 KiB
Go
237 lines
6.2 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"connectrpc.com/connect"
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
|
"git.omukk.dev/wrenn/wrenn/internal/db"
|
|
"git.omukk.dev/wrenn/wrenn/internal/id"
|
|
"git.omukk.dev/wrenn/wrenn/internal/lifecycle"
|
|
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
|
)
|
|
|
|
type fsHandler struct {
|
|
db *db.Queries
|
|
pool *lifecycle.HostClientPool
|
|
}
|
|
|
|
func newFSHandler(db *db.Queries, pool *lifecycle.HostClientPool) *fsHandler {
|
|
return &fsHandler{db: db, pool: pool}
|
|
}
|
|
|
|
type listDirRequest struct {
|
|
Path string `json:"path"`
|
|
Depth uint32 `json:"depth"`
|
|
}
|
|
|
|
type fileEntryResponse struct {
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
Type string `json:"type"`
|
|
Size int64 `json:"size"`
|
|
Mode uint32 `json:"mode"`
|
|
Permissions string `json:"permissions"`
|
|
Owner string `json:"owner"`
|
|
Group string `json:"group"`
|
|
ModifiedAt int64 `json:"modified_at"`
|
|
SymlinkTarget *string `json:"symlink_target,omitempty"`
|
|
}
|
|
|
|
type listDirResponse struct {
|
|
Entries []fileEntryResponse `json:"entries"`
|
|
}
|
|
|
|
type makeDirRequest struct {
|
|
Path string `json:"path"`
|
|
}
|
|
|
|
type makeDirResponse struct {
|
|
Entry fileEntryResponse `json:"entry"`
|
|
}
|
|
|
|
type removeRequest struct {
|
|
Path string `json:"path"`
|
|
}
|
|
|
|
// ListDir handles POST /v1/capsules/{id}/files/list.
|
|
func (h *fsHandler) ListDir(w http.ResponseWriter, r *http.Request) {
|
|
sandboxIDStr := chi.URLParam(r, "id")
|
|
ctx := r.Context()
|
|
ac := auth.MustFromContext(ctx)
|
|
|
|
sandboxID, err := id.ParseSandboxID(sandboxIDStr)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", "invalid sandbox ID")
|
|
return
|
|
}
|
|
|
|
sb, err := h.db.GetSandboxByTeam(ctx, db.GetSandboxByTeamParams{ID: sandboxID, TeamID: ac.TeamID})
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "not_found", "sandbox not found")
|
|
return
|
|
}
|
|
if sb.Status != "running" {
|
|
writeError(w, http.StatusConflict, "invalid_state", "sandbox is not running")
|
|
return
|
|
}
|
|
|
|
var req listDirRequest
|
|
if err := decodeJSON(r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", "invalid JSON body")
|
|
return
|
|
}
|
|
if req.Path == "" {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", "path is required")
|
|
return
|
|
}
|
|
|
|
agent, err := agentForHost(ctx, h.db, h.pool, sb.HostID)
|
|
if err != nil {
|
|
writeError(w, http.StatusServiceUnavailable, "host_unavailable", "sandbox host is not reachable")
|
|
return
|
|
}
|
|
|
|
resp, err := agent.ListDir(ctx, connect.NewRequest(&pb.ListDirRequest{
|
|
SandboxId: sandboxIDStr,
|
|
Path: req.Path,
|
|
Depth: req.Depth,
|
|
}))
|
|
if err != nil {
|
|
status, code, msg := agentErrToHTTP(err)
|
|
writeError(w, status, code, msg)
|
|
return
|
|
}
|
|
|
|
entries := make([]fileEntryResponse, 0, len(resp.Msg.Entries))
|
|
for _, e := range resp.Msg.Entries {
|
|
entries = append(entries, fileEntryFromPB(e))
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, listDirResponse{Entries: entries})
|
|
}
|
|
|
|
// MakeDir handles POST /v1/capsules/{id}/files/mkdir.
|
|
func (h *fsHandler) MakeDir(w http.ResponseWriter, r *http.Request) {
|
|
sandboxIDStr := chi.URLParam(r, "id")
|
|
ctx := r.Context()
|
|
ac := auth.MustFromContext(ctx)
|
|
|
|
sandboxID, err := id.ParseSandboxID(sandboxIDStr)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", "invalid sandbox ID")
|
|
return
|
|
}
|
|
|
|
sb, err := h.db.GetSandboxByTeam(ctx, db.GetSandboxByTeamParams{ID: sandboxID, TeamID: ac.TeamID})
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "not_found", "sandbox not found")
|
|
return
|
|
}
|
|
if sb.Status != "running" {
|
|
writeError(w, http.StatusConflict, "invalid_state", "sandbox is not running")
|
|
return
|
|
}
|
|
|
|
var req makeDirRequest
|
|
if err := decodeJSON(r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", "invalid JSON body")
|
|
return
|
|
}
|
|
if req.Path == "" {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", "path is required")
|
|
return
|
|
}
|
|
|
|
agent, err := agentForHost(ctx, h.db, h.pool, sb.HostID)
|
|
if err != nil {
|
|
writeError(w, http.StatusServiceUnavailable, "host_unavailable", "sandbox host is not reachable")
|
|
return
|
|
}
|
|
|
|
resp, err := agent.MakeDir(ctx, connect.NewRequest(&pb.MakeDirRequest{
|
|
SandboxId: sandboxIDStr,
|
|
Path: req.Path,
|
|
}))
|
|
if err != nil {
|
|
status, code, msg := agentErrToHTTP(err)
|
|
writeError(w, status, code, msg)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, makeDirResponse{Entry: fileEntryFromPB(resp.Msg.Entry)})
|
|
}
|
|
|
|
// Remove handles POST /v1/capsules/{id}/files/remove.
|
|
func (h *fsHandler) Remove(w http.ResponseWriter, r *http.Request) {
|
|
sandboxIDStr := chi.URLParam(r, "id")
|
|
ctx := r.Context()
|
|
ac := auth.MustFromContext(ctx)
|
|
|
|
sandboxID, err := id.ParseSandboxID(sandboxIDStr)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", "invalid sandbox ID")
|
|
return
|
|
}
|
|
|
|
sb, err := h.db.GetSandboxByTeam(ctx, db.GetSandboxByTeamParams{ID: sandboxID, TeamID: ac.TeamID})
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "not_found", "sandbox not found")
|
|
return
|
|
}
|
|
if sb.Status != "running" {
|
|
writeError(w, http.StatusConflict, "invalid_state", "sandbox is not running")
|
|
return
|
|
}
|
|
|
|
var req removeRequest
|
|
if err := decodeJSON(r, &req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", "invalid JSON body")
|
|
return
|
|
}
|
|
if req.Path == "" {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", "path is required")
|
|
return
|
|
}
|
|
|
|
agent, err := agentForHost(ctx, h.db, h.pool, sb.HostID)
|
|
if err != nil {
|
|
writeError(w, http.StatusServiceUnavailable, "host_unavailable", "sandbox host is not reachable")
|
|
return
|
|
}
|
|
|
|
if _, err := agent.RemovePath(ctx, connect.NewRequest(&pb.RemovePathRequest{
|
|
SandboxId: sandboxIDStr,
|
|
Path: req.Path,
|
|
})); err != nil {
|
|
status, code, msg := agentErrToHTTP(err)
|
|
writeError(w, status, code, msg)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func fileEntryFromPB(e *pb.FileEntry) fileEntryResponse {
|
|
if e == nil {
|
|
return fileEntryResponse{}
|
|
}
|
|
resp := fileEntryResponse{
|
|
Name: e.Name,
|
|
Path: e.Path,
|
|
Type: e.Type,
|
|
Size: e.Size,
|
|
Mode: e.Mode,
|
|
Permissions: e.Permissions,
|
|
Owner: e.Owner,
|
|
Group: e.Group,
|
|
ModifiedAt: e.ModifiedAt,
|
|
}
|
|
if e.SymlinkTarget != nil {
|
|
resp.SymlinkTarget = e.SymlinkTarget
|
|
}
|
|
return resp
|
|
}
|