github.com/golazy/golazy@v0.0.7-0.20221012133820-968fe65a0b65/lazydev/filewatcher/filewatcher.go (about) 1 // Package filewatcher notifies when the filesystem has change. 2 // It goes up to the top directory that holds a go.mod file 3 package filewatcher 4 5 import ( 6 "errors" 7 "fmt" 8 "log" 9 "os" 10 "path" 11 "path/filepath" 12 "time" 13 14 "github.com/dietsche/rfsnotify" 15 "gopkg.in/fsnotify.v1" 16 ) 17 18 // Op holds the operation name 19 type Op fsnotify.Op 20 21 // String return the operation name Create , Write , Remove , Rename or Chmod 22 func (o Op) String() string { 23 return fsnotify.Op(o).String() 24 } 25 26 // Change represent a change in the filesystem 27 type Change struct { 28 Path string // Path is the path that changed 29 Op Op // Op is the change operation 30 } 31 32 // ChangeSet is a collection of changes 33 type ChangeSet []Change 34 35 // FileWatcher looks for changes in the top most directory that have a go.mod 36 type FileWatcher struct { 37 topDir string 38 c chan (ChangeSet) 39 w *rfsnotify.RWatcher 40 } 41 42 // New initializes a FileWatcher in the given directory 43 // It will go up to the top most directory that holds a go.mod 44 // If dir is an empty string it will use the current directory 45 func New(dir string) (fw *FileWatcher, err error) { 46 if dir == "" { 47 dir, err = os.Getwd() 48 if err != nil { 49 return nil, err 50 } 51 } 52 if !filepath.IsAbs(dir) { 53 return nil, fmt.Errorf("filepath is not absolute") 54 } 55 topDir, err := findRootDirectory(dir) 56 if err != nil { 57 return nil, err 58 } 59 fw = &FileWatcher{ 60 topDir: topDir, 61 } 62 63 return fw, nil 64 } 65 66 // Close stop listening for changes in the file system 67 // Once close, the channel will be closed 68 func (fw *FileWatcher) Close() error { 69 return fw.w.Close() 70 } 71 72 // Watch start watching for recursively in the project 73 func (fw *FileWatcher) Watch() (<-chan (ChangeSet), error) { 74 if fw.c != nil { 75 return nil, fmt.Errorf("Watch was called more than once") 76 } 77 78 fw.c = make(chan (ChangeSet), 100) 79 80 watcher, err := rfsnotify.NewWatcher() 81 if err != nil { 82 return nil, err 83 } 84 fw.w = watcher 85 fw.w.AddRecursive(fw.topDir) 86 87 go fw.wait() 88 89 return fw.c, nil 90 } 91 92 // IgnoredFiles is a list of files that should not trigger a change 93 var IgnoredFiles = []string{} 94 95 // IgnoredDirs is a list of directories that should not tirgger a change 96 var IgnoredDirs = []string{".git", "log"} 97 98 func (fw *FileWatcher) shouldIgnore(e fsnotify.Event) bool { 99 changedPath := e.Name 100 for _, file := range IgnoredFiles { 101 if path.Base(changedPath) == file { 102 return true 103 } 104 } 105 106 dir := changedPath 107 108 // Be nice and not hit the disk constantly 109 // Side effect: A change in a file that is called the same as an ignoredDir is ignored 110 // 111 //fileInfo, err := os.Stat(dir) 112 //if err != nil || !fileInfo.IsDir() { 113 // dir = path.Dir(dir) 114 //} 115 116 for _, ignoredDir := range IgnoredDirs { 117 for ; len(dir) > len(fw.topDir); dir = path.Dir(dir) { 118 if path.Base(dir) == ignoredDir { 119 return true 120 } 121 } 122 123 } 124 125 return false 126 } 127 128 var delay time.Duration = 100 129 130 func (fw *FileWatcher) wait() { 131 wEvents := fw.w.Events 132 wErrors := fw.w.Errors 133 eventBuffer := make(ChangeSet, 0) 134 var timeOut <-chan (time.Time) 135 136 for wEvents != nil || wErrors != nil { 137 select { 138 case event, ok := <-wEvents: 139 if !ok { 140 wEvents = nil 141 continue 142 } 143 if fw.shouldIgnore(event) { 144 continue 145 } 146 eventBuffer = append(eventBuffer, Change{Path: event.Name, Op: Op(event.Op)}) 147 timeOut = time.After(time.Millisecond * delay) 148 case error, ok := <-wErrors: 149 if !ok { 150 wErrors = nil 151 continue 152 } 153 log.Println(error) 154 case _, ok := <-timeOut: 155 fw.c <- eventBuffer 156 eventBuffer = make(ChangeSet, 0) 157 if !ok { 158 timeOut = nil 159 } 160 } 161 } 162 if len(eventBuffer) > 0 { 163 fw.c <- eventBuffer 164 } 165 close(fw.c) 166 } 167 168 func findRootDirectory(start string) (string, error) { 169 topDir := "" 170 171 for dir := start; path.Dir(dir) != dir; dir = path.Dir(dir) { 172 gomod := path.Join(dir, "go.mod") 173 _, err := os.Stat(gomod) 174 if err == nil { 175 topDir = dir 176 continue 177 } 178 if errors.Is(err, os.ErrNotExist) { 179 continue 180 } 181 return "", err 182 } 183 184 return topDir, nil 185 }