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 an unmetered network 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 an unmetered network, 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 everything
else. The host prerequisites are a POSIX shell, coreutils, make, curl, python3,
tar, and git. The toolchain archives and rsync source are version- and
checksum-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, real-rsync integration, and Kotlin tests
    make verify-repro             # build twice, compare the unsigned APKs

`make test` also requires host rsync. The APK lands under
app/build/outputs/apk/. It is unsigned by default and cannot be installed until
signed. Continuous integration runs the same scripts: ci/setup-toolchain.sh,
ci/build.sh, and ci/test.sh.

For a locally installable APK, create a keystore and a gitignored
keystore.properties before building:

    keytool -genkeypair -keystore rsend.jks -alias rsend -keyalg EC -validity 3650
    cat > keystore.properties <<EOF
    storeFile=rsend.jks
    storePassword=change-me
    keyAlias=rsend
    keyPassword=change-me
    EOF

Keep both files private and backed up. The signed output is
app/build/outputs/apk/release/app-release.apk.


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 sync. Scheduled intervals have a
   15-minute minimum imposed by WorkManager.

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 displays the plain-text rsync log; tap Refresh to reload.
- The log records the rsync arguments and complete process output for 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.
