<feed xmlns='http://www.w3.org/2005/Atom'>
<title>android/rsend/app/src/test/java/invalid/lena, branch 0.2.1</title>
<subtitle>[no description]</subtitle>
<link rel='alternate' type='text/html' href='http://git.0xlena.dev/android/rsend/'/>
<entry>
<title>app: remove duplicated and dead code</title>
<updated>2026-08-23T00:00:00+00:00</updated>
<author>
<name>Lena</name>
<email>lena@omega</email>
</author>
<published>2026-08-23T00:00:00+00:00</published>
<link rel='alternate' type='text/html' href='http://git.0xlena.dev/android/rsend/commit/?id=0c653418a67fe960bb3126559fe77833faaa385c'/>
<id>0c653418a67fe960bb3126559fe77833faaa385c</id>
<content type='text'>
upgradeLegacy assembled every remote, folder and schedule twice: once as
Remote and Folder objects to validate, and again as a parallel JSON tree
to return. Build the objects and serialise them, and drop the dead
endpointAllowed branch, the redundant pin emptiness test, and the
excludes length pre-check that excludesAllowed already makes.

atomicWrite existed verbatim in Config and Keys and was open-coded a
third time in LastSync, which then reached into Config's companion for
the bounded reader. The same bounded read was open-coded twice more, for
the encrypted key and for an imported one. Both are plain file
operations on the same private directory and belong in one place, with
readText a thin wrapper over readBytes.

Generating and importing an identity key ran the same twenty lines of
worker thread, error capture, refresh, toast and dialog; only the action
and two strings differed. rotateIfBig rotated at length &gt;= MAX_BYTES,
which line() already subsumes, and its only caller invoked it
immediately before a line(). notifyError and notifyDeleted were the same
builder twice. openSettings nested a try/catch and repeated one toast;
take the candidates as a vararg. RemoteActivity.save defaulted its
argument to current(), which validates and writes the status field, so
the default hid that side effect from one of two callers.

Dead on arrival: Outcome.deleted's default, which no caller omits; the
rsa-sha2-* arms of hostKeyFile, since rsh prints PublicKey.Type() and an
RSA host key is always ssh-rsa; and the limit &lt;= 0 branch in
SyncLog.text, which tail already handles and handled more accurately.
The config recovery note is now written with an explicit charset like
every other write here.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
upgradeLegacy assembled every remote, folder and schedule twice: once as
Remote and Folder objects to validate, and again as a parallel JSON tree
to return. Build the objects and serialise them, and drop the dead
endpointAllowed branch, the redundant pin emptiness test, and the
excludes length pre-check that excludesAllowed already makes.

atomicWrite existed verbatim in Config and Keys and was open-coded a
third time in LastSync, which then reached into Config's companion for
the bounded reader. The same bounded read was open-coded twice more, for
the encrypted key and for an imported one. Both are plain file
operations on the same private directory and belong in one place, with
readText a thin wrapper over readBytes.

Generating and importing an identity key ran the same twenty lines of
worker thread, error capture, refresh, toast and dialog; only the action
and two strings differed. rotateIfBig rotated at length &gt;= MAX_BYTES,
which line() already subsumes, and its only caller invoked it
immediately before a line(). notifyError and notifyDeleted were the same
builder twice. openSettings nested a try/catch and repeated one toast;
take the candidates as a vararg. RemoteActivity.save defaulted its
argument to current(), which validates and writes the status field, so
the default hid that side effect from one of two callers.

