133 lines
4.1 KiB
Go
133 lines
4.1 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 "-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
|
|
}
|