all repos — homestead @ d9a119ca0abe7ffedd125c81c78f725aab886d29

Code for my website

shared/events/file.go (view raw)

package events

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

	"alin.ovh/x/log"
	"github.com/Southclaws/fault"
	"github.com/Southclaws/fault/fmsg"
	"github.com/fsnotify/fsnotify"
)

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

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

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

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

	for _, dir := range dirs {
		err = fw.AddRecursive(dir)
		if err != nil {
			return nil, fault.Wrap(
				err,
				fmsg.With(fmt.Sprintf("could not add directory %s to file watcher", dir)),
			)
		}
	}

	return fw, nil
}

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 fault.Wrap(err, fmsg.With(fmt.Sprintf("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 fault.Wrap(
					err,
					fmsg.With(fmt.Sprintf("could not add directory %s to watcher", path)),
				)
			}
		}

		return nil
	})

	return fault.Wrap(err, fmsg.With("error walking directory tree"))
}

func (fw *FileWatcher) GetLatestRunID() (uint64, error) {
	return 0, nil
}

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

	go func() {
		fileEvents := make(map[string]fsnotify.Op)
		for {
			select {
			case baseEvent := <-fw.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.Errors:
				fw.log.Warn("error in watcher", "error", err)
			}
		}
	}()

	return events, 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)))
}