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"` Sources []string `json:"sources"` 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 addSource(e *diskUsage, src string) { for _, s := range e.Sources { if s == src { return } } e.Sources = append(e.Sources, src) } 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 e, ok := entries[key] if !ok { e = &diskUsage{Path: key, Sources: []string{}, Available: true} entries[key] = e } addSource(e, "samba") e.UsedBy = append(e.UsedBy, share.Name) } } exports, err := s.DB.ListNFSExports() if err == nil { for _, exp := range exports { key := exp.Path e, ok := entries[key] if !ok { e = &diskUsage{Path: key, Sources: []string{}, Available: true} entries[key] = e } addSource(e, "nfs") } } watched, err := s.DB.ListWatchedMounts() if err == nil { for _, m := range watched { key := m.Path e, ok := entries[key] if !ok { e = &diskUsage{Path: key, Sources: []string{}, Available: true} entries[key] = e } addSource(e, "manual") } } var result []diskUsage for _, e := range entries { sort.Strings(e.Sources) fillDiskUsage(e) result = append(result, *e) } sort.Slice(result, func(i, j int) bool { if len(result[i].Sources) == 0 || len(result[j].Sources) == 0 { return len(result[i].Sources) > 0 } if result[i].Sources[0] != result[j].Sources[0] { return result[i].Sources[0] < result[j].Sources[0] } 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) }