Dead on arrival: Outcome.deleted's default, which no caller omits; the
rsa-sha2-* arms of hostKeyFile, since rsh prints PublicKey.Type() and an
RSA host key is always ssh-rsa; and the limit &lt;= 0 branch in
SyncLog.text, which tail already handles and handled more accurately.
The config recovery note is now written with an explicit charset like
every other write here.
</pre>
</div>
</content>
</entry>
<entry>
<title>app: rename RsyncRunner to Rsync</title>
<updated>2026-08-23T00:00:00+00:00</updated>
<author>
<name>Lena</name>
<email>lena@omega</email>
</author>
<published>2026-08-23T00:00:00+00:00</published>
<link rel='alternate' type='text/html' href='http://git.0xlena.dev/android/rsend/commit/?id=dc0338f8c0c9e74b277169d812c432f7ea4888e3'/>
<id>dc0338f8c0c9e74b277169d812c432f7ea4888e3</id>
<content type='text'>
Runner names the architecture rather than the thing. The object is the
rsync invocation.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Runner names the architecture rather than the thing. The object is the
rsync invocation.
</pre>
</div>
</content>
</entry>
<entry>
<title>app: report config limits instead of crashing</title>
<updated>2026-08-23T00:00:00+00:00</updated>
<author>
<name>Lena</name>
<email>lena@omega</email>
</author>
<published>2026-08-23T00:00:00+00:00</published>
<link rel='alternate' type='text/html' href='http://git.0xlena.dev/android/rsend/commit/?id=e8c6c4ebb3d76ee280043bb66cf499584c591310'/>
<id>e8c6c4ebb3d76ee280043bb66cf499584c591310</id>
<content type='text'>
The remote and folder counts and the serialized config size are enforced
only inside save, which throws. Every other rule an editor can break is
reported in the form, so a config that outgrew a limit crashed the
activity instead of saying so. overLimit answers before the entry is
added, and the editors report the answer.

hostKeyAllowed reconstructs the known_hosts address that rsh writes with
knownhosts.Normalize, and nothing recorded or tested that coupling.
Normalize leaves the host bare on port 22 for IPv6 literals too, so
bracketing them here looks right and would reject every IPv6 pin.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
The remote and folder counts and the serialized config size are enforced
only inside save, which throws. Every other rule an editor can break is
reported in the form, so a config that outgrew a limit crashed the
activity instead of saying so. overLimit answers before the entry is
added, and the editors report the answer.

hostKeyAllowed reconstructs the known_hosts address that rsh writes with
knownhosts.Normalize, and nothing recorded or tested that coupling.
Normalize leaves the host bare on port 22 for IPv6 literals too, so
bracketing them here looks right and would reject every IPv6 pin.
</pre>
</div>
</content>
</entry>
<entry>
<title>app: harden backup execution</title>
<updated>2026-08-16T00:00:00+00:00</updated>
<author>
<name>Lena</name>
<email>lena@omega</email>
</author>
<published>2026-08-16T00:00:00+00:00</published>
<link rel='alternate' type='text/html' href='http://git.0xlena.dev/android/rsend/commit/?id=0e8a87f2ad2bd40d34996374aebb6dff070e4b5f'/>
<id>0e8a87f2ad2bd40d34996374aebb6dff070e4b5f</id>
<content type='text'>
Validate persisted state and destination boundaries, make interruption
and mirror deletion explicit, and keep scheduled work singular.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Validate persisted state and destination boundaries, make interruption
and mirror deletion explicit, and keep scheduled work singular.
</pre>
</div>
</content>
</entry>
<entry>
<title>app: support multiple named remotes</title>
<updated>2026-08-16T00:00:00+00:00</updated>
<author>
<name>Lena</name>
<email>lena@omega</email>
</author>
<published>2026-08-16T00:00:00+00:00</published>
<link rel='alternate' type='text/html' href='http://git.0xlena.dev/android/rsend/commit/?id=0890d139e5fa501561e469aca7854791a936fcb6'/>
<id>0890d139e5fa501561e469aca7854791a936fcb6</id>
<content type='text'>
Let each folder select a named SSH endpoint and migrate the published
0.1.x configuration without discarding its folders or host pin.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Let each folder select a named SSH endpoint and migrate the published
0.1.x configuration without discarding its folders or host pin.
</pre>
</div>
</content>
</entry>
<entry>
<title>app: harden sync execution and persisted state</title>
<updated>2026-07-13T22:00:29+00:00</updated>
<author>
<name>Lena</name>
<email>lena@omega</email>
</author>
<published>2026-07-01T00:00:00+00:00</published>
<link rel='alternate' type='text/html' href='http://git.0xlena.dev/android/rsend/commit/?id=2593151ca2c67256442ad25b074504e3ecd3371f'/>
<id>2593151ca2c67256442ad25b074504e3ecd3371f</id>
<content type='text'>
Persist config, key blob, and host pin via AtomicFile so an
interrupted write cannot corrupt state. Validate the port, clear the
host pin when the remote changes, cap imported key size, time out
native helpers, and escalate rsync termination to destroyForcibly.
Serialize manual and scheduled syncs behind a mutex.

