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,205 @@
|
||||
package importer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/darroyo/nasctl/internal/db"
|
||||
"github.com/darroyo/nasctl/internal/system"
|
||||
)
|
||||
|
||||
var (
|
||||
sambaIgnoredSections = map[string]bool{
|
||||
"global": true,
|
||||
"printers": true,
|
||||
"homes": true,
|
||||
}
|
||||
)
|
||||
|
||||
type shareBuilder struct {
|
||||
Name string
|
||||
Path string
|
||||
Comment string
|
||||
ReadOnly bool
|
||||
GuestOK bool
|
||||
ValidUsers []string
|
||||
ValidGroups []string
|
||||
}
|
||||
|
||||
func ImportSambaShares(path string) ([]db.SambaShare, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("read smb.conf: %w", err)
|
||||
}
|
||||
|
||||
return parseSambaConf(data)
|
||||
}
|
||||
|
||||
func parseSambaConf(data []byte) ([]db.SambaShare, error) {
|
||||
var shares []db.SambaShare
|
||||
var current *shareBuilder
|
||||
scanner := bufio.NewScanner(bytes.NewReader(data))
|
||||
var lineNum int
|
||||
var continuation string
|
||||
|
||||
for scanner.Scan() {
|
||||
lineNum++
|
||||
raw := scanner.Text()
|
||||
|
||||
if continuation != "" {
|
||||
continuation += "\n" + raw
|
||||
if strings.HasSuffix(raw, "\\") {
|
||||
continuation = strings.TrimSuffix(continuation, "\\")
|
||||
continue
|
||||
}
|
||||
raw = continuation
|
||||
continuation = ""
|
||||
} else if strings.HasSuffix(strings.TrimSpace(raw), "\\") {
|
||||
continuation = strings.TrimSuffix(strings.TrimSpace(raw), "\\")
|
||||
continue
|
||||
}
|
||||
|
||||
raw = strings.TrimSpace(raw)
|
||||
|
||||
if raw == "" || startsWith(raw, '#', ';') {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(raw, "[") {
|
||||
if current != nil && current.Name != "" && current.Path != "" {
|
||||
if share := currentToShare(current); share.Name != "" {
|
||||
shares = append(shares, share)
|
||||
}
|
||||
}
|
||||
sectionName := parseSectionName(raw)
|
||||
if sectionName == "" || sambaIgnoredSections[strings.ToLower(sectionName)] {
|
||||
current = nil
|
||||
continue
|
||||
}
|
||||
current = &shareBuilder{Name: sectionName}
|
||||
continue
|
||||
}
|
||||
|
||||
if current == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
key, value := parseKeyValue(raw)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch strings.ToLower(key) {
|
||||
case "path":
|
||||
current.Path = value
|
||||
case "comment":
|
||||
current.Comment = value
|
||||
case "read only":
|
||||
current.ReadOnly = parseBool(value)
|
||||
case "writable":
|
||||
current.ReadOnly = !parseBool(value)
|
||||
case "guest ok":
|
||||
current.GuestOK = parseBool(value)
|
||||
case "valid users":
|
||||
current.ValidUsers = parseCommaList(value)
|
||||
case "valid groups":
|
||||
current.ValidGroups = parseCommaList(value)
|
||||
case "include", "copy", "inherit owner", "inherit permissions", "nt acl support",
|
||||
"printable", "print mode", "lppause command", "lpresume command",
|
||||
"queuepause command", "queueresume command":
|
||||
}
|
||||
}
|
||||
|
||||
if current != nil && current.Name != "" && current.Path != "" {
|
||||
if share := currentToShare(current); share.Name != "" {
|
||||
shares = append(shares, share)
|
||||
}
|
||||
}
|
||||
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
func startsWith(s string, chars ...byte) bool {
|
||||
for _, c := range chars {
|
||||
if len(s) > 0 && s[0] == c {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseSectionName(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if !strings.HasPrefix(raw, "[") || !strings.HasSuffix(raw, "]") {
|
||||
return ""
|
||||
}
|
||||
return strings.Trim(raw, "[]")
|
||||
}
|
||||
|
||||
func parseKeyValue(raw string) (key, value string) {
|
||||
eq := strings.IndexByte(raw, '=')
|
||||
if eq < 0 {
|
||||
return "", ""
|
||||
}
|
||||
key = strings.TrimSpace(raw[:eq])
|
||||
value = strings.TrimSpace(raw[eq+1:])
|
||||
value = strings.Trim(value, "\"")
|
||||
|
||||
value = strings.TrimPrefix(value, "\"")
|
||||
value = strings.TrimSuffix(value, "\"")
|
||||
|
||||
return key, value
|
||||
}
|
||||
|
||||
func parseBool(s string) bool {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
return s == "yes" || s == "true" || s == "1" || s == "on"
|
||||
}
|
||||
|
||||
func parseCommaList(s string) []string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(s, ",")
|
||||
var out []string
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func currentToShare(b *shareBuilder) db.SambaShare {
|
||||
if b.Name == "" || b.Path == "" {
|
||||
return db.SambaShare{}
|
||||
}
|
||||
if err := system.ValidateShareName(b.Name); err != nil {
|
||||
return db.SambaShare{}
|
||||
}
|
||||
if err := system.ValidatePath(b.Path); err != nil {
|
||||
return db.SambaShare{}
|
||||
}
|
||||
for _, u := range b.ValidUsers {
|
||||
if err := system.ValidateUsername(u); err != nil {
|
||||
return db.SambaShare{}
|
||||
}
|
||||
}
|
||||
return db.SambaShare{
|
||||
Name: b.Name,
|
||||
Path: b.Path,
|
||||
Comment: b.Comment,
|
||||
ReadOnly: b.ReadOnly,
|
||||
GuestOK: b.GuestOK,
|
||||
ValidUsers: b.ValidUsers,
|
||||
ValidGroups: b.ValidGroups,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user