CI / ci (pull_request) Failing after 5m37s
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.
88 lines
3.5 KiB
Bash
Executable File
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
|