all repos — homestead @ 4034ac2a849b499364d82b902896ca899d946c3a

Code for my website

internal/events/file.go (view raw)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
package events

import (
	"io/fs"
	"maps"
	"os"
	"path"
	"path/filepath"
	"slices"
	"time"

	"go.alanpearce.eu/x/log"

	"github.com/fsnotify/fsnotify"
	"gitlab.com/tozd/go/errors"
)

var (
	ignores = []string{
		"*.templ",
		"*.go",
		"*-journal",
	}
	checkSettleInterval = 200 * time.Millisecond
)

type FileWatcher struct {
	log *log.Logger
	*fsnotify.Watcher
}

func NewFileWatcher(logger *log.Logger, dirs ...string) (*FileWatcher, errors.E) {
	fsn, err := fsnotify.NewWatcher()
	if err != nil {
		return nil, errors.WithMessage(err, "could not create file watcher")
	}

	fw := &FileWatcher{
		Watcher: fsn,
		log:     logger,
	}

	for _, dir := range dirs {
		err = fw.AddRecursive(dir)
		if err != nil {
			return nil, errors.WithMessagef(err, "could not add directory %s to file watcher", dir)
		}
	}

	return fw, nil
}

func (fw *FileWatcher) matches(name string) func(string) bool {
	return func(pattern string) bool {
		matched, err := path.Match(pattern, name)
		if err != nil {
			fw.log.Warn("error checking watcher ignores", "error", err)
		}

		return matched
	}
}

func (fw *FileWatcher) ignored(pathname string) bool {
	return slices.ContainsFunc(ignores, fw.matches(path.Base(pathname)))
}

func (fw *FileWatcher) AddRecursive(from string) errors.E {
	fw.log.Debug("walking directory tree", "root", from)
	err := filepath.WalkDir(from, func(path string, entry fs.DirEntry, err error) error {
		if err != nil {
			return errors.WithMessagef(err, "could not walk directory %s", path)
		}
		if entry.IsDir() {
			if entry.Name() == ".git" {
				fw.log.Debug("skipping directory", "entry", entry.Name())

				return fs.SkipDir
			}
			fw.log.Debug("adding directory to watcher", "path", path)
			if err = fw.Add(path); err != nil {
				return errors.WithMessagef(err, "could not add directory %s to watcher", path)
			}
		}

		return nil
	})

	return errors.WithMessage(err, "error walking directory tree")
}

func (fw *FileWatcher) GetLatestRunID() (uint64, errors.E) {
	return 0, nil
}

func (fw *FileWatcher) Subscribe() (<-chan Event, errors.E) {
	var timer *time.Timer
	events := make(chan Event, 1)

	go func() {
		fileEvents := make(map[string]fsnotify.Op)
		for {
			select {
			case baseEvent := <-fw.Watcher.Events:
				if !fw.ignored(baseEvent.Name) {
					if baseEvent.Has(fsnotify.Create) || baseEvent.Has(fsnotify.Rename) {
						f, err := os.Stat(baseEvent.Name)
						if err != nil {
							fw.log.Warn(
								"error handling event",
								"op",
								baseEvent.Op,
								"name",
								baseEvent.Name,
							)
						} else if f.IsDir() {
							err = fw.Add(baseEvent.Name)
							if err != nil {
								fw.log.Warn("error adding new folder to watcher", "error", err)
							}
						}
					}
					if baseEvent.Has(fsnotify.Rename) || baseEvent.Has(fsnotify.Write) ||
						baseEvent.Has(fsnotify.Create) || baseEvent.Has(fsnotify.Chmod) {
						fileEvents[baseEvent.Name] |= baseEvent.Op
						if timer == nil {
							timer = time.AfterFunc(checkSettleInterval, func() {
								fw.log.Debug(
									"file update event",
									"changed",
									slices.Collect(maps.Keys(fileEvents)),
								)
								events <- Event{
									FSEvent: FSEvent{
										Events: fileEvents,
									},
								}
								clear(fileEvents)
							})
						}
						timer.Reset(checkSettleInterval)
					}
				}
			case err := <-fw.Watcher.Errors:
				fw.log.Warn("error in watcher", "error", err)
			}
		}
	}()

	return events, nil
}