// Package shares manages SMB (Samba) and NFS exports: the definitions live in // /etc/nasmon/shares.json, from which nasmon generates /etc/samba/smb.conf and // /etc/exports.d/nasmon.exports and reloads the daemons. package shares import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "sort" "sync" ) type SMBShare struct { Name string `json:"name"` Path string `json:"path"` Comment string `json:"comment"` ReadOnly bool `json:"read_only"` Guest bool `json:"guest"` // anyone on the LAN, no password Users []string `json:"users"` // SMB users allowed when Guest is false Browseable bool `json:"browseable"` HideSysFiles bool `json:"hide_sys_files"` // veto ._*, .DS_Store, Thumbs.db, desktop.ini } type SMBConfig struct { Enabled bool `json:"enabled"` Workgroup string `json:"workgroup"` ServerString string `json:"server_string"` Shares []SMBShare `json:"shares"` } type NFSExport struct { Path string `json:"path"` Clients []string `json:"clients"` // host, IP, CIDR or * ReadOnly bool `json:"read_only"` Squash string `json:"squash"` // root | all | none Async bool `json:"async"` } type NFSConfig struct { Enabled bool `json:"enabled"` Exports []NFSExport `json:"exports"` } type Config struct { SMB SMBConfig `json:"smb"` NFS NFSConfig `json:"nfs"` } func Defaults() Config { return Config{ SMB: SMBConfig{Enabled: true, Workgroup: "WORKGROUP", ServerString: "Home NAS"}, NFS: NFSConfig{Enabled: true}, } } // FindSMB returns the index of the share with that name, or -1. func (c *Config) FindSMB(name string) int { for i, s := range c.SMB.Shares { if s.Name == name { return i } } return -1 } // FindNFS returns the index of the export for that path, or -1. func (c *Config) FindNFS(path string) int { for i, e := range c.NFS.Exports { if e.Path == path { return i } } return -1 } func (c *Config) sortShares() { sort.Slice(c.SMB.Shares, func(i, j int) bool { return c.SMB.Shares[i].Name < c.SMB.Shares[j].Name }) sort.Slice(c.NFS.Exports, func(i, j int) bool { return c.NFS.Exports[i].Path < c.NFS.Exports[j].Path }) } // Validate checks the whole configuration (pure; no filesystem access). func (c *Config) Validate() error { if err := ValidateWorkgroup(c.SMB.Workgroup); err != nil { return err } seen := map[string]bool{} for _, s := range c.SMB.Shares { if err := s.Validate(); err != nil { return err } if seen[s.Name] { return fmt.Errorf("duplicate share name %s", s.Name) } seen[s.Name] = true } paths := map[string]bool{} for _, e := range c.NFS.Exports { if err := e.Validate(); err != nil { return err } if paths[e.Path] { return fmt.Errorf("duplicate NFS export %s", e.Path) } paths[e.Path] = true } return nil } // Store is a thread-safe holder for the share definitions backed by a file. type Store struct { path string mu sync.RWMutex cur Config } func Load(path string) (*Store, error) { st := &Store{path: path, cur: Defaults()} b, err := os.ReadFile(path) switch { case errors.Is(err, os.ErrNotExist): return st, nil case err != nil: return nil, err } if err := json.Unmarshal(b, &st.cur); err != nil { return nil, fmt.Errorf("parse %s: %w", path, err) } st.cur.sortShares() return st, nil } func (st *Store) Get() Config { st.mu.RLock() defer st.mu.RUnlock() return st.cur.clone() } func (c Config) clone() Config { out := c out.SMB.Shares = make([]SMBShare, len(c.SMB.Shares)) for i, s := range c.SMB.Shares { s.Users = append([]string(nil), s.Users...) out.SMB.Shares[i] = s } out.NFS.Exports = make([]NFSExport, len(c.NFS.Exports)) for i, e := range c.NFS.Exports { e.Clients = append([]string(nil), e.Clients...) out.NFS.Exports[i] = e } return out } func (st *Store) Save(c Config) error { if err := c.Validate(); err != nil { return err } c.sortShares() b, err := json.MarshalIndent(c, "", " ") if err != nil { return err } st.mu.Lock() defer st.mu.Unlock() if err := os.MkdirAll(filepath.Dir(st.path), 0o755); err != nil { return err } tmp := st.path + ".tmp" if err := os.WriteFile(tmp, append(b, '\n'), 0o644); err != nil { return err } if err := os.Rename(tmp, st.path); err != nil { return err } st.cur = c.clone() return nil }