From 86c7731d0c2e93e3a13bc007734d8bdd0d84d242 Mon Sep 17 00:00:00 2001 From: Kevin Bataille Date: Sat, 25 Jul 2026 21:16:30 +0200 Subject: [PATCH] Ignore files, remove old and add CLAUDE.md --- .gitignore | 2 + CLAUDE.md | 42 ++++++++ codedb.snapshot | Bin 153813 -> 0 bytes old/borg.sh | 145 -------------------------- old/maria.sh | 36 ------- tests/lib/setup_mocks.sh | 110 -------------------- tests/test_restore.sh | 218 --------------------------------------- 7 files changed, 44 insertions(+), 509 deletions(-) create mode 100644 .gitignore create mode 100644 CLAUDE.md delete mode 100644 codedb.snapshot delete mode 100644 old/borg.sh delete mode 100644 old/maria.sh delete mode 100644 tests/lib/setup_mocks.sh delete mode 100755 tests/test_restore.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5436939 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +CLAUDE.local.md +codedb.snapshot diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9489f93 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,42 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +Backup + disaster-recovery tooling for a Linux server: Borg (encrypted, offsite-synced via rclone) backing up `/home/srv/files/content`, including MariaDB running in Docker. Plain bash, no build system, no CI. `RUNBOOK.md` is the operational doc (setup, deploy, cron, day-2 ops, recovery). `old/` holds the legacy scripts this replaced — reference only, not maintained. + +- `borg-backup.sh` — daily backup orchestrator (cron). Never stops MariaDB: `dump_db.sh`'s `--single-transaction` dump is consistent on its own, and the raw data directory is excluded from the archive via a `.nobackup` marker file. +- `dump_db.sh` — per-database `mysqldump`/`mariadb-dump` with atomic staging/swap. Deployed to `/home/srv/files/content/mariadb/dump_db.sh` (borg-backup.sh invokes it at that exact path). +- `restore.sh` — recovery CLI: `full` (disaster recovery), `db `, `file `, `--list-archives`, all supporting `--dry-run`. + +## Global conventions (don't re-derive these) + +- `TARGET=/home/srv/files/content`, `REPO=/home/srv/files/backups/borg-2025`, `DB_CONTAINER=mariadb` +- `BORG_PASSPHRASE_FILE=/root/.borg-passphrase` (chmod 600, read via `BORG_PASSCOMMAND`) +- Backup DB user `backup` (password file `/root/.mariadb-backup.pw`, limited SELECT/LOCK grants) is separate from restore's `root` creds (`MYSQL_ROOT_PASSWORD` env or `/root/.mariadb-root.pw`) — restore needs CREATE/DROP privileges the backup user doesn't have. +- Dumps live at `mariadb/dump/*.sql` plus `00-users-and-grants.sql`, inside `$TARGET`. +- `restore.sh` and `borg-backup.sh` share one lockfile (`/var/lock/borg-backup.lock` via `flock -n 9`) so a restore and the nightly backup can never run concurrently. +- Destructive-op safety: `full` refuses a non-empty `$TARGET` without `--force`; `db` requires typed dbname confirmation unless `--yes`; every mode supports `--dry-run`. +- `resolve_archive()` in `restore.sh` deliberately does *not* filter archives by hostname (unlike backup's prune/list, which does) — disaster recovery may run from a different host than made the backup. +- Logs: `/var/log/borg/{backup,restore}-*.log` (override with `BORG_BACKUP_LOGFILE` / `RESTORE_LOGDIR` for local testing). + +## Testing + +``` +RESTORE_LOGDIR=/tmp/restore-test-logs bash tests/test_restore.sh +``` + +Plain bash, no framework (`assert_eq`/`assert_contains` + a `$FAILURES` counter in `tests/test_restore.sh`). `tests/lib/setup_mocks.sh` builds fake `borg`/`docker`/`mysql`/`mariadb`/`flock` executables into a temp dir. + +Mock isolation requires all three of: +- `PATH="$mockdir:$PATH"` +- `BASH_ENV="$(mock_bash_env "$mockdir")"` — both scripts prepend a hardened `PATH` with system dirs (`/usr/local/bin` etc.) *ahead* of `$PATH`, so a real binary there would win over a PATH-only mock. `BASH_ENV` shell functions take priority regardless of PATH order. +- Env overrides for `LOCKFILE` / `BORG_PASSPHRASE_FILE` / `ROOT_PASSWORD_FILE` pointed at throwaway paths, so `acquire_lock()`/`preflight()` never touch real `/var/lock` or `/root`. + +`flock(1)` (util-linux) doesn't exist on macOS — that's why it's mocked too, not just for isolation. + +## Conventions + +- Conventional commits (`feat:`, `fix:`, `docs:`, `chore:`). Direct commits to `master`, no PR workflow. +- New restore/backup logic should mirror the existing style in `borg-backup.sh`/`restore.sh`: `log()`/`step()`/`die()`/`run_cmd()` helpers, `set -euo pipefail`, config constants up top. diff --git a/codedb.snapshot b/codedb.snapshot deleted file mode 100644 index ec3678d396abdfe78d9a4ccdcaf380a6c3c05241..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 153813 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 CfoW&} diff --git a/old/borg.sh b/old/borg.sh deleted file mode 100644 index 2fb7b02..0000000 --- a/old/borg.sh +++ /dev/null @@ -1,145 +0,0 @@ -#!/bin/bash -# ============================================================================= -# Borg Backup Script - MariaDB Restart Guaranteed -# ============================================================================= - -set -euo pipefail - -# ========================= CONFIGURATION ========================= - -NAME="borg-2025" -REPO="/home/srv/files/backups/$NAME" -TARGET="/home/srv/files/content" - -LOGDIR="/var/log/borg" -LOGFILE="${LOGDIR}/backup-$(date +%Y-%m-%d-%H%M%S).log" - -# Optional but strongly recommended -HEALTHCHECK_URL="https://hc-ping.com/your-uuid-here" - -RCLONE_REMOTE="scaleway" - -# ================================================================= - -mkdir -p "$LOGDIR" -exec > >(tee -a "$LOGFILE") -exec 2>&1 - -echo "=== Backup started at $(date '+%Y-%m-%d %H:%M:%S') ===" -echo "Repository: $REPO" -echo "Target: $TARGET" -echo "Log: $LOGFILE" -echo "-----------------------------------------------------------" - -# ----------------------- Lockfile ----------------------- -LOCKFILE="/var/lock/borg-backup.lock" -MARIADB_STOPPED=false - -cleanup() { - local exit_code=${1:-$?} - - if [ "$MARIADB_STOPPED" = true ]; then - echo "=== Cleanup: Starting MariaDB container ===" - docker start mariadb || echo "WARNING: Failed to start mariadb container" - sleep 3 - echo "MariaDB restart completed." - fi - - rm -f "$LOCKFILE" - - if [ "$exit_code" -eq 0 ]; then - echo "=== Backup completed SUCCESSFULLY at $(date '+%Y-%m-%d %H:%M:%S') ===" - send_healthcheck - else - echo "=== Backup FAILED at $(date '+%Y-%m-%d %H:%M:%S') (exit code $exit_code) ===" - send_healthcheck "fail" - fi - echo "Full log: $LOGFILE" -} - -if [ -e "$LOCKFILE" ]; then - echo "ERROR: Another backup is already running (lockfile exists)" - echo "Remove it manually if stale: $LOCKFILE" - exit 1 -fi - -touch "$LOCKFILE" -trap 'cleanup $?' EXIT - -# ----------------------- Borg & Functions ----------------------- -export BORG_REPO="$REPO" -export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes - -send_healthcheck() { - local status="${1:-}" - if [[ -n "${HEALTHCHECK_URL:-}" ]] && [[ "$HEALTHCHECK_URL" != *"your-uuid-here"* ]]; then - case "$status" in - start) curl -s -m 10 --retry 3 "${HEALTHCHECK_URL}/start" >/dev/null || true ;; - fail) curl -s -m 10 --retry 3 "${HEALTHCHECK_URL}/fail" -d "Backup failed - check $LOGFILE" >/dev/null || true ;; - *) curl -s -m 10 --retry 3 "$HEALTHCHECK_URL" >/dev/null || true ;; - esac - fi -} - -run_cmd() { - echo "[RUN] $*" - "$@" -} - -# ================================================================= - -send_healthcheck "start" - -# 1. MariaDB dump (while running) -echo "=== Step 1: MariaDB dump ===" -DUMP_SCRIPT="${TARGET}/mariadb/dump_db.sh" -if [[ -x "$DUMP_SCRIPT" ]]; then - run_cmd "$DUMP_SCRIPT" -else - echo "WARNING: Dump script not found or not executable: $DUMP_SCRIPT" -fi - -# 2. Stop MariaDB for consistent backup -echo "=== Step 2: Stopping MariaDB container ===" -run_cmd docker stop mariadb -MARIADB_STOPPED=true - -# 3. Create Borg archive -echo "=== Step 3: Creating Borg archive ===" -run_cmd borg create \ - --stats \ - --progress \ - --list \ - --filter=AME \ - --compression zstd,8 \ - --exclude-caches \ - --exclude-if-present .nobackup \ - "::${now:%Y-%m-%dT%H-%M-%S}" \ - "$TARGET" - -# 4. Restart MariaDB (this line is now also in the cleanup trap) -echo "=== Step 4: Starting MariaDB container ===" -run_cmd docker start mariadb -MARIADB_STOPPED=false - -# 5. Offsite sync -echo "=== Step 5: Syncing to Scaleway S3 ===" -run_cmd rclone sync -v \ - --fast-list \ - --transfers=8 \ - --checkers=16 \ - "$REPO" "${RCLONE_REMOTE}:/par-backup-1/$NAME" - -# 6. Prune & Compact -echo "=== Step 6: Pruning and compacting ===" -run_cmd borg prune \ - --list \ - --keep-daily=7 \ - --keep-weekly=4 \ - --keep-monthly=6 \ - --keep-within=7d \ - "$REPO" - -run_cmd borg compact --progress "$REPO" - -# The cleanup trap will run automatically and mark success diff --git a/old/maria.sh b/old/maria.sh deleted file mode 100644 index 40b057c..0000000 --- a/old/maria.sh +++ /dev/null @@ -1,36 +0,0 @@ -root@chaudron:/home/srv/files/content/mariadb# cat dump_db.sh -#!/bin/bash - -# Load configuration from environment variables -MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-MySuperDatabase} -DUMP_DIR=${DUMP_DIR:-dump} -DOCKER_CONTAINER_NAME=${DOCKER_CONTAINER_NAME:-mariadb} - -# Check dependencies -if ! command -v docker &>/dev/null; then - echo "Docker is not installed or not in the PATH" - exit 1 -fi - -if ! docker ps -q -f name=$DOCKER_CONTAINER_NAME; then - echo "Docker container '$DOCKER_CONTAINER_NAME' is not running" - exit 1 -fi - -# Create dump directory if it doesn't exist -mkdir -p "$(dirname "$0")/$DUMP_DIR" - -# Delete old dump files -rm -f "$(dirname "$0")/$DUMP_DIR"/*.sql - -# Get list of databases and exclude system databases -databases=$(docker exec -it $DOCKER_CONTAINER_NAME mysql -u root -p"$MYSQL_ROOT_PASSWORD" -e "SHOW DATABASES;" --skip-column-names -s | grep -Ev "(information_schema|mysql|performance_schema|sys)" | tr -d '\r') - -# Iterate through databases and dump them individually -for db in $databases; do - echo "Dumping database: $db" - docker exec -i $DOCKER_CONTAINER_NAME mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" "$db" >"$(dirname "$0")/$DUMP_DIR/$db.sql" - echo "Dumped database: $db" -done - -echo "All non-system databases dumped to individual files in '$DUMP_DIR' directory." diff --git a/tests/lib/setup_mocks.sh b/tests/lib/setup_mocks.sh deleted file mode 100644 index a6f570b..0000000 --- a/tests/lib/setup_mocks.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/bin/bash -# Builds fake borg/docker/mysql/mariadb executables into $1 so restore.sh -# can be exercised without real infrastructure. Each mock logs its -# invocation to $1/mock.log. - -setup_mock_bin() { - local dir="$1" - mkdir -p "$dir" - local mocklog="$dir/mock.log" - : > "$mocklog" - - cat > "$dir/borg" <> "$mocklog" -case "\$1" in - list) - echo "host-2026-01-01T00-00-00" - echo "host-2026-06-01T00-00-00" - ;; - extract) - shift - paths=() - for a in "\$@"; do - case "\$a" in - -*) ;; - *::*) ;; - *) paths+=("\$a") ;; - esac - done - for p in "\${paths[@]}"; do - if [[ "\$p" == *.* ]]; then - mkdir -p "\$(dirname "\$p")" - printf 'mock-content\n' > "\$p" - else - mkdir -p "\$p" - touch "\$p/RESTORED_MARKER" - fi - done - ;; - *) ;; -esac -exit 0 -EOF - - cat > "$dir/docker" <> "$mocklog" -case "\$1" in - inspect) - if [[ "\$*" == *"State.Running"* ]]; then - echo true - else - echo healthy - fi - ;; - start|stop) - exit 0 - ;; - exec) - case "\$*" in - *"command -v mariadb"*) exit 0 ;; - *) cat >/dev/null 2>&1 || true; exit 0 ;; - esac - ;; - *) ;; -esac -exit 0 -EOF - - cat > "$dir/mysql" <> "$mocklog" -cat >/dev/null 2>&1 || true -exit 0 -EOF - cp "$dir/mysql" "$dir/mariadb" - - # flock(1) is Linux-only (util-linux) and absent on macOS dev machines; - # restore.sh only ever uses the "flock -n FD" form (never the - # command-wrapping form), so a no-op success is a faithful stand-in for - # exclusion testing purposes - no test here exercises actual contention. - cat > "$dir/flock" <> "$mocklog" -exit 0 -EOF - - chmod +x "$dir/borg" "$dir/docker" "$dir/mysql" "$dir/mariadb" "$dir/flock" -} - -# restore.sh hardens PATH with system dirs (/usr/local/bin etc.) placed -# ahead of $PATH, so `PATH="$mockdir:$PATH" ...` alone does NOT guarantee the -# mock is what actually runs: a real borg/docker/mysql/mariadb installed in -# one of those system dirs would shadow it. BASH_ENV is sourced by bash -# before running a script and shell functions win over PATH lookup for -# simple commands regardless of PATH ordering, so this is what actually -# guarantees isolation on any machine, not just ones that happen to lack -# those binaries in the hardened prefix. -mock_bash_env() { - local dir="$1" - local bashenv="$dir/bash_env.sh" - cat > "$bashenv" </dev/null 2>&1 - rc=$? - assert_eq "1" "$rc" "no args exits 1" -} - -test_unknown_command_exits_one() { - local rc - bash "$RESTORE" bogus >/dev/null 2>&1 - rc=$? - assert_eq "1" "$rc" "unknown command exits 1" -} - -source "$HERE/lib/setup_mocks.sh" - -# Every test below isolates two things beyond PATH: BASH_ENV forces borg/ -# docker/mysql/mariadb to the mock regardless of what's really installed on -# this machine's PATH (restore.sh's own hardened PATH would otherwise shadow -# the mock with any real binary in a system dir), and LOCKFILE/ -# BORG_PASSPHRASE_FILE point at throwaway files so acquire_lock()/preflight() -# never touch real system paths like /var/lock or /root. - -test_list_archives() { - local mockdir out - mockdir="$(mktemp -d)" - setup_mock_bin "$mockdir" - out="$(PATH="$mockdir:$PATH" BASH_ENV="$(mock_bash_env "$mockdir")" bash "$RESTORE" --list-archives)" - assert_contains "$out" "host-2026-01-01T00-00-00" "list-archives shows first archive" - assert_contains "$out" "host-2026-06-01T00-00-00" "list-archives shows latest archive" - rm -rf "$mockdir" -} - -test_file_restore_extracts_to_dest() { - local mockdir dest out final passfile lockfile - mockdir="$(mktemp -d)" - dest="$(mktemp -d)" - setup_mock_bin "$mockdir" - passfile="$mockdir/passphrase" - lockfile="$mockdir/lock" - echo "s3cr3t" > "$passfile" - out="$(PATH="$mockdir:$PATH" BASH_ENV="$(mock_bash_env "$mockdir")" \ - BORG_PASSPHRASE_FILE="$passfile" LOCKFILE="$lockfile" \ - bash "$RESTORE" file photos/img.jpg --dest "$dest")" - final="$dest/home/srv/files/content/photos/img.jpg" - assert_contains "$out" "Restored file available at: $final" "file mode reports final path" - if [[ -f "$final" ]]; then - echo "PASS: extracted file exists on disk" - else - echo "FAIL: extracted file missing at $final" - FAILURES=$((FAILURES + 1)) - fi - rm -rf "$mockdir" "$dest" -} - -test_file_restore_dry_run_makes_no_borg_call() { - local mockdir dest - mockdir="$(mktemp -d)" - dest="$(mktemp -d)" - setup_mock_bin "$mockdir" - PATH="$mockdir:$PATH" BASH_ENV="$(mock_bash_env "$mockdir")" LOCKFILE="$mockdir/lock" \ - bash "$RESTORE" file photos/img.jpg --dest "$dest" --dry-run >/dev/null - if [[ -s "$mockdir/mock.log" ]] && grep -q "^borg extract" "$mockdir/mock.log"; then - echo "FAIL: dry-run invoked borg extract" - FAILURES=$((FAILURES + 1)) - else - echo "PASS: dry-run made no borg extract call" - fi - rm -rf "$mockdir" "$dest" -} - -test_db_restore_with_yes_runs_full_sequence() { - local mockdir out passfile - mockdir="$(mktemp -d)" - setup_mock_bin "$mockdir" - echo "rootpass" > "$mockdir/rootpw" - passfile="$mockdir/passphrase" - echo "s3cr3t" > "$passfile" - out="$(PATH="$mockdir:$PATH" BASH_ENV="$(mock_bash_env "$mockdir")" \ - ROOT_PASSWORD_FILE="$mockdir/rootpw" BORG_PASSPHRASE_FILE="$passfile" \ - LOCKFILE="$mockdir/lock" \ - bash "$RESTORE" db shopdb --yes /dev/null 2>&1 - rc=$? - set -e - assert_eq "1" "$rc" "db mode aborts on mismatched confirmation" - rm -rf "$mockdir" -} - -test_full_restore_refuses_nonempty_target_without_force() { - local mockdir target out rc - mockdir="$(mktemp -d)" - target="$(mktemp -d)" - touch "$target/existing-file" - setup_mock_bin "$mockdir" - set +e - out="$(PATH="$mockdir:$PATH" BASH_ENV="$(mock_bash_env "$mockdir")" LOCKFILE="$mockdir/lock" bash -c ' - sed "s#^TARGET=\"/home/srv/files/content\"#TARGET=\"'"$target"'\"#; s@^ARCHIVE_TARGET_PATH=.*@ARCHIVE_TARGET_PATH=\"\${TARGET#/}\"@" "'"$RESTORE"'" > "'"$mockdir"'/restore_patched.sh" - bash "'"$mockdir"'/restore_patched.sh" full - ' 2>&1)" - rc=$? - set -e - assert_eq "1" "$rc" "full mode refuses non-empty target without --force" - assert_contains "$out" "is not empty - pass --force" "refusal message names the reason" - rm -rf "$mockdir" "$target" -} - -test_full_restore_dry_run_makes_no_calls() { - local mockdir target out - mockdir="$(mktemp -d)" - target="$(mktemp -d)" - setup_mock_bin "$mockdir" - out="$(PATH="$mockdir:$PATH" BASH_ENV="$(mock_bash_env "$mockdir")" LOCKFILE="$mockdir/lock" bash -c ' - sed "s#^TARGET=\"/home/srv/files/content\"#TARGET=\"'"$target"'\"#; s@^ARCHIVE_TARGET_PATH=.*@ARCHIVE_TARGET_PATH=\"\${TARGET#/}\"@" "'"$RESTORE"'" > "'"$mockdir"'/restore_patched.sh" - bash "'"$mockdir"'/restore_patched.sh" full --dry-run - ')" - assert_contains "$out" "DRY-RUN" "full dry-run prints DRY-RUN plan" - # cmd_full calls resolve_archive() (a borg list call) before checking - # DRY_RUN, same as cmd_db/cmd_file, so mock.log legitimately gets a - # "borg list" entry. What must NOT happen in dry-run is a borg extract, - # or any docker/db-client interaction. - if grep -qE "^(borg extract|docker|mysql|mariadb)" "$mockdir/mock.log" 2>/dev/null; then - echo "FAIL: full dry-run invoked borg extract or a docker/db-client mock binary" - FAILURES=$((FAILURES + 1)) - else - echo "PASS: full dry-run made no borg extract or docker/db-client calls" - fi - rm -rf "$mockdir" "$target" -} - -test_help_exits_zero -test_no_args_exits_one -test_unknown_command_exits_one -test_list_archives -test_file_restore_extracts_to_dest -test_file_restore_dry_run_makes_no_borg_call -test_db_restore_with_yes_runs_full_sequence -test_db_restore_dry_run_skips_confirmation_and_calls -test_db_restore_aborts_on_wrong_confirmation -test_full_restore_refuses_nonempty_target_without_force -test_full_restore_dry_run_makes_no_calls - -echo "-----" -if [[ "$FAILURES" -gt 0 ]]; then - echo "$FAILURES failure(s)" - exit 1 -fi -echo "All tests passed"