aboutsummaryrefslogtreecommitdiff
path: root/README
diff options
context:
space:
mode:
authorLena <lena@omega>2026-08-16 00:00:00 +0000
committerLena <lena@omega>2026-08-16 00:00:00 +0000
commit6578dc47a7d1f7946528e9540d9cfa2e9a5d88d2 (patch)
tree0d5867861e24f31166716fd414d879528c2c7cff /README
parent8c6390571d28ff21a98ed3802458aceddbb6d2a0 (diff)
downloadrsend-0.2.0.tar.gz
app: release 0.2.00.2.0
Document the multi-remote release, publish matching store metadata, and advance the version once from the released 0.1.3 base.
Diffstat (limited to 'README')
-rw-r--r--README195
1 files changed, 154 insertions, 41 deletions
diff --git a/README b/README
index 2f321c9..f9722c9 100644
--- a/README
+++ b/README
@@ -1,21 +1,44 @@
rsend
=====
-Push configured phone folders to a home SSH host using real rsync.
+Push configured phone folders to home SSH hosts 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.
+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 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.
+- 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
@@ -25,26 +48,34 @@ Layout
- 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.
+- THIRD_PARTY complete shipped-component inventory and license notices.
+- APACHE-2.0 license terms for the shipped Apache-licensed components.
- 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).
+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 # Go, real-rsync integration, and Kotlin tests
- make verify-repro # build twice, compare the unsigned APKs
+ make test # tests, race detector, lint, vulnerability scan
+ make verify-repro # clean commit, two paths, byte-identical APKs
-`make test` also requires host rsync. The APK lands under
+`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. Continuous integration runs the same scripts: ci/setup-toolchain.sh,
-ci/build.sh, and ci/test.sh.
+signed. .gitlab-ci.yml runs exactly those scripts and holds no build logic of
+its own: ci/setup-toolchain.sh, ci/test.sh, and ci/build.sh.
For a locally installable APK, create a keystore and a gitignored
keystore.properties before building:
@@ -63,32 +94,51 @@ app/build/outputs/apk/release/app-release.apk.
Run
---
-1. Install the APK and grant all-files access and notifications.
+1. Install the APK and grant all-files access and notifications. All-files
+ access is required: a sync refuses to start without it because configured
+ storage may otherwise be unreadable.
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.
+ ~/.ssh/authorized_keys. Import accepts unencrypted Ed25519 private keys only.
+3. Add a remote (name, host, user, port); run Test connection and accept the
+ pinned host-key fingerprint. The dialog shows the key type next to the
+ fingerprint; verify it against the matching key file on the server. Nothing
+ is saved until you accept a key, so a failed test cannot disturb a remote
+ that already works. Repeat for every host you push to.
+4. Add folders, pick each folder's remote and deletion policy, set the
+ schedule. Enabling mirror shows its exact deletion semantics before saving.
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.
+battery optimization. Phones with aggressive power management otherwise delay
+or skip background jobs. Android 16 also counts long WorkManager foreground
+runs against the app's job quota; a very large first backup may need several
+runs and resumes from .rsend-partial each time.
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:
+Any rsync-compatible SSH service works, including ordinary shell accounts and
+older rsync releases such as stock macOS. rsend pushes over rsync-over-SSH, not
+SFTP. Remote paths may be absolute, relative, or home-relative, but mappings on
+one endpoint must use either absolute or relative paths consistently. A path
+must name a directory below root or home and cannot contain dot or parent
+components. rsync creates the final component on its own; create deeper missing
+parent directories once with mkdir -p on the server. A remote path cannot begin
+with a colon: rsync interprets host::path as its unencrypted daemon protocol
+instead of SSH.
+
+Protocol compatibility is not a security promise. This release bundles rsync
+3.5.0, but it cannot update the rsync receiver or rrsync wrapper on the server.
+Install upstream 3.5.0 or a vendor package carrying the August 2026 security
+fixes on the server before relying on rrsync confinement.
+
+The recommended deployment confines the account to "rsync into one folder and
+nothing else" with a forced rrsync command. This is not a protocol requirement.
+sshd runs the forced command through the account's login shell, so the shell
+must be real: /bin/sh works, while /bin/false or /sbin/nologin breaks 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
@@ -124,17 +174,80 @@ 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.
+ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub on the server. The Test
+connection dialog prints the key type it pinned; if it says something other
+than ssh-ed25519, the server has no ed25519 host key and you should compare
+against that type's .pub file instead.
+
+Interrupted transfers leave a .rsend-partial directory inside the destination
+folder. rsync reuses it to resume and excludes it from the transfer, so it is
+never deleted by a mirror. Abandoned partials from a sync that never resumed
+can be removed by hand.
+
+
+Upgrading
+---------
+A configuration written by 0.1.x is read once and rewritten in the current
+shape on first launch. The single remote becomes the sole entry, named after
+its host; every safe folder points at it; schedule intervals below 15 minutes
+are raised to WorkManager's minimum; and the pin 0.1.x kept in its own
+known_hosts file is carried over. A pin that no longer describes that host and
+port is dropped rather than failing the upgrade, which costs one Test
+connection instead of the whole configuration. The exact old JSON is preserved
+as config.json.0.1. A folder or endpoint that violates current safety rules is
+omitted, preserved in that file, and reported in the app instead of causing the
+rest of the configuration to be discarded.
+
+This is a deliberate, time-limited exception to the project's rule against
+compatibility shims, kept because 0.1.3 is the last published release and is
+therefore what every upgrading install has. It will be removed in 0.4.0: a
+0.1.x configuration not opened by then is read as unparsable, preserved
+alongside as config.json.broken, and the app starts empty.
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.
+- The in-app log viewer displays the plain-text rsync log; tap Refresh to
+ reload.
+- The log records the rsync arguments, the errors, and every file a mirror
+ deleted on the server ("del. <path>"). 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. Run rsh by hand to isolate SSH from rsync.
+ 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
-------
-GPLv3. Bundling rsync makes the whole app GPLv3; see 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.