package main import ( "flag" "fmt" "os" "runtime" ) func main() { if len(os.Args) < 2 { usage() os.Exit(2) } var err error switch os.Args[1] { case "sync": err = cmdSync(os.Args[2:]) case "agent": err = cmdAgent(os.Args[2:]) case "version", "--version": fmt.Printf("clonetool %s/%s\n", runtime.GOOS, runtime.GOARCH) return case "-h", "--help", "help": usage() return default: usage() os.Exit(2) } if err != nil { fmt.Fprintln(os.Stderr, "clonetool: "+err.Error()) os.Exit(1) } } func usage() { fmt.Fprint(os.Stderr, `clonetool - block-level file/device sync Usage: clonetool sync --source LOC --dest LOC [options] clonetool version clonetool agent --role {control|sink|source-stream} ... (internal, spawned automatically) LOC is either a local path, or [user@]host:path for a path reached over SSH. Source, destination, and the machine running "sync" (the manager) may all be different machines: the manager only orchestrates, it never reads or writes a single block itself. clonetool keeps no state between runs: every sync re-reads and re-hashes both the source and the destination and transfers only the blocks that differ. Options for sync: --block-size SIZE block size, e.g. 4M (default 4M) --job NAME optional label shown in progress/log output --yes don't prompt before shrinking an existing destination file --sudo MODE device-access privilege escalation: auto (escalate on a permission error, default), always, or never --deploy copy this binary to remote hosts that lack it (default true; --deploy=false to disable) --connect-timeout SEC ssh connect timeout for the push/pull direction probe (default 8) --ssh PATH ssh binary to use (default "ssh") --ssh-opt OPT extra "-o OPT" passed to ssh (repeatable) --remote-bin PATH path to clonetool on remote hosts (default "clonetool") --manager-host HOST address peers should use to reach this machine, when source or dest has no host part (defaults to the local hostname) `) } type stringSlice []string func (s *stringSlice) String() string { return fmt.Sprint([]string(*s)) } func (s *stringSlice) Set(v string) error { *s = append(*s, v) return nil } func cmdSync(args []string) error { fs := flag.NewFlagSet("sync", flag.ContinueOnError) job := fs.String("job", "", "optional label for progress/log output") source := fs.String("source", "", "source location (required)") dest := fs.String("dest", "", "destination location (required)") blockSizeStr := fs.String("block-size", "4M", "block size, e.g. 4M") yes := fs.Bool("yes", false, "don't prompt before shrinking destination") sudoMode := fs.String("sudo", "auto", "device-access privilege escalation: auto|always|never") deploy := fs.Bool("deploy", true, "copy this binary to remote hosts that lack it") connectTimeout := fs.Int("connect-timeout", defaultConnectTimeoutSec, "ssh connect timeout (seconds)") sshBin := fs.String("ssh", "ssh", "ssh binary") remoteBin := fs.String("remote-bin", "clonetool", "clonetool path on remote hosts") managerHost := fs.String("manager-host", "", "address peers use to reach this machine") var sshOpts stringSlice fs.Var(&sshOpts, "ssh-opt", `extra "-o OPT" passed to ssh (repeatable)`) if err := fs.Parse(args); err != nil { return err } if *source == "" || *dest == "" { fs.Usage() return fmt.Errorf("--source and --dest are required") } blockSize, err := parseSize(*blockSizeStr) if err != nil { return fmt.Errorf("--block-size: %w", err) } switch *sudoMode { case "auto", "always", "never": default: return fmt.Errorf("--sudo: want auto|always|never, got %q", *sudoMode) } return runSync(SyncConfig{ Job: *job, Source: *source, Dest: *dest, BlockSize: blockSize, Yes: *yes, Sudo: *sudoMode, Deploy: *deploy, ConnectTimeoutSec: *connectTimeout, SSHBin: *sshBin, SSHOpts: sshOpts, RemoteBin: *remoteBin, ManagerHost: *managerHost, }) } func parseSize(s string) (int64, error) { if s == "" { return 0, fmt.Errorf("empty size") } mult := int64(1) numPart := s switch s[len(s)-1] { case 'k', 'K': mult = 1024 numPart = s[:len(s)-1] case 'm', 'M': mult = 1024 * 1024 numPart = s[:len(s)-1] case 'g', 'G': mult = 1024 * 1024 * 1024 numPart = s[:len(s)-1] } var n int64 if _, err := fmt.Sscanf(numPart, "%d", &n); err != nil { return 0, fmt.Errorf("invalid size %q", s) } if n <= 0 { return 0, fmt.Errorf("size must be positive") } return n * mult, nil }