4ae7335b31
Adds auto-detection of pre-existing Samba shares and NFS exports when nasctl is installed on a host that already has these configs. New package internal/importer parses smb.conf (INI-style) and /etc/exports (line-based) and imports them into SQLite. Imported shares/exports are marked dirty so the user must review and apply manually before any file is overwritten. Backup: before the first Apply, each module backs up the original config to <path>.nasctl.bak.<timestamp> (one time only). New CLI flag --import-on-boot / NASCTL_IMPORT_ON_BOOT env var (default false, opt-in). New API endpoints: GET /api/import/status POST /api/import/samba POST /api/import/nfs New DB methods ReplaceSambaShares/ReplaceNFSExports (transactional replace-all), guarded by import.samba.done / import.nfs.done settings flags.
184 lines
3.3 KiB
Go
184 lines
3.3 KiB
Go
package importer
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"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
|
|
|
|
if export.Options != "" {
|
|
if err := system.ValidateNFSOptions(export.Options); err != nil {
|
|
continue
|
|
}
|
|
}
|
|
|
|
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"
|
|
}
|
|
|
|
return db.NFSExport{
|
|
Path: path,
|
|
Clients: clients,
|
|
Options: options,
|
|
}, true
|
|
}
|
|
|
|
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
|
|
}
|