Files
meta-core/internal/mounts/handlers.go
T
2026-05-25 23:47:30 +02:00

335 lines
8.7 KiB
Go

package mounts
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
)
// Handlers provides HTTP handlers for mount operations
type Handlers struct {
manager *Manager
scanFunc ScanFunc
}
// NewHandlers creates new mount handlers
func NewHandlers(manager *Manager, scanFunc ScanFunc) *Handlers {
return &Handlers{
manager: manager,
scanFunc: scanFunc,
}
}
// RegisterRoutes registers all mount-related routes
func (h *Handlers) RegisterRoutes(r *mux.Router) {
// Mount management
r.HandleFunc("/api/mounts", h.handleListMounts).Methods("GET")
r.HandleFunc("/api/mounts", h.handleCreateMount).Methods("POST")
r.HandleFunc("/api/mounts/rclone/remotes", h.handleListRcloneRemotes).Methods("GET")
r.HandleFunc("/api/mounts/{id}", h.handleGetMount).Methods("GET")
r.HandleFunc("/api/mounts/{id}", h.handleUpdateMount).Methods("PUT", "PATCH")
r.HandleFunc("/api/mounts/{id}", h.handleDeleteMount).Methods("DELETE")
r.HandleFunc("/api/mounts/{id}/mount", h.handleRequestMount).Methods("POST")
r.HandleFunc("/api/mounts/{id}/unmount", h.handleRequestUnmount).Methods("POST")
r.HandleFunc("/api/mounts/{id}/safe-unmount", h.handleSafeUnmount).Methods("POST")
r.HandleFunc("/api/mounts/{id}/scan", h.handleTriggerMountScan).Methods("POST")
}
// handleListMounts handles GET /api/mounts
func (h *Handlers) handleListMounts(w http.ResponseWriter, r *http.Request) {
mounts, err := h.manager.ListMounts()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, MountsListResponse{Mounts: mounts})
}
// handleGetMount handles GET /api/mounts/{id}
func (h *Handlers) handleGetMount(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
mount, err := h.manager.GetMount(id)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if mount == nil {
writeError(w, http.StatusNotFound, "mount not found")
return
}
writeJSON(w, http.StatusOK, MountResponse{Mount: mount})
}
// handleCreateMount handles POST /api/mounts
func (h *Handlers) handleCreateMount(w http.ResponseWriter, r *http.Request) {
var req CreateMountRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
mount, err := h.manager.CreateMount(&req)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusCreated, MountResponse{Mount: mount})
}
// handleDeleteMount handles DELETE /api/mounts/{id}
func (h *Handlers) handleDeleteMount(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
if err := h.manager.DeleteMount(id); err != nil {
if err.Error() == "mount not found" {
writeError(w, http.StatusNotFound, err.Error())
return
}
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, StatusResponse{Status: "ok"})
}
// handleRequestMount handles POST /api/mounts/{id}/mount
func (h *Handlers) handleRequestMount(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
if err := h.manager.RequestMount(id); err != nil {
if err.Error() == "mount not found" {
writeError(w, http.StatusNotFound, err.Error())
return
}
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, StatusResponse{
Status: "ok",
Message: "Mount requested",
})
}
// handleRequestUnmount handles POST /api/mounts/{id}/unmount
func (h *Handlers) handleRequestUnmount(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
if err := h.manager.RequestUnmount(id); err != nil {
if err.Error() == "mount not found" {
writeError(w, http.StatusNotFound, err.Error())
return
}
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, StatusResponse{
Status: "ok",
Message: "Unmount requested",
})
}
// handleSafeUnmount handles POST /api/mounts/{id}/safe-unmount
func (h *Handlers) handleSafeUnmount(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
// Get timeout from query param (default 60000ms)
timeoutStr := r.URL.Query().Get("timeout")
timeoutMS := 60000
if timeoutStr != "" {
if parsed, err := parseIntOrDefault(timeoutStr, 60000); err == nil {
timeoutMS = parsed
}
}
// Get mount info
mount, err := h.manager.GetMount(id)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if mount == nil {
writeError(w, http.StatusNotFound, "mount not found")
return
}
// Request unmount
if err := h.manager.RequestUnmount(id); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
// Wait for unmount
success := h.manager.WaitForUnmount(mount.MountPath, timeoutMS)
if !success {
writeJSON(w, http.StatusOK, StatusResponse{
Status: "warning",
Message: "Unmount requested but mount may still be attached",
})
return
}
writeJSON(w, http.StatusOK, StatusResponse{
Status: "ok",
Message: "Safe unmount completed",
})
}
// handleListRcloneRemotes handles GET /api/mounts/rclone/remotes
func (h *Handlers) handleListRcloneRemotes(w http.ResponseWriter, r *http.Request) {
remotes, err := h.manager.ListRcloneRemotes()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, RcloneRemotesResponse{Remotes: remotes})
}
// handleUpdateMount handles PUT/PATCH /api/mounts/{id}
func (h *Handlers) handleUpdateMount(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
var updates map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
mount, err := h.manager.UpdateMount(id, updates)
if err != nil {
if err.Error() == "mount not found" {
writeError(w, http.StatusNotFound, err.Error())
return
}
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, MountResponse{Mount: mount})
}
// handleTriggerMountScan handles POST /api/mounts/{id}/scan
func (h *Handlers) handleTriggerMountScan(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
// Get mount info
mount, err := h.manager.GetMount(id)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if mount == nil {
writeError(w, http.StatusNotFound, "mount not found")
return
}
// Check if mount is mounted
if !mount.Mounted {
writeJSON(w, http.StatusBadRequest, MountScanResponse{
Status: "error",
Message: "mount is not currently mounted",
MountID: mount.ID,
MountPath: mount.MountPath,
})
return
}
// Check if scan function is available
if h.scanFunc == nil {
writeJSON(w, http.StatusServiceUnavailable, MountScanResponse{
Status: "error",
Message: "file watcher not available",
MountID: mount.ID,
MountPath: mount.MountPath,
})
return
}
// Trigger scan
fileCount, err := h.scanFunc(mount.MountPath)
if err != nil {
writeJSON(w, http.StatusInternalServerError, MountScanResponse{
Status: "error",
Message: err.Error(),
MountID: mount.ID,
MountPath: mount.MountPath,
})
return
}
writeJSON(w, http.StatusOK, MountScanResponse{
Status: "ok",
Message: "scan triggered successfully",
MountID: mount.ID,
MountPath: mount.MountPath,
FileCount: fileCount,
})
}
// Helper functions
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
// writeError writes the typed error envelope (see api/handlers.go for the
// canonical definition + slug vocabulary). Kept local to avoid an import
// cycle with the api package.
func writeError(w http.ResponseWriter, status int, message string) {
slug, retryable := slugForStatus(status)
writeJSON(w, status, map[string]interface{}{
"error": slug,
"message": message,
"retryable": retryable,
})
}
func slugForStatus(status int) (string, bool) {
switch status {
case http.StatusConflict:
return "alias_collision", false
case http.StatusNotFound:
return "unknown_root", false
case http.StatusBadRequest:
return "schema_violation", false
case http.StatusServiceUnavailable:
return "storage_unavailable", true
case http.StatusInternalServerError:
return "internal", true
default:
return "internal", status >= 500
}
}
func parseIntOrDefault(s string, defaultVal int) (int, error) {
var result int
if s == "" {
return defaultVal, nil
}
err := json.Unmarshal([]byte(s), &result)
if err != nil {
return defaultVal, nil
}
return result, nil
}