144 lines
4.5 KiB
Go
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
|
|
}
|