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:
@@ -0,0 +1,32 @@
|
||||
package nfs
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func generateRandomFSID() (uint32, error) {
|
||||
var b [4]byte
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
return 0, fmt.Errorf("crypto/rand read: %w", err)
|
||||
}
|
||||
n := binary.BigEndian.Uint32(b[:])
|
||||
if n == 0 {
|
||||
n = 1
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func UniqueFSID(used map[uint32]struct{}) (uint32, error) {
|
||||
for i := 0; i < 64; i++ {
|
||||
n, err := generateRandomFSID()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if _, dup := used[n]; !dup {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("fsid space exhausted after 64 attempts")
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package nfs
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUniqueFSIDNoCollision(t *testing.T) {
|
||||
const goroutines = 100
|
||||
results := make(chan uint32, goroutines)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < goroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
n, err := generateRandomFSID()
|
||||
if err != nil {
|
||||
t.Errorf("generateRandomFSID failed: %v", err)
|
||||
return
|
||||
}
|
||||
results <- n
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(results)
|
||||
|
||||
seen := make(map[uint32]struct{})
|
||||
for n := range results {
|
||||
if _, dup := seen[n]; dup {
|
||||
t.Errorf("duplicate fsid generated: %d", n)
|
||||
}
|
||||
seen[n] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniqueFSIDWithUsedSet(t *testing.T) {
|
||||
used := map[uint32]struct{}{
|
||||
1: {}, 2: {}, 3: {},
|
||||
}
|
||||
|
||||
for i := 0; i < 50; i++ {
|
||||
n, err := UniqueFSID(used)
|
||||
if err != nil {
|
||||
t.Fatalf("UniqueFSID failed at iteration %d: %v", i, err)
|
||||
}
|
||||
if n == 0 {
|
||||
t.Fatalf("fsid 0 is reserved")
|
||||
}
|
||||
if _, dup := used[n]; dup {
|
||||
t.Fatalf("returned duplicate fsid: %d", n)
|
||||
}
|
||||
used[n] = struct{}{}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -86,13 +87,62 @@ func (m *Module) Apply(ctx context.Context, database *db.DB) error {
|
||||
return database.ClearDirty(ModuleName)
|
||||
}
|
||||
|
||||
// clientSpec builds the "client(opts) client(opts)" segment of an exports line.
|
||||
// If no clients are configured, it defaults to "*(opts)".
|
||||
func clientSpec(clients []string, options string) string {
|
||||
opts := strings.TrimSpace(options)
|
||||
if opts == "" {
|
||||
opts = "ro"
|
||||
func buildFlags(e db.NFSExport) string {
|
||||
parts := make([]string, 0, 8)
|
||||
if e.ReadOnly {
|
||||
parts = append(parts, "ro")
|
||||
} else {
|
||||
parts = append(parts, "rw")
|
||||
}
|
||||
if e.Async {
|
||||
parts = append(parts, "async")
|
||||
} else {
|
||||
parts = append(parts, "sync")
|
||||
}
|
||||
if e.SubtreeCheck {
|
||||
parts = append(parts, "subtree_check")
|
||||
} else {
|
||||
parts = append(parts, "no_subtree_check")
|
||||
}
|
||||
if e.RootSquash {
|
||||
parts = append(parts, "root_squash")
|
||||
} else {
|
||||
parts = append(parts, "no_root_squash")
|
||||
}
|
||||
if e.Advanced != "" && e.Advanced != "{}" {
|
||||
var adv db.NFSAdvanced
|
||||
if err := json.Unmarshal([]byte(e.Advanced), &adv); err == nil {
|
||||
if adv.AllSquash {
|
||||
parts = append(parts, "all_squash")
|
||||
} else {
|
||||
parts = append(parts, "no_all_squash")
|
||||
}
|
||||
if adv.Secure {
|
||||
parts = append(parts, "secure")
|
||||
} else {
|
||||
parts = append(parts, "insecure")
|
||||
}
|
||||
if adv.WDelay {
|
||||
parts = append(parts, "wdelay")
|
||||
} else {
|
||||
parts = append(parts, "no_wdelay")
|
||||
}
|
||||
if adv.Hide {
|
||||
parts = append(parts, "hide")
|
||||
} else {
|
||||
parts = append(parts, "nohide")
|
||||
}
|
||||
if adv.Crossmnt {
|
||||
parts = append(parts, "crossmnt")
|
||||
}
|
||||
}
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("fsid=%d", e.FSID))
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func clientSpec(clients []string, e db.NFSExport) string {
|
||||
opts := buildFlags(e)
|
||||
if len(clients) == 0 {
|
||||
return fmt.Sprintf("*(%s)", opts)
|
||||
}
|
||||
@@ -125,7 +175,7 @@ func (m *Module) renderConfig(exports []db.NFSExport) ([]byte, error) {
|
||||
for _, export := range exports {
|
||||
data.Exports = append(data.Exports, templateExport{
|
||||
Path: export.Path,
|
||||
ClientSpec: clientSpec(export.Clients, export.Options),
|
||||
ClientSpec: clientSpec(export.Clients, export),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user