From 7e04941bccb2683f8a6e3ee38a99c50129234dd1 Mon Sep 17 00:00:00 2001 From: Lena Date: Thu, 1 Jan 2026 00:00:00 +0000 Subject: rsend: push phone folders to a home SSH host over rsync A small Android app for one-way folder backup, a KISS alternative to Syncthing. It bundles rsync (built from pinned source via the NDK) and a pure-Go SSH transport, both shipped in the APK as lib*.so and run from the native library directory. rsend pins the host key, stores the ed25519 identity Keystore-encrypted, pushes each folder additively or as a mirror, and runs on demand or on a WiFi-only schedule. The build is self-contained and reproducible: make setup provisions the toolchain, make builds rsync, rsh, and the APK. --- rsh/e2e_test.go | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 rsh/e2e_test.go (limited to 'rsh/e2e_test.go') diff --git a/rsh/e2e_test.go b/rsh/e2e_test.go new file mode 100644 index 0000000..1fa80c7 --- /dev/null +++ b/rsh/e2e_test.go @@ -0,0 +1,84 @@ +package main + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" + "strconv" + "testing" +) + +// TestEndToEndRealRsync drives the actual rsync binary through the actual rsh +// transport binary into the in-process SSH server, which runs the remote rsync +// via /bin/sh. It proves real rsync + the Go transport move files intact. +func TestEndToEndRealRsync(t *testing.T) { + if testing.Short() { + t.Skip("short mode") + } + rsyncBin, err := exec.LookPath("rsync") + if err != nil { + t.Skip("rsync not installed") + } + + // Build the rsh binary so rsync can exec it as its remote shell. + rshBin := filepath.Join(t.TempDir(), "rsh") + if out, err := exec.Command("go", "build", "-o", rshBin, ".").CombinedOutput(); err != nil { + t.Fatalf("build rsh: %v\n%s", err, out) + } + + keyPath, pub := genClientKey(t) + srv := newTestServer(t, pub, shellExec) + kh := writeKnownHosts(t, "127.0.0.1", srv.port(), srv.hostKey.PublicKey()) + + src := t.TempDir() + dst := t.TempDir() + mustWrite(t, filepath.Join(src, "a.txt"), "alpha") + mustWrite(t, filepath.Join(src, "sub", "b.bin"), "\x00\x01\x02\x03beta") + mustWrite(t, filepath.Join(src, "sub", "c.txt"), "gamma gamma gamma") + + cmd := exec.Command(rsyncBin, "-a", "-e", rshBin, src+"/", "u@127.0.0.1:"+dst+"/") + cmd.Env = append(os.Environ(), + "RSH_KEY="+keyPath, + "RSH_KNOWN_HOSTS="+kh, + "RSH_PORT="+strconv.Itoa(srv.port()), + ) + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("rsync: %v\n%s", err, out) + } + + checkSame(t, src, dst) +} + +func mustWrite(t *testing.T, path, content string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatal(err) + } +} + +func checkSame(t *testing.T, src, dst string) { + t.Helper() + err := filepath.Walk(src, func(p string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return err + } + rel, _ := filepath.Rel(src, p) + want, _ := os.ReadFile(p) + got, err := os.ReadFile(filepath.Join(dst, rel)) + if err != nil { + t.Errorf("missing on dest: %s (%v)", rel, err) + return nil + } + if !bytes.Equal(got, want) { + t.Errorf("content mismatch for %s", rel) + } + return nil + }) + if err != nil { + t.Fatal(err) + } +} -- cgit v1.2.3