package samba import ( "bytes" "context" "embed" "fmt" "os" "path/filepath" "strings" "text/template" "time" "github.com/darroyo/nasctl/internal/db" "github.com/darroyo/nasctl/internal/system" ) const ModuleName = "samba" //go:embed smb.conf.tmpl var smbConfTemplate embed.FS type Config struct { SMBConfPath string Reload bool } type Module struct { cfg Config } type templateShare struct { Name string Path string Comment string ReadOnly bool GuestOK bool ValidUsers []string ValidGroups []string InvalidUsers []string } type templateData struct { Shares []templateShare } func New(cfg Config) *Module { if cfg.SMBConfPath == "" { cfg.SMBConfPath = "/etc/samba/smb.conf" } return &Module{cfg: cfg} } func (m *Module) Name() string { return ModuleName } func (m *Module) IsDirty(ctx context.Context, database *db.DB) (bool, error) { return database.IsDirty(ModuleName) } func (m *Module) Apply(ctx context.Context, database *db.DB) error { if err := m.maybeBackup(database); err != nil { return err } shares, err := database.ListSambaShares() if err != nil { return err } content, err := m.renderConfig(shares) if err != nil { return err } if err := writeAtomic(m.cfg.SMBConfPath, content); err != nil { return err } if m.cfg.Reload { stdout, stderr, err := system.Run(ctx, "systemctl", "reload", "smbd") if err != nil { return fmt.Errorf("reload smbd: %w (stdout=%q stderr=%q)", err, stdout, stderr) } } message := fmt.Sprintf("applied %d samba share(s) to %s", len(shares), m.cfg.SMBConfPath) if err := database.AppendApplyLog(ModuleName, message, true); err != nil { return err } return database.ClearDirty(ModuleName) } func (m *Module) renderConfig(shares []db.SambaShare) ([]byte, error) { tmplContent, err := smbConfTemplate.ReadFile("smb.conf.tmpl") if err != nil { return nil, fmt.Errorf("read smb.conf template: %w", err) } funcMap := template.FuncMap{ "join": strings.Join, } tmpl, err := template.New("smb.conf").Funcs(funcMap).Parse(string(tmplContent)) if err != nil { return nil, fmt.Errorf("parse smb.conf template: %w", err) } data := templateData{Shares: make([]templateShare, 0, len(shares))} for _, share := range shares { data.Shares = append(data.Shares, templateShare{ Name: share.Name, Path: share.Path, Comment: share.Comment, ReadOnly: share.ReadOnly, GuestOK: share.GuestOK, ValidUsers: share.ValidUsers, ValidGroups: share.ValidGroups, InvalidUsers: share.InvalidUsers, }) } var buf bytes.Buffer if err := tmpl.Execute(&buf, data); err != nil { return nil, fmt.Errorf("execute smb.conf template: %w", err) } return buf.Bytes(), nil } func (m *Module) maybeBackup(database *db.DB) error { done, ok, err := database.GetSetting("backup.samba.done") if err != nil { return err } if ok && done == "true" { return nil } if _, err := os.Stat(m.cfg.SMBConfPath); os.IsNotExist(err) { return nil } else if err != nil { return nil } src, err := os.ReadFile(m.cfg.SMBConfPath) if err != nil { return nil } backupPath := m.cfg.SMBConfPath + ".nasctl.bak." + time.Now().Format("20060102T150405") if err := os.WriteFile(backupPath, src, 0o644); err != nil { return fmt.Errorf("backup smb.conf: %w", err) } _ = database.SetSetting("backup.samba.done", "true") return nil } func writeAtomic(path string, content []byte) error { dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0o755); err != nil { return fmt.Errorf("create config dir: %w", err) } tmp, err := os.CreateTemp(dir, ".smb.conf.*") if err != nil { return fmt.Errorf("create temp file: %w", err) } tmpPath := tmp.Name() cleanup := func() { _ = tmp.Close() _ = os.Remove(tmpPath) } if _, err := tmp.Write(content); err != nil { cleanup() return fmt.Errorf("write temp config: %w", err) } if err := tmp.Chmod(0o644); err != nil { cleanup() return fmt.Errorf("chmod temp config: %w", err) } if err := tmp.Close(); err != nil { cleanup() return fmt.Errorf("close temp config: %w", err) } if err := os.Rename(tmpPath, path); err != nil { cleanup() return fmt.Errorf("rename temp config: %w", err) } return nil }