508890a232
diskUsage now carries Sources []string instead of a single Source. collectDiskUsage accumulates all sources (samba, nfs, manual) per path. NFS no longer adds a redundant label to used_by. Dashboard and Settings render one chip per source. When a path is shared via SMB and NFS, both chips appear. API breaking: disks[].source replaced by disks[].sources[]. Version: 0.2.1 -> 0.3.0
205 lines
4.9 KiB
Go
205 lines
4.9 KiB
Go
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)
|
|
}
|