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:
2026-07-05 21:45:25 -04:00
parent d6b184d19b
commit 4ae7335b31
14 changed files with 1153 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
package importer
import (
"context"
"fmt"
"log"
"github.com/darroyo/nasctl/internal/db"
"github.com/darroyo/nasctl/internal/modules/nfs"
"github.com/darroyo/nasctl/internal/modules/samba"
)
type ImportResult struct {
SambaImported int
NFSImported int
SambaSkipped bool
NFSSkipped bool
SambaError string
NFSError string
}
type ImporterDB interface {
ListSambaShares() ([]db.SambaShare, error)
ListNFSExports() ([]db.NFSExport, error)
ReplaceSambaShares(shares []db.SambaShare) error
ReplaceNFSExports(exports []db.NFSExport) error
GetSetting(key string) (string, bool, error)
SetSetting(key, value string) error
MarkDirty(module string) error
}
func ImportOnBoot(ctx context.Context, database ImporterDB, smbPath, exportsPath string) ImportResult {
result := ImportResult{}
sambaDone, _, _ := database.GetSetting("import.samba.done")
nfsDone, _, _ := database.GetSetting("import.nfs.done")
if sambaDone == "true" && nfsDone == "true" {
return result
}
if sambaDone != "true" {
sr := importSamba(ctx, database, smbPath)
result.SambaImported = sr.count
result.SambaSkipped = sr.skipped
result.SambaError = sr.err
if sr.err == "" {
_ = database.SetSetting("import.samba.done", "true")
}
}
if nfsDone != "true" {
nr := importNFS(ctx, database, exportsPath)
result.NFSImported = nr.count
result.NFSSkipped = nr.skipped
result.NFSError = nr.err
if nr.err == "" {
_ = database.SetSetting("import.nfs.done", "true")
}
}
if result.SambaImported > 0 || result.NFSImported > 0 {
log.Printf("[importer] imported %d samba shares, %d nfs exports", result.SambaImported, result.NFSImported)
}
return result
}
func ResetImportFlags(ctx context.Context, database ImporterDB) error {
_ = database.SetSetting("import.samba.done", "")
_ = database.SetSetting("import.nfs.done", "")
return nil
}
type importStep struct {
count int
skipped bool
err string
}
func importSamba(ctx context.Context, database ImporterDB, path string) importStep {
shares, err := ImportSambaShares(path)
if err != nil {
return importStep{err: err.Error()}
}
if shares == nil {
return importStep{skipped: true}
}
existing, err := database.ListSambaShares()
if err != nil {
return importStep{err: err.Error()}
}
if len(existing) > 0 {
return importStep{skipped: true}
}
if err := database.ReplaceSambaShares(shares); err != nil {
return importStep{err: err.Error()}
}
if err := database.MarkDirty(samba.ModuleName); err != nil {
return importStep{count: len(shares), err: fmt.Sprintf("imported but could not mark dirty: %v", err)}
}
return importStep{count: len(shares)}
}
func importNFS(ctx context.Context, database ImporterDB, path string) importStep {
exports, err := ImportNFSExports(path)
if err != nil {
return importStep{err: err.Error()}
}
if exports == nil {
return importStep{skipped: true}
}
existing, err := database.ListNFSExports()
if err != nil {
return importStep{err: err.Error()}
}
if len(existing) > 0 {
return importStep{skipped: true}
}
if err := database.ReplaceNFSExports(exports); err != nil {
return importStep{err: err.Error()}
}
if err := database.MarkDirty(nfs.ModuleName); err != nil {
return importStep{count: len(exports), err: fmt.Sprintf("imported but could not mark dirty: %v", err)}
}
return importStep{count: len(exports)}
}