fix: reject + handle remote-to-remote sync pairs
Option A (validation): - handlers_syncpairs.go: reject create/update when both SourceMachineID and DestMachineID are set; clear error message explains the constraint - engine.go: detect 'both remote' rsync error at runtime and surface it as error_code=remote_to_remote_unsupported with a human-readable message Option B (remote-to-remote support): - rsync_runner.go: add RunRemote() method that SSHs to the source machine and runs rsync locally there (src=local path, dst=user@host:/path), streaming output back through the onLine callback - engine.go: when both srcMachine and dstMachine are non-nil, use RunRemote() instead of Run(), SSHing to srcMachine and running rsync from there. Also wake dstMachine via WoL when both sides are remote.
This commit is contained in:
@@ -155,3 +155,135 @@ func (r *RsyncRunner) buildArgs(pair *SyncPairConfig) []string {
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
func (r *RsyncRunner) buildRemoteArgs(pair *SyncPairConfig) []string {
|
||||
var args []string
|
||||
|
||||
flags := strings.Fields(pair.RsyncFlags)
|
||||
args = append(args, flags...)
|
||||
|
||||
for _, pattern := range pair.ExcludePatterns {
|
||||
args = append(args, "--exclude="+pattern)
|
||||
}
|
||||
|
||||
if pair.Direction == "mirror" {
|
||||
args = append(args, "--delete")
|
||||
}
|
||||
|
||||
if pair.Direction == "pull" {
|
||||
args = append(args, pair.Dest, pair.Source)
|
||||
} else {
|
||||
args = append(args, pair.Source, pair.Dest)
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
type RemoteMachine struct {
|
||||
Host string
|
||||
Port int
|
||||
SSHUser string
|
||||
PrivKey string
|
||||
}
|
||||
|
||||
func (r *RsyncRunner) RunRemote(ctx context.Context, pair *SyncPairConfig, remote RemoteMachine, onLine func(stream string, line string)) (*RsyncResult, error) {
|
||||
args := r.buildRemoteArgs(pair)
|
||||
|
||||
sshArgs := []string{
|
||||
"ssh",
|
||||
"-i", remote.PrivKey,
|
||||
"-o", "StrictHostKeyChecking=accept-new",
|
||||
"-o", "UserKnownHostsFile=" + strings.TrimRight(r.sshDir, "/") + "/known_hosts",
|
||||
"-tt",
|
||||
"-p", fmt.Sprintf("%d", remote.Port),
|
||||
fmt.Sprintf("%s@%s", remote.SSHUser, remote.Host),
|
||||
"rsync",
|
||||
}
|
||||
sshArgs = append(sshArgs, args...)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "ssh", sshArgs...)
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stdout pipe: %w", err)
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stderr pipe: %w", err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, fmt.Errorf("starting ssh: %w", err)
|
||||
}
|
||||
|
||||
var outLines, errLines []string
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
br := io.Reader(stdout)
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, err := br.Read(buf)
|
||||
if n > 0 {
|
||||
line := strings.TrimRight(string(buf[:n]), "\r\n")
|
||||
if line != "" {
|
||||
outLines = append(outLines, line)
|
||||
if onLine != nil {
|
||||
onLine("stdout", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-done:
|
||||
default:
|
||||
close(done)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
br := io.Reader(stderr)
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, err := br.Read(buf)
|
||||
if n > 0 {
|
||||
line := strings.TrimRight(string(buf[:n]), "\r\n")
|
||||
if line != "" {
|
||||
errLines = append(errLines, line)
|
||||
if onLine != nil {
|
||||
onLine("stderr", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-done:
|
||||
default:
|
||||
close(done)
|
||||
}
|
||||
}()
|
||||
|
||||
err = cmd.Wait()
|
||||
<-done
|
||||
|
||||
result := &RsyncResult{
|
||||
ExitCode: 0,
|
||||
Stdout: strings.Join(outLines, "\n"),
|
||||
Stderr: strings.Join(errLines, "\n"),
|
||||
Stats: ParseFinalStats(strings.Join(outLines, "\n")),
|
||||
}
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
result.ExitCode = exitErr.ExitCode()
|
||||
} else {
|
||||
result.ExitCode = -1
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user