Replace the chained one-time jobs with plain periodic work: it
survives reboots without a boot receiver and cannot silently die
like a broken chain. Costs the sub-15-minute interval, which photo
backup does not need. Document the floor and that syncs run on any
unmetered network, not only WiFi.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Persist config, key blob, and host pin via AtomicFile so an
interrupted write cannot corrupt state. Validate the port, clear the
host pin when the remote changes, cap imported key size, time out
native helpers, and escalate rsync termination to destroyForcibly.
Serialize manual and scheduled syncs behind a mutex.

Replace the chained one-time jobs with plain periodic work: it
survives reboots without a boot receiver and cannot silently die
like a broken chain. Costs the sub-15-minute interval, which photo
backup does not need. Document the floor and that syncs run on any
unmetered network, not only WiFi.
</pre>
</div>
</content>
</entry>
<entry>
<title>app: drop --mkpath for compatibility with old remote rsyncs</title>
<updated>2026-07-01T00:00:00+00:00</updated>
<author>
<name>Lena</name>
<email>lena@omega</email>
</author>
<published>2026-07-01T00:00:00+00:00</published>
<link rel='alternate' type='text/html' href='http://git.0xlena.dev/android/rsend/commit/?id=44ebf6db6cf2725e18f0e1129f4005fd456b03dc'/>
<id>44ebf6db6cf2725e18f0e1129f4005fd456b03dc</id>
<content type='text'>
rsync forwards --mkpath to the remote side when sending, so a remote
rsync older than 3.2.3 (notably stock macOS, whose openrsync is
"rsync 2.6.9 compatible") rejects every sync with "unrecognized
option '--mkpath'".

rsync creates the final component of the destination path on its own,
so flat remote paths keep working everywhere, including the documented
rrsync setup whose root is pre-created. Only a nested remote path with
missing parents now needs a one-time mkdir -p on the server; the log
shows rsync's error when it is missing.

Reported-by: jsvk
Link: https://codeberg.org/0xlena/rsend/pulls/1
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
rsync forwards --mkpath to the remote side when sending, so a remote
rsync older than 3.2.3 (notably stock macOS, whose openrsync is
"rsync 2.6.9 compatible") rejects every sync with "unrecognized
option '--mkpath'".

rsync creates the final component of the destination path on its own,
so flat remote paths keep working everywhere, including the documented
rrsync setup whose root is pre-created. Only a nested remote path with
missing parents now needs a one-time mkdir -p on the server; the log
shows rsync's error when it is missing.

Reported-by: jsvk
Link: https://codeberg.org/0xlena/rsend/pulls/1
</pre>
</div>
</content>
</entry>
<entry>
<title>rsend: push phone folders to a home SSH host over rsync</title>
<updated>2026-01-01T00:00:00+00:00</updated>
<author>
<name>Lena</name>
<email>lena@omega</email>
</author>
<published>2026-01-01T00:00:00+00:00</published>
<link rel='alternate' type='text/html' href='http://git.0xlena.dev/android/rsend/commit/?id=7e04941bccb2683f8a6e3ee38a99c50129234dd1'/>
<id>7e04941bccb2683f8a6e3ee38a99c50129234dd1</id>
<content type='text'>
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.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
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.
</pre>
</div>
</content>
</entry>
</feed>
