Last active
January 27, 2022 11:16
-
-
Save js2854/005afa32aa162308e2081cad70fc8c1c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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