//go:build !windows package main import ( "bufio" "context" "encoding/json" "flag" "net" "os" "os/signal" "path/filepath" "syscall" "time" ) var ( hostDir = flag.String("host", "/host", "Host source directory (read-only)") dataDir = flag.String("data", "/data", "Data destination directory") verbose = flag.Bool("verbose", false, "Enable verbose logging (shorthand for -log-level debug)") logLevel = flag.String("log-level", "info", "Log level: debug, info, warn, error, none") dryRun = flag.Bool("dry-run", false, "Preview what would be synced without copying") socketPath = flag.String("socket", "", "Unix socket to receive events from (Windows watcher)") ) func run() { flag.Parse() if *verbose { *logLevel = "debug" } level, err := ParseLogLevel(*logLevel) if err != nil { logErrorf("%v", err) os.Exit(1) } SetLogLevel(level) stack := NewGitignoreStack(*hostDir) manifest, err := initialSync(*hostDir, *dataDir, stack, *dryRun) if err != nil { logErrorf("initial sync failed: %v", err) os.Exit(1) } if *dryRun { logInfo("dry-run complete, exiting") return } if *socketPath == "" { logInfo("no -socket specified, running in standalone mode (no file watching)") ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancel() <-ctx.Done() return } logInfo("watcher: connecting to %s ...", *socketPath) ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancel() if err := listenSocket(ctx, *socketPath, manifest, stack); err != nil { logErrorf("socket listener failed: %v", err) os.Exit(1) } } func listenSocket(ctx context.Context, socketPath string, manifest *Manifest, stack *GitignoreStack) error { var conn net.Conn var err error for { if ctx.Err() != nil { return ctx.Err() } conn, err = net.Dial("unix", socketPath) if err == nil { break } logDebug("connect failed: %v, retrying...", err) select { case <-ctx.Done(): return ctx.Err() case <-time.After(1 * time.Second): } } defer conn.Close() logInfo("watcher: connected, monitoring for changes ...") scanner := bufio.NewScanner(conn) for scanner.Scan() { var evt FileEvent if err := json.Unmarshal(scanner.Bytes(), &evt); err != nil { logDebug("bad event: %v", err) continue } logDebug("event: %s %s", evt.Op, evt.Path) if isGitDir(evt.Path) { continue } if stack.IsIgnored(evt.Path) { continue } destPath := filepath.Join(*dataDir, evt.Path) srcPath := filepath.Join(*hostDir, evt.Path) switch evt.Op { case "create": syncCreate(srcPath, destPath, evt.Path, manifest, stack, *dryRun, nil) case "write": syncWrite(srcPath, destPath, evt.Path, manifest, *dryRun) case "remove": syncRemove(destPath, evt.Path, manifest, *dryRun) } } if err := scanner.Err(); err != nil { return err } return nil }