clonetool/main.go
2026-09-06 20:13:13 +02:00

144 lines
4.5 KiB
Go

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 "version", "--version":
fmt.Println(versionString())
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
}