Files
librenotes/scripts/backup.sh
T
libretech 9eefece320 feat(backup): add rsync --link-dest off-host transport, restore docs
backup.sh now supports BACKUP_REMOTE_RSYNC alongside BACKUP_REMOTE.
The rsync path writes <root>/YYYY-MM-DD/<archive> on the target with
--link-dest pointing at the previous day's directory, so unchanged
archives become hard links and daily snapshots cost almost zero
extra bytes. BACKUP_REMOTE_SSH_KEY routes the rsync ssh leg to a
dedicated identity (e.g. rsync.net restricted accounts).

Timer moved to 03:00 Europe/Berlin (was 03:17 UTC) per #41.

docs/operations.md: full restore procedure (parallel stack first,
then atomic swap) plus the rsync vs rclone trade-off. Closes most
of #41 — the only remaining task is the operator's choice of
off-host target.
2026-04-29 14:47:42 +02:00

88 lines
3.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# backup.sh — daily backup of librenotes state.
#
# Produces $BACKUP_DIR/librenotes-YYYYMMDD-HHMMSS.tar.gz containing:
# - librenotes.db (consistent SQLite .backup snapshot)
# - notes.tar.gz (per-tenant note files)
# - info.txt (timestamp, hostname, version)
#
# Required env:
# LIBRENOTES_DB path to the SQLite database
# LIBRENOTES_DATA_DIR path to the per-tenant note directory
#
# Optional env:
# BACKUP_DIR where to write archives (default /var/backups/librenotes)
# BACKUP_REMOTE rclone target for off-site copy (e.g. s3:bucket/path)
# BACKUP_REMOTE_RSYNC rsync destination for hard-link-deduplicated
# off-host copy, e.g. backup@rsync.net:librenotes/
# Layout written at the target:
# <root>/YYYY-MM-DD/librenotes-<ts>.tar.gz
# Each run passes --link-dest=../<previous>/ so
# unchanged archives cost zero extra bytes.
# BACKUP_REMOTE_SSH_KEY path to a private SSH key used for the rsync
# leg (passed via -e "ssh -i <key>").
# BACKUP_VERSION version string written into info.txt
#
# The script is intentionally a single self-contained file so it
# can run on a minimal host with only sqlite3, tar, gzip, rsync, and
# (optionally) rclone.
set -euo pipefail
: "${LIBRENOTES_DB:?LIBRENOTES_DB is required}"
: "${LIBRENOTES_DATA_DIR:?LIBRENOTES_DATA_DIR is required}"
BACKUP_DIR="${BACKUP_DIR:-/var/backups/librenotes}"
BACKUP_VERSION="${BACKUP_VERSION:-unknown}"
mkdir -p "$BACKUP_DIR"
ts="$(date -u +%Y%m%d-%H%M%S)"
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
# Online SQLite snapshot. .backup is atomic from the application's
# perspective even while writes are happening.
sqlite3 "$LIBRENOTES_DB" ".backup '$work/librenotes.db'"
# Notes archive. Use --warning=no-file-changed because per-user
# files may be touched concurrently; we still get a consistent
# point-in-time view per file thanks to tar's read semantics.
tar --warning=no-file-changed -C "$LIBRENOTES_DATA_DIR" -czf "$work/notes.tar.gz" .
cat > "$work/info.txt" <<EOF
backup_at: $ts UTC
host: $(hostname)
version: $BACKUP_VERSION
db_size: $(stat -c%s "$work/librenotes.db" 2>/dev/null || stat -f%z "$work/librenotes.db")
notes_size:$(stat -c%s "$work/notes.tar.gz" 2>/dev/null || stat -f%z "$work/notes.tar.gz")
EOF
archive="$BACKUP_DIR/librenotes-$ts.tar.gz"
tar -C "$work" -czf "$archive" librenotes.db notes.tar.gz info.txt
echo "wrote $archive ($(stat -c%s "$archive" 2>/dev/null || stat -f%z "$archive") bytes)"
if [ -n "${BACKUP_REMOTE:-}" ]; then
rclone copy "$archive" "$BACKUP_REMOTE" --quiet
echo "uploaded to $BACKUP_REMOTE"
fi
if [ -n "${BACKUP_REMOTE_RSYNC:-}" ]; then
# Compute today / previous-day directory names. Layout at the
# remote target is <root>/YYYY-MM-DD/. We pass --link-dest pointing
# at yesterday's dir so unchanged archives become hard links —
# ~free for daily snapshots that are mostly identical.
today="$(date -u +%Y-%m-%d)"
yesterday="$(date -u -d 'yesterday' +%Y-%m-%d 2>/dev/null \
|| date -u -v-1d +%Y-%m-%d)"
ssh_opts=()
if [ -n "${BACKUP_REMOTE_SSH_KEY:-}" ]; then
ssh_opts=(-e "ssh -i $BACKUP_REMOTE_SSH_KEY -o StrictHostKeyChecking=accept-new")
fi
# Strip any trailing slash so we control the join.
remote="${BACKUP_REMOTE_RSYNC%/}"
rsync -a --link-dest="../$yesterday/" "${ssh_opts[@]}" \
"$archive" "$remote/$today/"
echo "rsync'd $archive -> $remote/$today/"
fi