package main import ( "flag" "fmt" "os" ) 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 "-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 --job NAME --source LOC --dest LOC [options] 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. Options for sync: --block-size SIZE block size, e.g. 4M (default 4M) --state-dir DIR where job hash-tables are stored (default ~/.clonetool/jobs) --yes don't prompt before shrinking an existing destination file --force rebind this job to a new source/dest, discarding hash history --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) The destination must not be modified by anything else between syncs of the same job: repeated runs trust the job's recorded hash table instead of re-reading the destination. `) } 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", "", "job name (required)") 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") stateDir := fs.String("state-dir", defaultStateDir(), "job state directory") yes := fs.Bool("yes", false, "don't prompt before shrinking destination") force := fs.Bool("force", false, "rebind job to a new source/dest") 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 *job == "" || *source == "" || *dest == "" { fs.Usage() return fmt.Errorf("--job, --source and --dest are required") } blockSize, err := parseSize(*blockSizeStr) if err != nil { return fmt.Errorf("--block-size: %w", err) } return runSync(SyncConfig{ Job: *job, Source: *source, Dest: *dest, BlockSize: blockSize, StateDir: *stateDir, Yes: *yes, Force: *force, 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 }