Backing up my Android photos with rsync

To back up my Android photos, I would manually sync them via USB to my local server. Needless to say, I forget to do this, so it’s time for a better solution!

Requirements

Here’s the rough set of things I was looking for:

Out-of-scope:

Design

tl;dr Run rsync on Termux to backup photos via SSH that only permits rsync commands using rrsync.

rsync is a simple reliable way to sync files from one computer to another. I use this to sync photos from my phone to my server.

For this, I need an rsync client, rsync on the server, and some sort of monitoring.

The client: rsync in Termux

  1. Install termux. I use scrcpy to control my phone from my desktop.
  2. Install termux-api, to expose Android APIs to your Termux shell.
  3. Run termux-setup-storage to mount the Camera photos in your Termux shell.
  4. Create an SSH key and provide the SSH key to the server (below).
  5. Schedule rsync every 2 hours using termux-job-scheduler. termux-job-scheduler uses the Android job scheduling framework, is battery-efficient and runs across restarts without background access.
$ PERIOD_MS=$((2 * 60 * 60 * 1000))
$ termux-job-scheduler \
    --script ~/.shortcuts/rsync-photos.sh \
    --period-ms "$PERIOD_MS" \
    --network unmetered \
    --battery-not-low false \
    --charging true \
    --persisted true

~/.shortcuts/rsync-photos.sh

#!/bin/sh
rsync -Pav -e "ssh -i $HOME/.ssh/id_ed25519.photo" storage/dcim/Camera/ photo@$SERVER:
RESULT="$?"
if [ "$RESULT" -eq 0 ];
then
    curl --retry 3 "https://hc-ping.com/$HEALTH_CHECK_ID"
fi

To catch script failures, the script reports reports successes to https://healthchecks.io. If healthchecks.io doesn’t receive a success within 2 days, I’ll receive an alert via email. This allows the script to fail a few times without it bothering me. I don’t mind occasional failures as long as the script manages to occasionally succeed.

The server: restricted SSH

My low-power server already has SSH exposed via Tailscale, and rsync can run over SSH.

To avoid giving the client undue access to the server, the client can be restricted to just rsync using rsync’s rrsync command. I use the -wo flag to prevent the client from reading from the server.

To prevent ransomware attacks, I use the -no-overwrite flag.

To prevent escalation attacks (e.g. if a client was able to write to ~/.ssh), I restrict writes to a subdirectory of ~. I’d like a more robust solution to this.

~photo/.ssh/authorized_keys

command="rrsync -wo -no-overwrite /home/photo/dir/",restrict ssh-ed25519 AAAAC3NzaCetcetera/etceteraJZMfk3QPfQ

For NixOS, this is documented at the NixOS wiki.

Risks

Alternatives considered

syncthing

Syncthing is great, but it has complexity beyond my needs (ACL model, relay model, two-way sync), and I’ve had sync/battery problems with it in the past.

An rsync Android app

syncopoli is an Rsync Android app, but it lacks two crucial features: per-profile settings, and private key handling.

ente

ente is an open source end-to-end encrypted photo service. It lacks incremental exports, and has no practical solution for Linux Mobile.

Troubleshooting

If this breaks, I’ll be visiting this page for help, so here goes: