// watcher.go — permanent whitelist file watcher // // The watcher polls the permanent whitelist file every cfg.WatchInterval // seconds and reloads the in-memory store if the file has changed. // // Why poll instead of inotify? // // - Polling is simpler and works inside Docker containers where inotify // may not be available (no host filesystem access). // - The polling interval (30s default) is fast enough for admin // whitelisting operations. // - If you want inotify, add it later. // // We only reload when the file's mtime changes, so frequent touch- // operations don't cause unnecessary reloads. // // The watcher uses a mutex to protect the in-memory store. This is // necessary because the watcher runs in a separate goroutine and the // auth handler reads from the same store. package main import ( "bufio" "log" "os" "time" ) // newPermanentWhitelistWatcher creates a watcher that reloads the // permanent whitelist file from disk. // // The watcher loads the file once immediately on creation, then polls // every interval. func newPermanentWhitelistWatcher(path string, interval time.Duration, store *PermanentWhitelist) *watcher { w := &watcher{ path: path, interval: interval, store: store, } w.reload() return w } // watcher polls a file and reloads the whitelist when it changes. type watcher struct { path string interval time.Duration store *PermanentWhitelist lastMod time.Time } // start begins the polling loop. func (w *watcher) start() { go func() { ticker := time.NewTicker(w.interval) defer ticker.Stop() for { select { case <-ticker.C: w.reload() } } }() } // reload reads the whitelist file and updates the in-memory store. // // We only reload if the file has changed (mtime). This prevents // unnecessary reloads when the file is touched frequently. func (w *watcher) reload() { info, err := os.Stat(w.path) if err != nil { // File doesn't exist yet — skip. return } // Only reload if the file has changed. if info.ModTime().Equal(w.lastMod) { return } w.lastMod = info.ModTime() // Read and parse the file. entries := make(map[string]struct{}) f, err := os.Open(w.path) if err != nil { log.Printf("failed to open whitelist file: %v", err) return } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() if line == "" || line[0] == '#' { continue } entries[line] = struct{}{} } // Update the store. w.store.mu.Lock() w.store.entries = entries w.store.mu.Unlock() log.Printf("loaded %d entries from %s", len(entries), w.path) }