fix: rsync double SSH spec in RunRemote, restore dropped flags in Run
- Fix Bug A: RunRemote was re-prefixing destination with user@host: when engine.go:135 already pre-prefixed it for remote-to-remote, causing "admin@host:admin@host:/path" to be passed to rsync - Fix Bug B: Run used args[1:] when inserting -e ssh, silently dropping the first rsync flag (e.g. -aP became flag-less) - Fix Bug C: RunRemote extracted source/dest in wrong order for pull direction (buildArgs reverses them but RunRemote assumed push order) - Add rsync_runner_test.go covering buildArgs, RunRemote prefixing, and flag preservation
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
BINARY=syncserver
|
BINARY=syncserver
|
||||||
VERSION?=1.0.36
|
VERSION?=1.0.37
|
||||||
GO?=go
|
GO?=go
|
||||||
LDFLAGS=-s -w -X main.version=$(VERSION) -X main.commit=$(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
LDFLAGS=-s -w -X main.version=$(VERSION) -X main.commit=$(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||||
BUILD_FLAGS=CGO_ENABLED=0
|
BUILD_FLAGS=CGO_ENABLED=0
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ import (
|
|||||||
"github.com/syncserver/internal/syncengine"
|
"github.com/syncserver/internal/syncengine"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.0.36"
|
var version = "1.0.37"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfgPath := flag.String("config", "", "Path to config.yaml")
|
cfgPath := flag.String("config", "", "Path to config.yaml")
|
||||||
|
|||||||
@@ -40,16 +40,19 @@ func NewRsyncRunner(sshDir, privKey string) *RsyncRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *RsyncRunner) Run(ctx context.Context, pair *SyncPairConfig, onLine func(stream string, line string)) (*RsyncResult, error) {
|
func (r *RsyncRunner) Run(ctx context.Context, pair *SyncPairConfig, onLine func(stream string, line string)) (*RsyncResult, error) {
|
||||||
args := r.buildArgs(pair)
|
cmd := r.buildRsyncCmd(pair)
|
||||||
|
return r.runCmd(ctx, cmd, onLine)
|
||||||
|
}
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "rsync", args...)
|
func (r *RsyncRunner) buildRsyncCmd(pair *SyncPairConfig) *exec.Cmd {
|
||||||
|
args := r.buildArgs(pair)
|
||||||
|
cmd := exec.CommandContext(context.Background(), "rsync", args...)
|
||||||
if r.privKey != "" {
|
if r.privKey != "" {
|
||||||
sshCmd := fmt.Sprintf("ssh -i %s -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=%s",
|
sshCmd := fmt.Sprintf("ssh -i %s -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=%s",
|
||||||
r.privKey, strings.TrimRight(r.sshDir, "/")+"/known_hosts")
|
r.privKey, strings.TrimRight(r.sshDir, "/")+"/known_hosts")
|
||||||
cmd.Args = append([]string{"rsync", "-e", sshCmd}, args[1:]...)
|
cmd.Args = append([]string{"rsync", "-e", sshCmd}, args...)
|
||||||
}
|
}
|
||||||
|
return cmd
|
||||||
return r.runCmd(ctx, cmd, onLine)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RsyncRunner) buildArgs(pair *SyncPairConfig) []string {
|
func (r *RsyncRunner) buildArgs(pair *SyncPairConfig) []string {
|
||||||
@@ -93,17 +96,39 @@ func (r *RsyncRunner) RunRemote(ctx context.Context, pair *SyncPairConfig, src *
|
|||||||
destKey = filepath.Join(r.sshDir, "id_ed25519")
|
destKey = filepath.Join(r.sshDir, "id_ed25519")
|
||||||
}
|
}
|
||||||
|
|
||||||
destUserHost := fmt.Sprintf("%s@%s", dst.SSHUser, dst.Host)
|
srcUserHost := fmt.Sprintf("%s@%s", src.SSHUser, src.Host)
|
||||||
args := r.buildArgs(pair)
|
args := r.buildArgs(pair)
|
||||||
|
|
||||||
innerSSH := fmt.Sprintf("ssh -i %s -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=%s",
|
innerSSH := fmt.Sprintf("ssh -i %s -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=%s",
|
||||||
destKey, filepath.Join(r.sshDir, "known_hosts"))
|
destKey, filepath.Join(r.sshDir, "known_hosts"))
|
||||||
rsyncFlags := strings.Join(args[:len(args)-2], " ")
|
rsyncFlags := strings.Join(args[:len(args)-2], " ")
|
||||||
sourcePath := args[len(args)-2]
|
|
||||||
destPath := args[len(args)-1]
|
var sourcePath, destPath string
|
||||||
|
if pair.Direction == "pull" {
|
||||||
|
sourcePath = args[len(args)-1]
|
||||||
|
destPath = args[len(args)-2]
|
||||||
|
} else {
|
||||||
|
sourcePath = args[len(args)-2]
|
||||||
|
destPath = args[len(args)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
var remoteSrc, remoteDst string
|
||||||
|
if pair.Direction == "pull" {
|
||||||
|
remoteSrc = sourcePath
|
||||||
|
remoteDst = destPath
|
||||||
|
if !strings.Contains(destPath, "@") {
|
||||||
|
remoteDst = fmt.Sprintf("%s@%s:%s", dst.SSHUser, dst.Host, destPath)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
remoteDst = destPath
|
||||||
|
if !strings.Contains(destPath, "@") {
|
||||||
|
remoteDst = fmt.Sprintf("%s@%s:%s", dst.SSHUser, dst.Host, destPath)
|
||||||
|
}
|
||||||
|
remoteSrc = fmt.Sprintf("%s:%s", srcUserHost, sourcePath)
|
||||||
|
}
|
||||||
|
|
||||||
remoteCmd := fmt.Sprintf("rsync %s -e %q %s %s",
|
remoteCmd := fmt.Sprintf("rsync %s -e %q %s %s",
|
||||||
rsyncFlags, innerSSH, sourcePath, destUserHost+":"+destPath)
|
rsyncFlags, innerSSH, remoteSrc, remoteDst)
|
||||||
|
|
||||||
sshArgs := []string{
|
sshArgs := []string{
|
||||||
"-i", src.PrivKey,
|
"-i", src.PrivKey,
|
||||||
|
|||||||
@@ -0,0 +1,259 @@
|
|||||||
|
package syncengine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type remoteCmdTest struct {
|
||||||
|
srcHost string
|
||||||
|
srcUser string
|
||||||
|
srcPath string
|
||||||
|
dstHost string
|
||||||
|
dstUser string
|
||||||
|
dstPath string
|
||||||
|
direction string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc remoteCmdTest) build() (srcArg, dstArg string) {
|
||||||
|
srcUserHost := tc.srcUser + "@" + tc.srcHost
|
||||||
|
dstUserHost := tc.dstUser + "@" + tc.dstHost
|
||||||
|
|
||||||
|
runner := NewRsyncRunner("/tmp/ssh", "")
|
||||||
|
args := runner.buildArgs(&SyncPairConfig{
|
||||||
|
Source: tc.srcPath,
|
||||||
|
Dest: tc.dstPath,
|
||||||
|
Direction: tc.direction,
|
||||||
|
})
|
||||||
|
|
||||||
|
var sourcePath, destPath string
|
||||||
|
if tc.direction == "pull" {
|
||||||
|
sourcePath = args[len(args)-1]
|
||||||
|
destPath = args[len(args)-2]
|
||||||
|
} else {
|
||||||
|
sourcePath = args[len(args)-2]
|
||||||
|
destPath = args[len(args)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
var remoteSrc, remoteDst string
|
||||||
|
if tc.direction == "pull" {
|
||||||
|
remoteSrc = sourcePath
|
||||||
|
remoteDst = destPath
|
||||||
|
if !strings.Contains(destPath, "@") {
|
||||||
|
remoteDst = dstUserHost + ":" + destPath
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
remoteDst = destPath
|
||||||
|
if !strings.Contains(destPath, "@") {
|
||||||
|
remoteDst = dstUserHost + ":" + destPath
|
||||||
|
}
|
||||||
|
remoteSrc = srcUserHost + ":" + sourcePath
|
||||||
|
}
|
||||||
|
return remoteSrc, remoteDst
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildArgs_Push(t *testing.T) {
|
||||||
|
runner := NewRsyncRunner("/tmp/ssh", "")
|
||||||
|
pair := &SyncPairConfig{
|
||||||
|
Source: "/local/src",
|
||||||
|
Dest: "admin@10.5.0.144:/remote/dst",
|
||||||
|
Direction: "push",
|
||||||
|
RsyncFlags: "-aP",
|
||||||
|
ExcludePatterns: []string{},
|
||||||
|
}
|
||||||
|
args := runner.buildArgs(pair)
|
||||||
|
if args[0] != "-aP" {
|
||||||
|
t.Errorf("first flag = %q, want %q", args[0], "-aP")
|
||||||
|
}
|
||||||
|
if args[len(args)-2] != "/local/src" {
|
||||||
|
t.Errorf("source = %q, want %q", args[len(args)-2], "/local/src")
|
||||||
|
}
|
||||||
|
if args[len(args)-1] != "admin@10.5.0.144:/remote/dst" {
|
||||||
|
t.Errorf("dest = %q, want %q", args[len(args)-1], "admin@10.5.0.144:/remote/dst")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildArgs_Pull(t *testing.T) {
|
||||||
|
runner := NewRsyncRunner("/tmp/ssh", "")
|
||||||
|
pair := &SyncPairConfig{
|
||||||
|
Source: "admin@10.5.0.144:/remote/src",
|
||||||
|
Dest: "/local/dst",
|
||||||
|
Direction: "pull",
|
||||||
|
RsyncFlags: "-aP",
|
||||||
|
ExcludePatterns: []string{},
|
||||||
|
}
|
||||||
|
args := runner.buildArgs(pair)
|
||||||
|
if args[len(args)-2] != "/local/dst" {
|
||||||
|
t.Errorf("pull: second-to-last (dest) = %q, want %q", args[len(args)-2], "/local/dst")
|
||||||
|
}
|
||||||
|
if args[len(args)-1] != "admin@10.5.0.144:/remote/src" {
|
||||||
|
t.Errorf("pull: last (source) = %q, want %q", args[len(args)-1], "admin@10.5.0.144:/remote/src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildArgs_Mirror(t *testing.T) {
|
||||||
|
runner := NewRsyncRunner("/tmp/ssh", "")
|
||||||
|
pair := &SyncPairConfig{
|
||||||
|
Source: "/local/src",
|
||||||
|
Dest: "admin@10.5.0.144:/remote/dst",
|
||||||
|
Direction: "mirror",
|
||||||
|
RsyncFlags: "-aP",
|
||||||
|
ExcludePatterns: []string{},
|
||||||
|
}
|
||||||
|
args := runner.buildArgs(pair)
|
||||||
|
foundDelete := false
|
||||||
|
for _, a := range args {
|
||||||
|
if a == "--delete" {
|
||||||
|
foundDelete = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundDelete {
|
||||||
|
t.Errorf("mirror args = %v, want --delete present", args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildArgs_MultiTokenFlags(t *testing.T) {
|
||||||
|
runner := NewRsyncRunner("/tmp/ssh", "")
|
||||||
|
pair := &SyncPairConfig{
|
||||||
|
Source: "/local/src",
|
||||||
|
Dest: "admin@10.5.0.144:/remote/dst",
|
||||||
|
Direction: "push",
|
||||||
|
RsyncFlags: "-aP --partial",
|
||||||
|
ExcludePatterns: []string{},
|
||||||
|
}
|
||||||
|
args := runner.buildArgs(pair)
|
||||||
|
if args[0] != "-aP" {
|
||||||
|
t.Errorf("first flag = %q, want %q", args[0], "-aP")
|
||||||
|
}
|
||||||
|
if args[1] != "--partial" {
|
||||||
|
t.Errorf("second flag = %q, want %q", args[1], "--partial")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildArgs_ExcludePatterns(t *testing.T) {
|
||||||
|
runner := NewRsyncRunner("/tmp/ssh", "")
|
||||||
|
pair := &SyncPairConfig{
|
||||||
|
Source: "/local/src",
|
||||||
|
Dest: "admin@10.5.0.144:/remote/dst",
|
||||||
|
Direction: "push",
|
||||||
|
RsyncFlags: "-aP",
|
||||||
|
ExcludePatterns: []string{"*.tmp", ".DS_Store"},
|
||||||
|
}
|
||||||
|
args := runner.buildArgs(pair)
|
||||||
|
var excludes []string
|
||||||
|
for _, a := range args {
|
||||||
|
if strings.HasPrefix(a, "--exclude=") {
|
||||||
|
excludes = append(excludes, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(excludes) != 2 {
|
||||||
|
t.Errorf("excludes = %v, want 2 exclude entries", excludes)
|
||||||
|
}
|
||||||
|
if excludes[0] != "--exclude=*.tmp" {
|
||||||
|
t.Errorf("exclude[0] = %q, want %q", excludes[0], "--exclude=*.tmp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRemote_PushDestNoDoublePrefix(t *testing.T) {
|
||||||
|
tc := remoteCmdTest{
|
||||||
|
srcHost: "10.5.1.10",
|
||||||
|
srcUser: "root",
|
||||||
|
srcPath: "/share/homes/admin/media",
|
||||||
|
dstHost: "10.5.0.144",
|
||||||
|
dstUser: "admin",
|
||||||
|
dstPath: "admin@10.5.0.144:/share/media/peliculas",
|
||||||
|
direction: "push",
|
||||||
|
}
|
||||||
|
_, dstArg := tc.build()
|
||||||
|
|
||||||
|
if countOccurrences(dstArg, "@") > 1 {
|
||||||
|
t.Errorf("push dest %q has double SSH spec", dstArg)
|
||||||
|
}
|
||||||
|
if dstArg != "admin@10.5.0.144:/share/media/peliculas" {
|
||||||
|
t.Errorf("push dest = %q, want 'admin@10.5.0.144:/share/media/peliculas'", dstArg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRemote_PushSrcCorrectlyPrefixed(t *testing.T) {
|
||||||
|
tc := remoteCmdTest{
|
||||||
|
srcHost: "10.5.1.10",
|
||||||
|
srcUser: "root",
|
||||||
|
srcPath: "/share/homes/admin/media",
|
||||||
|
dstHost: "10.5.0.144",
|
||||||
|
dstUser: "admin",
|
||||||
|
dstPath: "/share/media/peliculas",
|
||||||
|
direction: "push",
|
||||||
|
}
|
||||||
|
srcArg, dstArg := tc.build()
|
||||||
|
|
||||||
|
if !strings.HasPrefix(srcArg, "root@10.5.1.10:/") {
|
||||||
|
t.Errorf("push src = %q, want prefix 'root@10.5.1.10:/'", srcArg)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(dstArg, "admin@10.5.0.144:/") {
|
||||||
|
t.Errorf("push dst = %q, want prefix 'admin@10.5.0.144:/'", dstArg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRemote_PullSrcNotPrefixed(t *testing.T) {
|
||||||
|
tc := remoteCmdTest{
|
||||||
|
srcHost: "10.5.0.144",
|
||||||
|
srcUser: "admin",
|
||||||
|
srcPath: "admin@10.5.0.144:/share/media/peliculas",
|
||||||
|
dstHost: "10.5.1.10",
|
||||||
|
dstUser: "root",
|
||||||
|
dstPath: "/share/data",
|
||||||
|
direction: "pull",
|
||||||
|
}
|
||||||
|
srcArg, _ := tc.build()
|
||||||
|
|
||||||
|
if countOccurrences(srcArg, "@") > 1 {
|
||||||
|
t.Errorf("pull src %q has double SSH spec", srcArg)
|
||||||
|
}
|
||||||
|
if srcArg != "admin@10.5.0.144:/share/media/peliculas" {
|
||||||
|
t.Errorf("pull src = %q, want 'admin@10.5.0.144:/share/media/peliculas'", srcArg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func countOccurrences(s, substr string) int {
|
||||||
|
return strings.Count(s, substr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRun_FlagsPreservedWithPrivKey(t *testing.T) {
|
||||||
|
runner := NewRsyncRunner("/tmp/ssh", "/tmp/ssh/id_ed25519")
|
||||||
|
pair := &SyncPairConfig{
|
||||||
|
Source: "/local/src",
|
||||||
|
Dest: "admin@10.5.0.144:/remote/dst",
|
||||||
|
Direction: "push",
|
||||||
|
RsyncFlags: "-aP --partial",
|
||||||
|
ExcludePatterns: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := runner.buildRsyncCmd(pair)
|
||||||
|
|
||||||
|
if cmd.Args[0] != "rsync" {
|
||||||
|
t.Errorf("cmd.Args[0] = %q, want 'rsync'", cmd.Args[0])
|
||||||
|
}
|
||||||
|
if cmd.Args[1] != "-e" {
|
||||||
|
t.Errorf("cmd.Args[1] = %q, want '-e' (the -e flag for ssh)", cmd.Args[1])
|
||||||
|
}
|
||||||
|
if !strings.Contains(cmd.Args[2], "ssh -i") {
|
||||||
|
t.Errorf("cmd.Args[2] = %q, want ssh -i ...", cmd.Args[2])
|
||||||
|
}
|
||||||
|
hasAP := false
|
||||||
|
hasPartial := false
|
||||||
|
for i, a := range cmd.Args {
|
||||||
|
if a == "-aP" && i > 2 {
|
||||||
|
hasAP = true
|
||||||
|
}
|
||||||
|
if a == "--partial" && i > 2 {
|
||||||
|
hasPartial = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasAP {
|
||||||
|
t.Errorf("cmd.Args = %v, want -aP flag preserved (not dropped)", cmd.Args)
|
||||||
|
}
|
||||||
|
if !hasPartial {
|
||||||
|
t.Errorf("cmd.Args = %v, want --partial flag present", cmd.Args)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user