Files
photog/watcher.go
T
puttaalu 25384d3f3b Rebuild on file renames so deletes take effect
The watcher only reacted to Create/Write/Remove. A /manage delete moves the
image and sidecar into content/.trash/, which on Linux arrives as a Rename
(inotify IN_MOVED_FROM), not a Remove — so the watcher never fired and the
delete left the published site unchanged (uploads worked because they Create
and Write). Watch Rename too, which also covers removing a photo with a manual
mv. The delete handler still triggers its own immediate rebuild; this makes the
watcher path agree with it.
2026-10-18 22:19:09 +02:00

83 lines
1.8 KiB
Go

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.
//
// Rename is watched alongside Create/Write/Remove: moving a photo out of the
// content directory — which is exactly what a /manage delete does when it
// sends files to content/.trash/ — arrives as a Rename (inotify IN_MOVED_FROM)
// on Linux, not a Remove, so without it a delete (or a manual mv) would leave
// the site unrebuilt.
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) || event.Has(fsnotify.Rename) {
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"
}