Commit 512b3ef922

512b3ef922b80f777ac7458b41bf2df670f0103a

parent: 538e7e75c5

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-25 23:54 UTC

Rescan on filesystem changes

Layout: unified · split

go.mod +8 −2
@@ -2,6 +2,12 @@ module gitbay.org/cmc/gallery
22
33go 1.27.1
44
5require github.com/niklasfasching/go-org v1.9.1
5require (
6 github.com/fsnotify/fsnotify v1.10.1
7 github.com/niklasfasching/go-org v1.9.1
8)
69
7require golang.org/x/net v0.38.0 // indirect
10require (
11 golang.org/x/net v0.38.0 // indirect
12 golang.org/x/sys v0.31.0 // indirect
13)
go.sum +4
@@ -1,6 +1,10 @@
1github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
2github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
13github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0=
24github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
35github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
46github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
57golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
68golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
9golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
10golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
internal/library/watch.go added +73
@@ -0,0 +1,73 @@
1package library
2
3import (
4 "context"
5 "log"
6 "os"
7 "path/filepath"
8 "strings"
9 "time"
10
11 "github.com/fsnotify/fsnotify"
12)
13
14// Watch calls onChange after filesystem activity under root has been quiet for debounce.
15func Watch(ctx context.Context, root string, debounce time.Duration, onChange func()) error {
16 w, err := fsnotify.NewWatcher()
17 if err != nil {
18 return err
19 }
20 if err := addTree(w, root); err != nil {
21 w.Close()
22 return err
23 }
24 go func() {
25 defer w.Close()
26 timer := time.NewTimer(debounce)
27 timer.Stop()
28 for {
29 select {
30 case <-ctx.Done():
31 return
32 case ev, ok := <-w.Events:
33 if !ok {
34 return
35 }
36 if ev.Has(fsnotify.Create) {
37 if fi, err := os.Stat(ev.Name); err == nil && fi.IsDir() {
38 if err := w.Add(ev.Name); err != nil {
39 log.Printf("watch %s: %v", ev.Name, err)
40 }
41 }
42 }
43 timer.Reset(debounce)
44 case err, ok := <-w.Errors:
45 if !ok {
46 return
47 }
48 log.Printf("watch: %v", err)
49 case <-timer.C:
50 onChange()
51 }
52 }
53 }()
54 return nil
55}
56
57func addTree(w *fsnotify.Watcher, root string) error {
58 if err := w.Add(root); err != nil {
59 return err
60 }
61 entries, err := os.ReadDir(root)
62 if err != nil {
63 return err
64 }
65 for _, e := range entries {
66 if e.IsDir() && !strings.HasPrefix(e.Name(), ".") {
67 if err := w.Add(filepath.Join(root, e.Name())); err != nil {
68 return err
69 }
70 }
71 }
72 return nil
73}
internal/library/watch_test.go added +45
@@ -0,0 +1,45 @@
1package library
2
3import (
4 "context"
5 "os"
6 "path/filepath"
7 "testing"
8 "time"
9)
10
11func expectCall(t *testing.T, ch <-chan struct{}, what string) {
12 t.Helper()
13 select {
14 case <-ch:
15 case <-time.After(3 * time.Second):
16 t.Fatalf("no onChange after %s", what)
17 }
18}
19
20func TestWatchDebouncesAndFollowsNewDirs(t *testing.T) {
21 root := writeTree(t, map[string]string{"tokyo/a.jpg": "x"})
22 ctx, cancel := context.WithCancel(context.Background())
23 defer cancel()
24
25 calls := make(chan struct{}, 10)
26 if err := Watch(ctx, root, 100*time.Millisecond, func() { calls <- struct{}{} }); err != nil {
27 t.Fatal(err)
28 }
29
30 for _, n := range []string{"b.jpg", "c.jpg", "d.jpg"} {
31 os.WriteFile(filepath.Join(root, "tokyo", n), []byte("x"), 0o644)
32 }
33 expectCall(t, calls, "writes in existing album")
34 select {
35 case <-calls:
36 t.Fatal("burst of writes produced more than one call")
37 case <-time.After(300 * time.Millisecond):
38 }
39
40 os.Mkdir(filepath.Join(root, "iceland"), 0o755)
41 expectCall(t, calls, "new album dir")
42 time.Sleep(150 * time.Millisecond) // let the watcher add the new dir
43 os.WriteFile(filepath.Join(root, "iceland", "a.jpg"), []byte("x"), 0o644)
44 expectCall(t, calls, "write in new album")
45}