65cd753818
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
217 lines
4.1 KiB
Go
217 lines
4.1 KiB
Go
package importer
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/darroyo/nasctl/internal/db"
|
|
"github.com/darroyo/nasctl/internal/system"
|
|
)
|
|
|
|
var nfsClientPattern = regexp.MustCompile(`^[a-zA-Z0-9_.:\-/\*@]+$`)
|
|
|
|
func ImportNFSExports(path string) ([]db.NFSExport, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("read exports: %w", err)
|
|
}
|
|
|
|
return parseExports(data)
|
|
}
|
|
|
|
func parseExports(data []byte) ([]db.NFSExport, error) {
|
|
var exports []db.NFSExport
|
|
scanner := bufio.NewScanner(bytes.NewReader(data))
|
|
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
|
|
export, ok := parseExportLine(line)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if err := system.ValidatePath(export.Path); err != nil {
|
|
continue
|
|
}
|
|
|
|
validClients := make([]string, 0, len(export.Clients))
|
|
for _, c := range export.Clients {
|
|
if err := system.ValidateNFSClient(c); err != nil {
|
|
continue
|
|
}
|
|
validClients = append(validClients, c)
|
|
}
|
|
if len(validClients) == 0 && len(export.Clients) > 0 {
|
|
continue
|
|
}
|
|
export.Clients = validClients
|
|
|
|
fsid, err := generateImportFSID()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
export.FSID = fsid
|
|
|
|
exports = append(exports, export)
|
|
}
|
|
|
|
return exports, nil
|
|
}
|
|
|
|
func parseExportLine(line string) (db.NFSExport, bool) {
|
|
parenDepth := 0
|
|
spaceIdx := -1
|
|
|
|
for i, ch := range line {
|
|
switch ch {
|
|
case '(':
|
|
parenDepth++
|
|
case ')':
|
|
parenDepth--
|
|
case ' ':
|
|
if parenDepth == 0 && spaceIdx < 0 {
|
|
spaceIdx = i
|
|
}
|
|
}
|
|
}
|
|
|
|
if spaceIdx < 0 {
|
|
return db.NFSExport{}, false
|
|
}
|
|
|
|
path := strings.TrimSpace(line[:spaceIdx])
|
|
if path == "" {
|
|
return db.NFSExport{}, false
|
|
}
|
|
|
|
rest := strings.TrimSpace(line[spaceIdx:])
|
|
if rest == "" {
|
|
return db.NFSExport{}, false
|
|
}
|
|
|
|
var clients []string
|
|
var options string
|
|
|
|
parts := splitExportsClients(rest)
|
|
for _, part := range parts {
|
|
part = strings.TrimSpace(part)
|
|
if part == "" {
|
|
continue
|
|
}
|
|
|
|
open := strings.IndexByte(part, '(')
|
|
close := strings.LastIndexByte(part, ')')
|
|
|
|
var client, opts string
|
|
if open >= 0 && close > open {
|
|
client = strings.TrimSpace(part[:open])
|
|
opts = strings.TrimSpace(part[open+1 : close])
|
|
} else {
|
|
client = part
|
|
opts = ""
|
|
}
|
|
|
|
if !nfsClientPattern.MatchString(client) {
|
|
continue
|
|
}
|
|
|
|
clients = append(clients, client)
|
|
if opts != "" && options == "" {
|
|
options = opts
|
|
}
|
|
}
|
|
|
|
if len(clients) == 0 {
|
|
return db.NFSExport{}, false
|
|
}
|
|
|
|
if options == "" {
|
|
options = "rw,sync,no_root_squash"
|
|
}
|
|
|
|
readOnly := strings.Contains(options, "ro")
|
|
async := strings.Contains(options, "async")
|
|
rootSquash := !strings.Contains(options, "no_root_squash")
|
|
subtreeCheck := strings.Contains(options, "subtree_check")
|
|
|
|
adv := db.NFSAdvanced{
|
|
AllSquash: strings.Contains(options, "all_squash"),
|
|
Secure: strings.Contains(options, "secure"),
|
|
WDelay: strings.Contains(options, "wdelay"),
|
|
Hide: strings.Contains(options, "hide"),
|
|
Crossmnt: strings.Contains(options, "crossmnt"),
|
|
}
|
|
advJSON, _ := json.Marshal(adv)
|
|
|
|
return db.NFSExport{
|
|
Path: path,
|
|
Clients: clients,
|
|
ReadOnly: readOnly,
|
|
Async: async,
|
|
RootSquash: rootSquash,
|
|
SubtreeCheck: subtreeCheck,
|
|
Advanced: string(advJSON),
|
|
}, true
|
|
}
|
|
|
|
func generateImportFSID() (int64, error) {
|
|
var b [4]byte
|
|
if _, err := rand.Read(b[:]); err != nil {
|
|
return 0, err
|
|
}
|
|
n := binary.BigEndian.Uint32(b[:])
|
|
if n == 0 {
|
|
n = 1
|
|
}
|
|
return int64(n), nil
|
|
}
|
|
|
|
func splitExportsClients(s string) []string {
|
|
var result []string
|
|
var current []byte
|
|
parenDepth := 0
|
|
|
|
for _, ch := range []byte(s) {
|
|
switch ch {
|
|
case '(':
|
|
parenDepth++
|
|
current = append(current, ch)
|
|
case ')':
|
|
parenDepth--
|
|
current = append(current, ch)
|
|
case ' ':
|
|
if parenDepth == 0 {
|
|
if len(current) > 0 {
|
|
result = append(result, string(current))
|
|
current = nil
|
|
}
|
|
continue
|
|
}
|
|
current = append(current, ch)
|
|
default:
|
|
current = append(current, ch)
|
|
}
|
|
}
|
|
|
|
if len(current) > 0 {
|
|
result = append(result, string(current))
|
|
}
|
|
|
|
return result
|
|
}
|