package web import ( "context" "encoding/json" "io" "net/http" "github.com/go-chi/chi/v5" "github.com/darroyo/nasctl/internal/db" "github.com/darroyo/nasctl/internal/modules/nfs" "github.com/darroyo/nasctl/internal/system" ) type nfsClientRequest struct { Host string `json:"host"` ReadOnly bool `json:"read_only"` Async bool `json:"async"` RootSquash bool `json:"root_squash"` SubtreeCheck bool `json:"subtree_check"` Advanced db.NFSAdvanced `json:"advanced"` } type nfsExportRequest struct { Path string `json:"path"` Clients []nfsClientRequest `json:"clients"` // Main options (used as template defaults for new hosts) ReadOnly bool `json:"read_only"` Async bool `json:"async"` RootSquash bool `json:"root_squash"` SubtreeCheck bool `json:"subtree_check"` Advanced string `json:"advanced"` } func (req nfsExportRequest) validate(allowedRoots []string) error { if err := system.ValidatePathAllowed(req.Path, allowedRoots); err != nil { return err } for _, client := range req.Clients { if err := system.ValidateNFSClient(client.Host); err != nil { return err } } return nil } func (req nfsExportRequest) toModel() db.NFSExport { clients := make([]db.NFSClient, 0, len(req.Clients)) for _, c := range req.Clients { clients = append(clients, db.NFSClient{ Host: c.Host, ReadOnly: c.ReadOnly, Async: c.Async, RootSquash: c.RootSquash, SubtreeCheck: c.SubtreeCheck, Advanced: c.Advanced, }) } return db.NFSExport{ Path: req.Path, Clients: clients, ReadOnly: req.ReadOnly, Async: req.Async, RootSquash: req.RootSquash, SubtreeCheck: req.SubtreeCheck, Advanced: req.Advanced, } } func (s *Server) handleListNFSExports(w http.ResponseWriter, r *http.Request) { exports, err := s.DB.ListNFSExports() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{"exports": exports}) } func (s *Server) handleGetNFSExport(w http.ResponseWriter, r *http.Request) { id, err := parseID(chi.URLParam(r, "id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } export, err := s.DB.GetNFSExport(id) if err != nil { writeError(w, http.StatusNotFound, err.Error()) return } writeJSON(w, http.StatusOK, export) } func generateFSID(ctx context.Context, dbConn *db.DB) (int64, error) { used, err := dbConn.GetAllFSIDs() if err != nil { return 0, err } usedSet := make(map[uint32]struct{}, len(used)) for _, id := range used { usedSet[uint32(id)] = struct{}{} } n, err := nfs.UniqueFSID(usedSet) if err != nil { return 0, err } return int64(n), nil } func (s *Server) handleCreateNFSExport(w http.ResponseWriter, r *http.Request) { req, err := decodeNFSExportRequest(r.Body) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if err := req.validate(s.AllowedRoots); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } model := req.toModel() fsid, err := generateFSID(r.Context(), s.DB) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } model.FSID = fsid export, err := s.DB.CreateNFSExport(model) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if err := s.DB.MarkDirty(nfs.ModuleName); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusCreated, export) } func (s *Server) handleUpdateNFSExport(w http.ResponseWriter, r *http.Request) { id, err := parseID(chi.URLParam(r, "id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } existing, err := s.DB.GetNFSExport(id) if err != nil { writeError(w, http.StatusNotFound, err.Error()) return } req, err := decodeNFSExportRequest(r.Body) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if err := req.validate(s.AllowedRoots); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } model := req.toModel() model.ID = id model.FSID = existing.FSID export, err := s.DB.UpdateNFSExport(id, model) if err != nil { writeError(w, http.StatusNotFound, err.Error()) return } if err := s.DB.MarkDirty(nfs.ModuleName); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, export) } func (s *Server) handleDeleteNFSExport(w http.ResponseWriter, r *http.Request) { id, err := parseID(chi.URLParam(r, "id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } if err := s.DB.DeleteNFSExport(id); err != nil { writeError(w, http.StatusNotFound, err.Error()) return } if err := s.DB.MarkDirty(nfs.ModuleName); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } w.WriteHeader(http.StatusNoContent) } func decodeNFSExportRequest(body io.ReadCloser) (nfsExportRequest, error) { defer body.Close() var req nfsExportRequest if err := json.NewDecoder(body).Decode(&req); err != nil { return nfsExportRequest{}, err } return req, nil }