Skip to content

Instantly share code, notes, and snippets.

@js2854
Last active January 27, 2022 11:16
Show Gist options
  • Save js2854/005afa32aa162308e2081cad70fc8c1c to your computer and use it in GitHub Desktop.
Save js2854/005afa32aa162308e2081cad70fc8c1c to your computer and use it in GitHub Desktop.
// watches the source directory for changes and synchronize to destination directory
package main
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/howeyc/fsnotify"
"github.com/otiai10/copy"
)
var (
sync = flag.Bool("sync", false, "synchronize on start up")
depth = flag.Int("depth", 5, "recursion depth")
src = flag.String("src", ".", "directory root to use for watching")
dst = flag.String("dst", "", "directory synchronized to")
wait = flag.Duration("wait", 10*time.Millisecond, "time to wait between change detection")
ignore = flag.String("ignore", "", "path ignore pattern")
)
func usage() {
fmt.Fprintf(os.Stderr, "usage: %s [flags]\n", os.Args[0])
flag.PrintDefaults()
}
func main() {
flag.Usage = usage
flag.Parse()
watcher, err := newWatcher()
if err != nil {
log.Fatal(err)
}
fileEvents := make(chan interface{}, 100)
// pipe all events to fileEvents (for buffering and draining)
go watcher.pipeEvents(fileEvents)
// if we have an ignore pattern, set up predicate and replace fileEvents
if *ignore != "" {
fileEvents = filter(fileEvents, func(e interface{}) bool {
fe := e.(*fsnotify.FileEvent)
ignored, err := filepath.Match(*ignore, filepath.Base(fe.Name))
if err != nil {
fmt.Fprintln(os.Stderr, "error performing match:", err)
}
return !ignored
})
}
srcDir, err := filepath.Abs(*src)
if err != nil {
log.Fatal(err)
}
dstDir, err := filepath.Abs(*dst)
if err != nil {
log.Fatal(err)
}
if *sync {
fmt.Printf("Start sync: %v -> %v\n\n", srcDir, dstDir)
if err := copy.Copy(srcDir, dstDir); err != nil {
fmt.Fprintf(os.Stderr, "#### Copy: %v -> %v failed! err: %v\n", srcDir, dstDir, err)
}
}
go watchAndSyc(fileEvents, srcDir, dstDir)
err = watcher.watchDirAndChildren(srcDir, *depth)
if err != nil {
log.Fatal(err)
}
fmt.Fprintln(os.Stderr, "")
select {}
watcher.Close()
}
type watcher struct {
*fsnotify.Watcher
}
func newWatcher() (watcher, error) {
fsnw, err := fsnotify.NewWatcher()
return watcher{fsnw}, err
}
// Execute cmd with args when a file event occurs
func watchAndSyc(fileEvents chan interface{}, srcDir, dstDir string) {
for {
time.Sleep(*wait)
ev := <-fileEvents
fe := ev.(*fsnotify.FileEvent)
srcFilePath := fe.Name
dstFilePath := strings.Replace(srcFilePath, srcDir, dstDir, -1)
// fmt.Println("File changed:", ev)
if fe.IsCreate() {
if !exists(srcFilePath) {
continue
}
fmt.Printf("Copy: %v -> %v\n", srcFilePath, dstFilePath)
if err := copy.Copy(srcFilePath, dstFilePath); err != nil {
fmt.Fprintf(os.Stderr, "#### Copy: %v -> %v failed! err: %v\n", srcFilePath, dstFilePath, err)
}
}
if fe.IsDelete() {
fmt.Printf("Remove: %v\n", dstFilePath)
if err := os.RemoveAll(dstFilePath); err != nil {
fmt.Fprintf(os.Stderr, "#### Remove: %v failed! err: %v\n", dstFilePath, err)
}
}
if fe.IsModify() {
if !exists(srcFilePath) {
continue
}
fmt.Printf("Modify: %v -> %v\n", srcFilePath, dstFilePath)
if err := copy.Copy(srcFilePath, dstFilePath); err != nil {
fmt.Fprintf(os.Stderr, "#### Copy: %v -> %v failed! err: %v\n", srcFilePath, dstFilePath, err)
}
}
if fe.IsRename() {
if exists(srcFilePath) {
fmt.Printf("Copy: %v -> %v\n", srcFilePath, dstFilePath)
if err := copy.Copy(srcFilePath, dstFilePath); err != nil {
fmt.Fprintf(os.Stderr, "#### Copy: %v -> %v failed! err: %v\n", srcFilePath, dstFilePath, err)
}
} else {
fmt.Printf("Remove: %v\n", dstFilePath)
if err := os.RemoveAll(dstFilePath); err != nil {
fmt.Fprintf(os.Stderr, "#### Remove: %v failed! err: %v\n", dstFilePath, err)
}
}
}
}
}
// Add dir and children (recursively) to watcher
func (w watcher) watchDirAndChildren(path string, depth int) error {
if err := w.Watch(path); err != nil {
return err
}
baseNumSeps := strings.Count(path, string(os.PathSeparator))
return filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
pathDepth := strings.Count(path, string(os.PathSeparator)) - baseNumSeps
if pathDepth > depth {
return filepath.SkipDir
}
fmt.Fprintln(os.Stderr, "Watching", path)
if err := w.Watch(path); err != nil {
return err
}
}
return nil
})
}
// pipeEvents sends valid events to `events` and errors to stderr
func (w watcher) pipeEvents(events chan interface{}) {
for {
select {
case ev := <-w.Event:
events <- ev
// @todo handle created/renamed/deleted dirs
case err := <-w.Error:
log.Println("fsnotify error:", err)
}
}
}
func filter(items chan interface{}, predicate func(interface{}) bool) chan interface{} {
results := make(chan interface{})
go func() {
for {
item := <-items
if predicate(item) {
results <- item
}
}
}()
return results
}
// exists returns whether the given file or directory exists
func exists(path string) bool {
_, err := os.Stat(path)
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
return true
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment