feat: import existing smb.conf and /etc/exports on first boot
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.
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user