Files
librenotes/scripts/backup-prune.sh
T
libretechandClaude Opus 4.7 bcccba92f7 Add deploy workflow and backup tooling
CI deployment (.gitea/workflows/deploy.yml):
- Two jobs (build, deploy) gated on the repo variable
  DEPLOY_ENABLED=true so the workflow exists but does nothing
  until secrets and host are configured.
- Build pushes two image tags per run: rolling :main + the short
  SHA on main, or vX.Y.Z + :latest on tag pushes. Immutable per
  commit/tag tags make rollback trivial.
- Deploy SSHes to DEPLOY_HOST, runs docker compose pull && up -d
  in DEPLOY_PATH, then polls HEALTH_URL for up to a minute. A
  failed health check fails the workflow, which is the alert.
- Required secrets and the rollback procedure are documented in
  docs/operations.md.

Backup tooling (scripts/):
- backup.sh: SQLite online .backup snapshot + tarball of the
  per-tenant data dir + info.txt header, all wrapped into a
  single librenotes-YYYYMMDD-HHMMSS.tar.gz. Optional BACKUP_REMOTE
  triggers an rclone copy for off-site storage.
- backup-prune.sh: enforces retention "30 daily + 12 monthly".
  Sorts archives by filename (date is in the name so lex order
  matches chronological) and keeps the newest 30 plus the newest
  archive for each of the most recent 12 months.
- backup-restore-test.sh: extracts the most recent archive into
  a tmpdir, runs sqlite3 .schema (proves DB readability), and
  asserts the notes tar has at least one entry. Failure is the
  alert. Wired into a separate weekly timer.
- librenotes-backup.{service,timer}: systemd units for the daily
  03:17 UTC run with 5min jitter; ProtectSystem=strict, only
  /var/backups/librenotes is writable.
- librenotes-backup-verify.{service,timer}: weekly Monday
  04:00 UTC restore test.

Closes #26 and #27.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:49:40 +02:00

58 lines
1.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# backup-prune.sh — enforce retention "keep 30 daily + 12 monthly".
#
# Walks $BACKUP_DIR for librenotes-YYYYMMDD-HHMMSS.tar.gz and
# deletes archives outside the retention policy.
# - keep the most recent N daily archives (default 30)
# - additionally keep the most recent archive of each of the
# last M distinct months (default 12)
# Anything else is removed.
set -euo pipefail
BACKUP_DIR="${BACKUP_DIR:-/var/backups/librenotes}"
DAILY_KEEP="${DAILY_KEEP:-30}"
MONTHLY_KEEP="${MONTHLY_KEEP:-12}"
if [ ! -d "$BACKUP_DIR" ]; then
echo "backup dir $BACKUP_DIR does not exist; nothing to prune"
exit 0
fi
cd "$BACKUP_DIR"
# Sort archives by name (date is in the filename, lexicographic
# order == chronological order).
mapfile -t archives < <(ls -1 librenotes-*.tar.gz 2>/dev/null | sort)
declare -A keep
# Keep the newest DAILY_KEEP outright.
for a in "${archives[@]: -$DAILY_KEEP}"; do
keep["$a"]=1
done
# Walk archives newest-first, recording one per month until we
# have MONTHLY_KEEP distinct months.
declare -A month_seen
months_kept=0
for ((i=${#archives[@]}-1; i>=0; i--)); do
a="${archives[$i]}"
# filename: librenotes-YYYYMMDD-HHMMSS.tar.gz -> YYYYMM
ym="${a:11:6}"
if [ -z "${month_seen[$ym]:-}" ] && [ "$months_kept" -lt "$MONTHLY_KEEP" ]; then
keep["$a"]=1
month_seen[$ym]=1
months_kept=$((months_kept + 1))
fi
done
removed=0
for a in "${archives[@]}"; do
if [ -z "${keep[$a]:-}" ]; then
rm -f "$a"
removed=$((removed + 1))
fi
done
echo "kept ${#keep[@]} archives, removed $removed"