feat: add storage info to settings page and improve NFS export handling
Backend: - Add source/used_by/available/error fields to diskUsage in system status - collectDiskUsage() now reads /proc/mounts, samba shares and NFS exports from DB - Paths are deduplicated; shared paths list all shares/exports using them - syscall.Statfs errors surface as available=false with user-facing error - collectServiceStatus made a method of Server (receiver consistency) Frontend: - Settings page now shows two cards: mount points and shared resources - Each path shows source badge (Sistema/Mount/SMB/NFS), used_by chips, progress bar - Unavailable paths show amber warning instead of progress bar - DiskUsage interface updated with new fields - NFSExport interface updated with structured fields (fsid, async, etc) - NFS page updated to use new export fields
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
@@ -17,7 +16,13 @@ import (
|
||||
type nfsExportRequest struct {
|
||||
Path string `json:"path"`
|
||||
Clients []string `json:"clients"`
|
||||
Options string `json:"options"`
|
||||
// Main options
|
||||
ReadOnly bool `json:"read_only"`
|
||||
Async bool `json:"async"`
|
||||
RootSquash bool `json:"root_squash"`
|
||||
SubtreeCheck bool `json:"subtree_check"`
|
||||
// Advanced options stored as JSON string
|
||||
Advanced string `json:"advanced"`
|
||||
}
|
||||
|
||||
func (req nfsExportRequest) validate(allowedRoots []string) error {
|
||||
@@ -29,21 +34,18 @@ func (req nfsExportRequest) validate(allowedRoots []string) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(req.Options) == "" {
|
||||
return nil
|
||||
}
|
||||
return system.ValidateNFSOptions(req.Options)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (req nfsExportRequest) toModel() db.NFSExport {
|
||||
options := strings.TrimSpace(req.Options)
|
||||
if options == "" {
|
||||
options = "rw,sync,no_root_squash"
|
||||
}
|
||||
return db.NFSExport{
|
||||
Path: req.Path,
|
||||
Clients: req.Clients,
|
||||
Options: options,
|
||||
Path: req.Path,
|
||||
Clients: req.Clients,
|
||||
ReadOnly: req.ReadOnly,
|
||||
Async: req.Async,
|
||||
RootSquash: req.RootSquash,
|
||||
SubtreeCheck: req.SubtreeCheck,
|
||||
Advanced: req.Advanced,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +72,22 @@ func (s *Server) handleGetNFSExport(w http.ResponseWriter, r *http.Request) {
|
||||
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 {
|
||||
@@ -81,7 +99,15 @@ func (s *Server) handleCreateNFSExport(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
export, err := s.DB.CreateNFSExport(req.toModel())
|
||||
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
|
||||
@@ -99,6 +125,12 @@ func (s *Server) handleUpdateNFSExport(w http.ResponseWriter, r *http.Request) {
|
||||
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())
|
||||
@@ -109,7 +141,11 @@ func (s *Server) handleUpdateNFSExport(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
export, err := s.DB.UpdateNFSExport(id, req.toModel())
|
||||
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
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
@@ -9,11 +12,15 @@ import (
|
||||
)
|
||||
|
||||
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"`
|
||||
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 {
|
||||
@@ -24,39 +31,97 @@ type serviceStatus struct {
|
||||
|
||||
func (s *Server) handleSystemStatus(w http.ResponseWriter, r *http.Request) {
|
||||
status := map[string]any{
|
||||
"disks": collectDiskUsage(),
|
||||
"services": collectServiceStatus(r),
|
||||
"disks": s.collectDiskUsage(),
|
||||
"services": s.collectServiceStatus(r),
|
||||
}
|
||||
writeJSON(w, http.StatusOK, status)
|
||||
}
|
||||
|
||||
func collectDiskUsage() []diskUsage {
|
||||
paths := []string{"/"}
|
||||
var usages []diskUsage
|
||||
for _, path := range paths {
|
||||
var stat syscall.Statfs_t
|
||||
if err := syscall.Statfs(path, &stat); err != nil {
|
||||
continue
|
||||
func (s *Server) collectDiskUsage() []diskUsage {
|
||||
entries := make(map[string]*diskUsage)
|
||||
|
||||
entries["/"] = &diskUsage{Path: "/", Source: "system", Available: true}
|
||||
|
||||
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}
|
||||
}
|
||||
}
|
||||
total := stat.Blocks * uint64(stat.Bsize)
|
||||
free := stat.Bavail * uint64(stat.Bsize)
|
||||
used := total - free
|
||||
var pct float64
|
||||
if total > 0 {
|
||||
pct = float64(used) / float64(total) * 100
|
||||
}
|
||||
usages = append(usages, diskUsage{
|
||||
Path: path,
|
||||
TotalBytes: total,
|
||||
FreeBytes: free,
|
||||
UsedBytes: used,
|
||||
UsedPercent: pct,
|
||||
})
|
||||
}
|
||||
return usages
|
||||
|
||||
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}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readProcMounts(entries)
|
||||
|
||||
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 collectServiceStatus(r *http.Request) []serviceStatus {
|
||||
func readProcMounts(entries map[string]*diskUsage) {
|
||||
f, err := os.Open("/proc/mounts")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) < 3 {
|
||||
continue
|
||||
}
|
||||
mountPoint := fields[1]
|
||||
if _, ok := entries[mountPoint]; !ok {
|
||||
entries[mountPoint] = &diskUsage{Path: mountPoint, Source: "mount", Available: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user