package web import ( "encoding/json" "net/http" "sort" "strings" "syscall" "github.com/go-chi/chi/v5" "github.com/darroyo/nasctl/internal/system" ) type diskUsage struct { Path string `json:"path"` TotalBytes uint64 `json:"total_bytes"` FreeBytes uint64 `json:"free_bytes"` UsedBytes uint64 `json:"used_bytes"` UsedPercent float64 `json:"used_percent"` Source string `json:"source"` UsedBy []string `json:"used_by,omitempty"` Available bool `json:"available"` Error string `json:"error,omitempty"` } type serviceStatus struct { Name string `json:"name"` Active bool `json:"active"` State string `json:"state"` } func (s *Server) handleSystemStatus(w http.ResponseWriter, r *http.Request) { status := map[string]any{ "disks": s.collectDiskUsage(), "services": s.collectServiceStatus(r), } writeJSON(w, http.StatusOK, status) } func (s *Server) handleVersion(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{ "version": Version, "commit": Commit, }) } func (s *Server) collectDiskUsage() []diskUsage { entries := make(map[string]*diskUsage) shares, err := s.DB.ListSambaShares() if err == nil { for _, share := range shares { key := share.Path if e, ok := entries[key]; ok { e.UsedBy = append(e.UsedBy, share.Name) } else { entries[key] = &diskUsage{Path: key, Source: "samba", UsedBy: []string{share.Name}, Available: true} } } } exports, err := s.DB.ListNFSExports() if err == nil { for _, exp := range exports { key := exp.Path label := "nfs:" + exp.Path if e, ok := entries[key]; ok { e.UsedBy = append(e.UsedBy, label) } else { entries[key] = &diskUsage{Path: key, Source: "nfs", UsedBy: []string{label}, Available: true} } } } watched, err := s.DB.ListWatchedMounts() if err == nil { for _, m := range watched { key := m.Path if _, ok := entries[key]; !ok { entries[key] = &diskUsage{Path: key, Source: "manual", Available: true} } } } var result []diskUsage for _, e := range entries { fillDiskUsage(e) result = append(result, *e) } sort.Slice(result, func(i, j int) bool { if result[i].Source != result[j].Source { return result[i].Source < result[j].Source } return result[i].Path < result[j].Path }) return result } func fillDiskUsage(e *diskUsage) { var stat syscall.Statfs_t if err := syscall.Statfs(e.Path, &stat); err != nil { e.Available = false e.Error = "path no disponible" return } e.Available = true e.TotalBytes = stat.Blocks * uint64(stat.Bsize) e.FreeBytes = stat.Bavail * uint64(stat.Bsize) e.UsedBytes = e.TotalBytes - e.FreeBytes if e.TotalBytes > 0 { e.UsedPercent = float64(e.UsedBytes) / float64(e.TotalBytes) * 100 } } func (s *Server) collectServiceStatus(r *http.Request) []serviceStatus { services := []string{"smbd", "nfs-server"} var statuses []serviceStatus for _, name := range services { stdout, _, err := system.Run(r.Context(), "systemctl", "is-active", name) state := strings.TrimSpace(stdout) if state == "" && err != nil { state = "unknown" } statuses = append(statuses, serviceStatus{ Name: name, Active: state == "active", State: state, }) } return statuses } func (s *Server) handleListWatchedMounts(w http.ResponseWriter, r *http.Request) { mounts, err := s.DB.ListWatchedMounts() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{"mounts": mounts}) } func (s *Server) handleCreateWatchedMount(w http.ResponseWriter, r *http.Request) { var req struct { Path string `json:"path"` } defer r.Body.Close() if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if err := system.ValidatePathAllowed(req.Path, s.AllowedRoots); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } exists, err := s.DB.WatchedMountPathExists(req.Path) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if exists { writeError(w, http.StatusConflict, "path already watched") return } mount, err := s.DB.CreateWatchedMount(req.Path) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusCreated, mount) } func (s *Server) handleDeleteWatchedMount(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.DeleteWatchedMount(id); err != nil { writeError(w, http.StatusNotFound, err.Error()) return } w.WriteHeader(http.StatusNoContent) }