rsend ===== Push configured phone folders to home SSH hosts using real rsync. rsend backs up folders such as DCIM, WhatsApp media, and call recordings to one or more remote hosts 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 downloaded application .so files are repackaged. What it does ------------ - Pushes one or more local folders to one or more SSH hosts over rsync, each folder mapped to a named remote. - Incremental: only changed files move; large videos resume. Partial transfers are staged in a .rsend-partial directory, so an interrupted sync never replaces a complete file on the server with a truncated one. - Per-folder deletion policy: additive backup (default) or mirror (--delete-after). A mirror follows rsync exactly: an empty readable source empties its remote destination. A missing, non-directory, root, or unreadable source fails before rsync starts. Deletions happen only after transfers, so an interrupted run never removes the server's copy of a file whose replacement has not arrived. Every item rsync removes there is named in the log, and a mirror that ends with files unaccounted for is reported as failed. - Rejects duplicate and ancestor remote destinations on the same configured endpoint. The check is lexical, case-insensitive, and does not allow absolute and relative paths to be mixed for one endpoint. Root, bare home, dot, and parent components are rejected. Separate folder mappings use separate trees. - Regular files and directories only. A source path that is itself a symlink is refused. Symlinks, devices and sockets inside it are skipped and named in the log ("skipping non-regular file"), which is what media backup wants; rsync's -l would copy them if you ever need it. - Native executables and APK packaging are aligned for Android devices with 4 KB or 16 KB memory pages. - Runs on an unmetered network, periodically (WorkManager) or on demand. A manual sync obeys the same unmetered setting, and says so and stops rather than queueing itself until the phone next sees wifi. - Ed25519 client-key auth with strict, pinned host-key verification. rsend accepts modern Ed25519, ECDSA, and RSA-SHA2 host keys, pins the key type with the key, and warns loudly if a pinned host presents a different one. 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. - fastlane/ F-Droid store listing: descriptions, changelogs, screenshots. - THIRD_PARTY complete shipped-component inventory and license notices. - APACHE-2.0 license terms for the shipped Apache-licensed components. - REPRODUCIBLE-BUILDS determinism, pinning, 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. Toolchain setup supports x86_64 Linux with glibc or musl and selects a separately checksummed Temurin JDK for each. The host prerequisites are a POSIX shell, coreutils, make, curl, python3, tar, git, a C compiler for the Go race detector, and the commands checked by ci/test.sh. Alpine also needs bash for the NDK compiler launchers and gcompat for the NDK host binaries. The CI file gives exact Debian and Alpine package lists. Toolchain archives, the SDK platform revision, and rsync source are version- and checksum-pinned by the build; see versions. 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 # tests, race detector, lint, vulnerability scan make verify-repro # clean commit, two paths, byte-identical APKs `make test` also requires host rsync and lets govulncheck query the public Go vulnerability database. No source code is uploaded. The APK lands under app/build/outputs/apk/. It is unsigned by default and cannot be installed until signed. .gitlab-ci.yml runs exactly these scripts and holds no build logic of its own: ci/setup-toolchain.sh, ci/test.sh, ci/build.sh, and, on a tag, ci/verify-repro.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 <"). Transfers are counted rather than listed one line each: on a first sync of a large library the filenames would push everything worth reading out of the capped log. The log is capped at 512 KB and rolls over to sync.log.1. The viewer reads at most the newest 64 KB across both files so layout cost stays bounded. Control characters are escaped before writing so remote output cannot forge terminal log lines. - If config.json cannot be read or parsed, rsend atomically renames it to a unique config.json.broken file, starts from an empty configuration, and shows the preserved filename in the app. It never deletes unreadable config bytes. - rsh transport: RSH_KEY, RSH_KNOWN_HOSTS, and RSH_PORT select the key, known_hosts file, and port; RSH_KEY_DATA passes the key itself, which is what the app uses so the plaintext key never reaches the filesystem. Run rsh by hand to isolate SSH from rsync. - During transport, rsh offers only the host-key type already pinned for that remote. Test connection prefers the pinned type so an untouched server reproduces the same key, but can show a newly rotated type for explicit approval. Re-run Test connection after deliberately rotating a host key. - An IPv6 host goes in the host field as a bare literal (2001:db8::1); rsend adds the brackets where rsh and rsync each need them. A bracketed literal is accepted too. - "foreground service refused" in the log means the sync ran as a plain background job, which the system may stop early. Grant Battery > unrestricted; rsync resumes from .rsend-partial on the next run either way. - A host that drops packets instead of refusing them, a firewall closed to the phone's current network for instance, stalls the connect for the full budget. That budget is 10 seconds; RSH_CONNECT_TIMEOUT overrides it in whole seconds. The log line reads "rsh: unreachable:", and the remaining folders bound to that remote are skipped for the rest of the run rather than each paying the timeout again. License ------- Copyright (C) 2026 Lena. GPLv3; see LICENSE. Bundling rsync makes the whole app GPLv3. THIRD_PARTY records every native and JVM component shipped in the APK, its license, and its corresponding source. Native source and tool archives are pinned and checksummed; JVM dependencies use exact Maven versions. Test and build-only dependencies are listed separately because they are not part of the APK. LICENSE and THIRD_PARTY are packaged as plain APK assets, along with APACHE-2.0.