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.
206 lines
4.3 KiB
Go
206 lines
4.3 KiB
Go
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,
|
|
}
|
|
}
|