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  }