Files
MealQuest/deploy/systemd/mealquest-media-backup.sh
T
dev_agentandClaude Opus 5.5 765740b921
CI / ci (push) Successful in 1m35s
CI / ci (pull_request) Successful in 1m34s
Run the media backup as uid 10001 and add the deploy runbook
Address review of 9ac3342:

- The media backup now runs as the host user mealquest (10001) with no
  capabilities. systemd bind-mounts the data volume read-only at
  /run/mealquest-data and passes the NAS key as a credential; the script
  changes into media, refuses it if the physical path differs (symlink)
  and rsyncs "./", so the path cannot be swapped under it. ssh pins the
  NAS host keys from /etc/mealquest/known_hosts (deploy/known_hosts).
- A missing media directory now fails the job. The deploy step creates it.
- deploy/RUNBOOK.md records the Traefik route (task 0c1dc172), backup
  setup, the restore procedure (stop, move db/-wal/-shm aside, restore,
  count check, start) and the 2026-10-05 drill with ltx -level all.

Task: d2f2e87e-5114-4ef1-93ab-360ffb5c009e

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:28:36 -04:00

45 lines
1.9 KiB
Bash
Executable File

#!/bin/sh
# Nightly copy of the MealQuest media directory (recipe and dinner photos)
# to the Synology NAS. The database itself is replicated continuously by
# Litestream; this covers the files beside it.
#
# Runs as the unprivileged app user (10001, host user "mealquest"), never
# root: mealquest-media-backup.service bind-mounts the data volume
# read-only at /run/mealquest-data and hands the NAS key in as a systemd
# credential. Anything the app could plant in the volume is therefore read
# with the app's own permissions only.
#
# The script changes into the media directory and checks that the physical
# path is the expected one, so a media path that is a symlink is refused.
# rsync then copies "./" — the directory the shell is already in — so
# swapping the path afterwards cannot redirect it. Symlinks inside the
# directory are copied as links (-a), never followed.
#
# The media directory is created at deploy time (deploy/RUNBOOK.md). If it
# is missing the job fails; it is never treated as "nothing to back up".
#
# rsync mirrors into .../mealquest-backups/media/ only: the target ends in
# a slash, so --delete removes files from that directory and nothing
# outside it. Re-runs transfer only what changed. Output goes to the
# journal via mealquest-media-backup.service.
set -eu
src=/run/mealquest-data/media
dest=joshua@10.60.1.50:/volume1/homes/Joshua/mealquest-backups/media/
key=$CREDENTIALS_DIRECTORY/nas_key
known_hosts=/etc/mealquest/known_hosts
if ! cd "$src"; then
echo "mealquest-media-backup: media directory $src is missing" >&2
exit 1
fi
if [ "$(pwd -P)" != "$src" ]; then
echo "mealquest-media-backup: $src is a symlink to $(pwd -P); refusing to back it up" >&2
exit 1
fi
rsync -a --delete --stats \
-e "ssh -F none -i $key -o IdentitiesOnly=yes -o BatchMode=yes -o UserKnownHostsFile=$known_hosts -o GlobalKnownHostsFile=/dev/null -o StrictHostKeyChecking=yes" \
./ "$dest"
echo "mealquest-media-backup: synced $src to $dest as uid $(id -u)"