package main import ( "crypto/sha256" "fmt" "io" "log" "maps" "os" "os/exec" "os/signal" "slices" "time" "github.com/fsnotify/fsnotify" ) type Listen struct { FileMap map[string]bool CksumMap map[string][]byte IntervalMap map[string]time.Time Condition string Cksum bool Interval time.Duration Command []string Quiet bool } func cksumCheck(l Listen, k string, t *bool) bool { hasher := sha256.New() f, err := os.Open(k) if err != nil { log.Fatal(err) } defer f.Close() if _, err := io.Copy(hasher, f); err != nil { log.Fatal(err) } if !slices.Equal(l.CksumMap[k], hasher.Sum(nil)) { l.CksumMap[k] = hasher.Sum(nil) if l.Condition == "any" { *t = true return true } l.FileMap[k] = true } return false } func allCheck(l Listen) bool { for value := range maps.Values(l.FileMap) { if !value { return false } } return true } func loopFsnotify(l Listen, quit chan bool) { watcher, err := fsnotify.NewWatcher() if err != nil { log.Fatal(err) } defer watcher.Close() for key := range l.FileMap { watcher.Add(key) } for { var renameAdd []string = []string{} trigger := false select { case event, ok := <-watcher.Events: if !ok { return } for key := range l.FileMap { if event.Name == key { if l.Cksum { // return value indicates a break is needed if cksumCheck(l, key, &trigger) { break } } else { if event.Has(fsnotify.Write) { if l.Condition == "any" { trigger = true break } l.FileMap[key] = true } else if event.Has(fsnotify.Rename) { // we need to rewatch file // sleeping small amount to allow CREATE event to propogate time.Sleep(10 * time.Millisecond) // ... then adding to a list to allow additional time renameAdd = append(renameAdd, key) if l.Condition == "any" { trigger = true break } l.FileMap[key] = true } } } } // end for if l.Condition == "all" { trigger = allCheck(l) } // end case event case _, ok := <-watcher.Errors: if !ok { return } } // end switch // end case errors if trigger { if len(l.Command) >= 1 { cmd := exec.Command(l.Command[0], l.Command[1:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Run() } else { quit <- true } } // end if trigger for _, value := range renameAdd { err := watcher.Add(value) if err != nil { fmt.Println(err) } } } // end for } func loopInterval(l Listen, quit chan bool) { for { time.Sleep(l.Interval) trigger := false for key := range l.FileMap { if l.Cksum { // return value indicates a break is needed if cksumCheck(l, key, &trigger) { break } } else { s, err := os.Stat(key) if err != nil { log.Fatal() } if l.IntervalMap[key] != s.ModTime() { l.IntervalMap[key] = s.ModTime() if l.Condition == "any" { trigger = true break } l.FileMap[key] = true } } } // end for key if l.Condition == "all" { trigger = allCheck(l) } if trigger { if len(l.Command) >= 1 { cmd := exec.Command(l.Command[0], l.Command[1:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Run() } else { quit <- true } } // end if trigger } } func (l Listen) Run() { // catch ^C quit := make(chan bool) c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) // start main loop if l.Interval == time.Duration(0*time.Second) { go loopFsnotify(l, quit) } else { go loopInterval(l, quit) } select { case <-c: case <-quit: } }