github.com/blend/go-sdk@v1.20220411.3/fileutil/watcher.go (about) 1 /* 2 3 Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file. 5 6 */ 7 8 package fileutil 9 10 import ( 11 "context" 12 "os" 13 "time" 14 15 "github.com/blend/go-sdk/async" 16 "github.com/blend/go-sdk/ex" 17 ) 18 19 // Watch constants 20 const ( 21 ErrWatchStopped ex.Class = "watch file should stop" 22 DefaultWatchPollInterval = 500 * time.Millisecond 23 ) 24 25 // Watch watches a file for changes and calls the action if there are changes. 26 // It does this by polling the file for ModTime changes every 500ms. 27 // It is not designed for watching a large number of files. 28 // This function blocks, and you should probably call this with its own goroutine. 29 // The action takes a direct file handle, and is _NOT_ responsible for closing 30 // the file; the watcher will do that when the action has completed. 31 func Watch(ctx context.Context, path string, action WatchAction) error { 32 errors := make(chan error, 1) 33 w := NewWatcher(path, action) 34 w.Errors = errors 35 w.Starting() 36 w.Watch(ctx) 37 if len(errors) > 0 { 38 return <-errors 39 } 40 return nil 41 } 42 43 // NewWatcher returns a new watcher. 44 func NewWatcher(path string, action WatchAction, opts ...WatcherOption) *Watcher { 45 watch := Watcher{ 46 Latch: async.NewLatch(), 47 Path: path, 48 Action: action, 49 } 50 for _, opt := range opts { 51 opt(&watch) 52 } 53 return &watch 54 } 55 56 // WatchAction is an action for the file watcher. 57 type WatchAction func(*os.File) error 58 59 // WatcherOption is an option for a watcher. 60 type WatcherOption func(*Watcher) 61 62 // Watcher watches a file for changes and calls the action. 63 type Watcher struct { 64 *async.Latch 65 66 Path string 67 PollInterval time.Duration 68 Action func(*os.File) error 69 Errors chan error 70 } 71 72 // PollIntervalOrDefault returns the polling interval or a default. 73 func (w Watcher) PollIntervalOrDefault() time.Duration { 74 if w.PollInterval > 0 { 75 return w.PollInterval 76 } 77 return DefaultWatchPollInterval 78 } 79 80 // Watch watches a given file. 81 func (w Watcher) Watch(ctx context.Context) { 82 stat, err := os.Stat(w.Path) 83 if err != nil { 84 w.handleError(ex.New(err)) 85 return 86 } 87 88 w.Started() 89 lastMod := stat.ModTime() 90 ticker := time.NewTicker(w.PollIntervalOrDefault()) 91 defer ticker.Stop() 92 93 for { 94 select { 95 case <-w.NotifyStopping(): 96 w.Stopped() 97 return 98 case <-ctx.Done(): 99 w.Stopped() 100 return 101 default: 102 } 103 select { 104 case <-ticker.C: 105 stat, err = os.Stat(w.Path) 106 if err != nil { 107 w.handleError(ex.New(err)) 108 return 109 } 110 if stat.ModTime().After(lastMod) { 111 file, err := os.Open(w.Path) 112 if err != nil { 113 w.handleError(ex.New(err)) 114 return 115 } 116 117 // call the action 118 // and no matter what, close the file. 119 func() { 120 defer file.Close() 121 err = w.Action(file) 122 }() 123 124 if err != nil { 125 if ex.Is(err, ErrWatchStopped) { 126 return 127 } 128 w.handleError(ex.New(err)) 129 return 130 } 131 lastMod = stat.ModTime() 132 } 133 case <-w.NotifyStopping(): 134 w.Stopped() 135 return 136 case <-ctx.Done(): 137 w.Stopped() 138 return 139 } 140 } 141 } 142 143 func (w Watcher) handleError(err error) { 144 if err == nil { 145 return 146 } 147 if w.Errors != nil { 148 w.Errors <- err 149 } 150 }