package main import ( "log" "path/filepath" "strings" "time" "github.com/fsnotify/fsnotify" ) type Watcher struct { w *fsnotify.Watcher g *Generator done chan struct{} } func NewWatcher(dir string, g *Generator) (*Watcher, error) { w, err := fsnotify.NewWatcher() if err != nil { return nil, err } if err := w.Add(dir); err != nil { w.Close() return nil, err } return &Watcher{w: w, g: g, done: make(chan struct{})}, nil } // Watch blocks, watching for changes and triggering rebuilds. // Rapid events (e.g. scp writing an image then its sidecar) are debounced // with a 500 ms window so only one rebuild fires per logical upload. func (w *Watcher) Watch() { var timer *time.Timer rebuild := func() { if err := w.g.Build(); err != nil { log.Printf("rebuild: %v", err) } } for { select { case event, ok := <-w.w.Events: if !ok { return } if !isRelevant(event.Name) { continue } if event.Has(fsnotify.Create) || event.Has(fsnotify.Write) || event.Has(fsnotify.Remove) { if timer != nil { timer.Stop() } timer = time.AfterFunc(500*time.Millisecond, rebuild) } case err, ok := <-w.w.Errors: if !ok { return } log.Printf("watcher: %v", err) case <-w.done: return } } } func (w *Watcher) Close() { close(w.done) w.w.Close() } func isRelevant(name string) bool { ext := strings.ToLower(filepath.Ext(name)) return ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".toml" }