Last active
April 16, 2026 11:26
-
-
Save jez500/f41e2813389ca459c7f39dd4c4e1f386 to your computer and use it in GitHub Desktop.
PaperclipAi backup rotation script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| BACKUP_DIR="${BACKUP_DIR:-$HOME/.paperclip/instances/default/data/backups}" | |
| # Retention config (override via env) | |
| RETENTION_HOURLY="${RETENTION_HOURLY:-24}" | |
| RETENTION_DAILY_DAYS="${RETENTION_DAILY_DAYS:-14}" | |
| RETENTION_WEEKLY_DAYS="${RETENTION_WEEKLY_DAYS:-28}" | |
| RETENTION_MONTHLY="${RETENTION_MONTHLY:-1}" # 1=enabled, 0=disabled | |
| DRY_RUN=false | |
| usage() { | |
| cat <<EOF | |
| Backup rotation script | |
| Usage: | |
| $(basename "$0") [--dry-run] [--help] | |
| Options: | |
| --dry-run Show what would be deleted without deleting | |
| --help Show this help | |
| Environment variables: | |
| BACKUP_DIR Backup directory | |
| RETENTION_HOURLY Number of most recent backups to always keep (default: 24) | |
| RETENTION_DAILY_DAYS Keep 1 per day for N days (default: 14) | |
| RETENTION_WEEKLY_DAYS Keep 1 per week for N days (default: 28) | |
| RETENTION_MONTHLY Keep 1 per month forever (1=on, 0=off) | |
| Examples: | |
| RETENTION_HOURLY=48 $(basename "$0") | |
| RETENTION_DAILY_DAYS=7 RETENTION_WEEKLY_DAYS=21 $(basename "$0") --dry-run | |
| EOF | |
| } | |
| # Parse args | |
| for arg in "$@"; do | |
| case "$arg" in | |
| --dry-run) DRY_RUN=true ;; | |
| --help|-h) usage; exit 0 ;; | |
| *) echo "Unknown argument: $arg" >&2; usage; exit 1 ;; | |
| esac | |
| done | |
| [[ -d "$BACKUP_DIR" ]] || { echo "Backup directory not found: $BACKUP_DIR" >&2; exit 1; } | |
| get_mtime() { | |
| if [[ "$(uname)" == "Darwin" ]]; then | |
| stat -f %m "$1" | |
| else | |
| stat -c %Y "$1" | |
| fi | |
| } | |
| fmt_date() { | |
| if date -d "@$1" +"$2" >/dev/null 2>&1; then | |
| date -d "@$1" +"$2" | |
| else | |
| date -r "$1" +"$2" | |
| fi | |
| } | |
| shopt -s nullglob | |
| raw_files=( "$BACKUP_DIR"/*.sql ) | |
| shopt -u nullglob | |
| (( ${#raw_files[@]} )) || { echo "No backups found."; exit 0; } | |
| # Sort newest first | |
| mapfile -t files < <( | |
| for f in "${raw_files[@]}"; do | |
| printf '%s\t%s\n' "$(get_mtime "$f")" "$f" | |
| done | sort -rn | cut -f2- | |
| ) | |
| now=$(date +%s) | |
| declare -A keep daily weekly monthly | |
| kept=0 | |
| deleted=0 | |
| # Keep N most recent | |
| for i in "${!files[@]}"; do | |
| (( i < RETENTION_HOURLY )) && keep["${files[$i]}"]=1 | |
| done | |
| for f in "${files[@]}"; do | |
| mtime=$(get_mtime "$f") | |
| age_days=$(( (now - mtime) / 86400 )) | |
| day=$(fmt_date "$mtime" "%Y-%m-%d") | |
| week=$(fmt_date "$mtime" "%Y-%W") | |
| month=$(fmt_date "$mtime" "%Y-%m") | |
| if (( age_days <= RETENTION_DAILY_DAYS )) && [[ -z "${daily[$day]:-}" ]]; then | |
| daily["$day"]="$f" | |
| fi | |
| if (( age_days <= RETENTION_WEEKLY_DAYS )) && [[ -z "${weekly[$week]:-}" ]]; then | |
| weekly["$week"]="$f" | |
| fi | |
| if (( RETENTION_MONTHLY == 1 )) && [[ -z "${monthly[$month]:-}" ]]; then | |
| monthly["$month"]="$f" | |
| fi | |
| done | |
| for k in "${!daily[@]}"; do keep["${daily[$k]}"]=1; done | |
| for k in "${!weekly[@]}"; do keep["${weekly[$k]}"]=1; done | |
| for k in "${!monthly[@]}"; do keep["${monthly[$k]}"]=1; done | |
| for f in "${files[@]}"; do | |
| if [[ -n "${keep[$f]:-}" ]]; then | |
| ((++kept)) | |
| else | |
| if $DRY_RUN; then | |
| echo "[dry-run] would delete: $(basename "$f")" | |
| else | |
| rm -- "$f" | |
| echo "deleted: $(basename "$f")" | |
| fi | |
| ((++deleted)) | |
| fi | |
| done | |
| suffix="" | |
| $DRY_RUN && suffix=" (dry run)" | |
| echo "$(date): kept=$kept deleted=$deleted total=${#files[@]}$suffix" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment