rsend ===== Push configured phone folders to a home SSH host using real rsync. rsend backs up folders such as DCIM, WhatsApp media, and call recordings to a remote host over SSH. One-way push only, on WiFi by default, periodic or manual. Small, boring, self-contained: real rsync and a pure-Go SSH transport, both built from source and shipped inside the APK. No foreign prebuilt binaries. What it does ------------ - Pushes one or more local folders to remote paths over rsync-over-SSH. - Incremental: only changed files move; large videos resume (rsync --partial). - Per-folder deletion policy: additive backup (default) or mirror (--delete). - Runs on unmetered WiFi, periodically (WorkManager) or on demand. - ed25519 key auth with strict, pinned host-key verification. Layout ------ - rsh/ pure-Go SSH transport used as rsync's remote shell (-e). - rsync/ build script that compiles pinned rsync from source via the NDK. - app/ Android app (Kotlin, classic Views); bundles both as lib*.so. - ci/ CI-agnostic build and test scripts; the Makefile drives them. - metadata/ reproducibility and F-Droid build notes. - versions pinned toolchain and source versions; the single source of truth. Build ----- The repository is self-contained: clone it and the build fetches and pins everything else from source. The only host prerequisites are a POSIX shell, curl, python3, tar, and git. The toolchain (JDK, Go, Android SDK and NDK, Gradle) and the rsync source are downloaded and version-pinned by the build (see versions for every pin). make setup # provision the toolchain into $HOME/toolchains . "$HOME/toolchains/env.sh" # put it on PATH (do this in each shell) make # rsync (NDK) -> rsh (Go) -> APK make test # Go tests plus the host-side end-to-end push make verify-repro # build twice, diff the artifacts The APK lands under app/build/outputs/apk/. It is unsigned by default; signing config is local and gitignored. Continuous integration runs the same scripts: ci/setup-toolchain.sh then ci/build.sh and ci/test.sh. Run --- 1. Install the APK and grant all-files access and notifications. 2. Generate a key in the app and add the shown public key to the home host's ~/.ssh/authorized_keys. 3. Set the remote host, user, and port; run Test connection and accept the pinned host-key fingerprint. 4. Add folders, choose each folder's deletion policy, set the schedule. 5. Tap Sync now, or wait for the periodic WiFi sync. For scheduled backup to run reliably, tap Battery and allow rsend to ignore battery optimization. Phones with aggressive power management (Samsung, Xiaomi, and others) otherwise delay or skip background jobs; see dontkillmyapp.com. Server ------ Any host with sshd and rsync works, old rsyncs included (stock macOS); rsend pushes over rsync-over-SSH, not SFTP. rsync creates the final component of a folder's remote path on its own; create deeper missing parents once with mkdir -p on the server. The account is confined to "rsync into one folder and nothing else" by a forced rrsync command, not by its shell. sshd runs that command through the account's login shell, so the shell must be real: /bin/sh works, while /bin/false or /sbin/nologin would break rsync. The confinement comes from key-only auth, the forced command, and rrsync's write-only root. As root on the server: useradd -m -d /srv/backup -s /bin/sh rsendbackup passwd -l rsendbackup mkdir -p /srv/backup/phone && chown -R rsendbackup:rsendbackup /srv/backup This assumes UsePAM yes in sshd_config (the default on most distros; set it if your build has it off). With UsePAM yes a password-locked account still accepts key logins. With UsePAM no, sshd refuses any locked account even for keys ("account is locked"); there, skip passwd -l, set PasswordAuthentication no, and leave the account with a non-locked password field. Put the app's public key in /srv/backup/.ssh/authorized_keys as one restricted line that forces rrsync, write-only, into that directory: command="/usr/bin/rrsync -wo /srv/backup/phone",restrict ssh-ed25519 AAAA... rsend To apply the same limit account-wide, so it holds even if another key is added later and not only on this key, force the command in sshd_config and reload sshd: Match User rsendbackup ForceCommand /usr/bin/rrsync -wo /srv/backup/phone PasswordAuthentication no PermitTTY no AllowTcpForwarding no AllowAgentForwarding no AllowStreamLocalForwarding no X11Forwarding no The account can then do nothing but receive rsync into that folder: every session, with any key, is forced through rrsync, which rejects anything but a plain rsync transfer into its root. Remote paths are relative to that root, so set a folder's remote path to DCIM, not an absolute path. rrsync may instead live at /usr/share/rsync/scripts/rrsync; check command -v rrsync. Verify the host key from Test connection against ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub on the server. Debug ----- - The in-app log viewer tails the plain-text rsync log. - Reproduce a sync from a shell using the same rsync invocation rsend logs at the top of each run. - rsh transport: RSH_KEY, RSH_KNOWN_HOSTS, and RSH_PORT select the key, known_hosts file, and port. Run rsh by hand to isolate SSH from rsync. License ------- GPLv3. Bundling rsync makes the whole app GPLv3; see LICENSE.