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  }