Two things surfaced from actual use this session: 1. Tab completion (tools/setup-completion.bash, added earlier) required manually editing ~/.bashrc — easy to skip or get wrong (confirmed live: the source line never actually landed the first time). base now wires it in automatically (idempotent, checked by grep first), matching how it already touches ~/.bashrc for SSH Host aliases. 2. No pruning existed anywhere for the *.backup.<timestamp> files ~60 different services create before overwriting a live config (Caddyfile, /etc/fstab, etc) — every one of them backs up, none clean up, so they accumulate forever on a box reconfigured regularly. tools/prune-old-backups.sh prunes by file mtime (not by parsing the timestamp out of the filename — robust to the %Y%m%d-%H%M%S vs %Y%m%d_%H%M%S inconsistency across services), always keeping the single newest backup per distinct file regardless of age. Verified both the normal case (mixed old/new, prunes only the old ones) and the edge case (every backup for a file is old, keeps the newest one anyway) against real fixtures. base offers it as a daily systemd timer (prompted, since it deletes files — unlike the tab-completion wiring, which doesn't). Also added logrotate for Caddy's own access logs (/var/log/caddy/*.log), which had no rotation at all and grow unbounded on an active box. Uses copytruncate specifically: the log directory is bind-mounted into the running Caddy container and read live by CrowdSec, so truncating in place avoids either of them needing to notice or react to a rotation happening. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn
70 lines
2.7 KiB
Bash
70 lines
2.7 KiB
Bash
#!/usr/bin/env bash
|
|
# tools/prune-old-backups.sh — Deletes old *.backup.* files this repo's
|
|
# services create before overwriting a live config (Caddyfile, /etc/fstab,
|
|
# etc — the pattern is always <name>.backup.<timestamp>, though the exact
|
|
# timestamp format varies by service: %Y%m%d-%H%M%S vs %Y%m%d_%H%M%S).
|
|
# Nothing in this repo ever cleans these up at the point they're created —
|
|
# every service backs up before a destructive write, none of them prune
|
|
# afterward, so they accumulate forever on any box reconfigured regularly.
|
|
#
|
|
# Prunes by file mtime (not by parsing the timestamp out of the filename —
|
|
# robust to the format inconsistency above, since it never has to parse it
|
|
# at all), and always keeps the single newest backup per distinct file
|
|
# regardless of age, so a box that hasn't been touched in months never
|
|
# ends up with zero backups for something.
|
|
#
|
|
# Usage:
|
|
# sudo bash prune-old-backups.sh [KEEP_DAYS] (default 30)
|
|
|
|
set -uo pipefail
|
|
|
|
KEEP_DAYS="${1:-30}"
|
|
[[ "$KEEP_DAYS" =~ ^[0-9]+$ ]] || { echo "KEEP_DAYS must be a number" >&2; exit 1; }
|
|
|
|
ACTUAL_USER="${SUDO_USER:-${USER:-root}}"
|
|
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "/home/$ACTUAL_USER")"
|
|
DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}"
|
|
|
|
SEARCH_DIRS=()
|
|
[ -d "$DOCKER_DIR" ] && SEARCH_DIRS+=("$DOCKER_DIR")
|
|
[ -d /etc ] && SEARCH_DIRS+=(/etc)
|
|
|
|
if [ "${#SEARCH_DIRS[@]}" -eq 0 ]; then
|
|
echo "Nothing to scan (no $DOCKER_DIR, no /etc)."
|
|
exit 0
|
|
fi
|
|
|
|
# Pass 1: find every distinct "base" (the path with .backup.TIMESTAMP
|
|
# stripped) and remember its single newest match — that one is always
|
|
# kept below, regardless of age.
|
|
declare -A NEWEST_PER_BASE
|
|
while IFS= read -r -d '' f; do
|
|
base="${f%.backup.*}"
|
|
cur="${NEWEST_PER_BASE[$base]:-}"
|
|
if [ -z "$cur" ] || [ "$f" -nt "$cur" ]; then
|
|
NEWEST_PER_BASE["$base"]="$f"
|
|
fi
|
|
done < <(find "${SEARCH_DIRS[@]}" -maxdepth 6 -type f -name "*.backup.*" -print0 2>/dev/null)
|
|
|
|
DELETED=0
|
|
KEPT_AS_NEWEST=0
|
|
|
|
# Pass 2: delete only files older than KEEP_DAYS, skipping any that are
|
|
# the sole/newest backup for their base (the safety net above).
|
|
while IFS= read -r -d '' f; do
|
|
is_newest=false
|
|
for keep in "${NEWEST_PER_BASE[@]}"; do
|
|
if [ "$f" = "$keep" ]; then
|
|
is_newest=true
|
|
break
|
|
fi
|
|
done
|
|
if [ "$is_newest" = true ]; then
|
|
KEPT_AS_NEWEST=$((KEPT_AS_NEWEST + 1))
|
|
continue
|
|
fi
|
|
rm -f "$f" && DELETED=$((DELETED + 1))
|
|
done < <(find "${SEARCH_DIRS[@]}" -maxdepth 6 -type f -name "*.backup.*" -mtime "+${KEEP_DAYS}" -print0 2>/dev/null)
|
|
|
|
echo "Backup pruning: removed $DELETED file(s) older than ${KEEP_DAYS} days (kept $KEPT_AS_NEWEST as the newest-per-file safety net)"
|