all repos — homestead @ e4bdf0dd0740c21e89dd42413b7bc76da381fd37

Code for my website

internal/update/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
package update

import (
	"io/fs"
	"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",
	}
	checkSettleInterval = 200 * time.Millisecond
)

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

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

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

	return &FileWatcher{
		Watcher: fsn,
		log:     logger,
	}, 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) error {
	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) Wait(events chan<- Event, errs chan<- error) error {
	var timer *time.Timer

	go func() {
		var fileEvents []fsnotify.Event
		for {
			select {
			case baseEvent := <-fw.Watcher.Events:
				if !fw.ignored(baseEvent.Name) {
					fw.log.Debug(
						"watcher event",
						"name",
						baseEvent.Name,
						"op",
						baseEvent.Op.String(),
					)
					if baseEvent.Has(fsnotify.Create) || baseEvent.Has(fsnotify.Rename) {
						f, err := os.Stat(baseEvent.Name)
						if err != nil {
							errs <- errors.WithMessagef(err, "error handling event %s", baseEvent.Op.String())
						} else if f.IsDir() {
							err = fw.Add(baseEvent.Name)
							if err != nil {
								errs <- errors.WithMessage(err, "error adding new folder to watcher")
							}
						}
					}
					if baseEvent.Has(fsnotify.Rename) || baseEvent.Has(fsnotify.Write) ||
						baseEvent.Has(fsnotify.Create) || baseEvent.Has(fsnotify.Chmod) {
						fileEvents = append(fileEvents, baseEvent)
						if timer == nil {
							timer = time.AfterFunc(checkSettleInterval, func() {
								events <- Event{
									FileEvents: fileEvents,
									Revision:   "",
								}
								fileEvents = []fsnotify.Event{}
							})
						}
						timer.Reset(checkSettleInterval)
					}
				}
			case err := <-fw.Watcher.Errors:
				errs <- errors.WithMessage(err, "error in watcher")
			}
		}
	}()

	return nil
}