backup/stages/rclone-sync.sh

#!/usr/bin/env bash
# backup/stages/rclone-sync.sh
# Ship the `latest/` tree to B2. Keep 30 daily + 12 monthly backups in
# the bucket using rclone lsjson + delete (server-side retention is
# enabled too but this script is defensive).

set -euo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
. "${HERE}/../../scripts/lib/log.sh"

RCLONE_CONF="${RCLONE_CONF:-/srv/homelab/secrets/rclone.conf}"
LOCAL="${LOCAL:-/srv/homelab/backup/latest}"
REMOTE="${REMOTE:-b2-homelab:homelab-backups}"
KEEP_DAILY="${KEEP_DAILY:-30}"
KEEP_MONTHLY="${KEEP_MONTHLY:-12}"

TS="$(date +%Y%m%d)"

run_rclone() {
    rclone --config "${RCLONE_CONF}" "$@"
}

sync_one() {
    local name="$1"
    local src="${LOCAL}/${name}"
    local dst="${REMOTE}/${TS}/${name}"
    if [[ ! -e "${src}" ]]; then
        log_info "skip ${name} (no local data)"
        return 0
    fi
    log_info "sync ${name} -> ${dst}"
    run_rclone copy --transfers 4 --checkers 8 --fast-list \
        --b2-hard-delete --bwlimit 30M "${src}" "${dst}"
}

prune() {
    local now monthly_cutoff daily_cutoff
    now=$(date +%s)
    daily_cutoff=$((now - KEEP_DAILY * 86400))
    monthly_cutoff=$((now - KEEP_MONTHLY * 30 * 86400))
    run_rclone lsjson --dirs-only "${REMOTE}" \
        | jq -r '.[].Name' \
        | while read -r day; do
              local ts
              ts=$(date -d "${day}" +%s 2>/dev/null || echo 0)
              if (( ts == 0 )); then continue; fi
              local keep=0
              if (( ts >= daily_cutoff )); then keep=1; fi
              if (( ts >= monthly_cutoff )) && [[ "${day: -2}" == "01" ]]; then keep=1; fi
              if (( keep == 0 )); then
                  log_info "prune ${day}"
                  run_rclone purge "${REMOTE}/${day}"
              fi
          done
}

main() {
    for sub in pg sqlite vols snapshots; do
        sync_one "${sub}"
    done
    prune
}

main "$@"