#!/usr/bin/env bash set -euo pipefail BASE="${BASE:-/srv/backups}" KEEP_DAILY_DAYS="${KEEP_DAILY_DAYS:-7}" KEEP_WEEKLY_DAYS="${KEEP_WEEKLY_DAYS:-365}" python3 - <<'PY' "$BASE" "$KEEP_DAILY_DAYS" "$KEEP_WEEKLY_DAYS" import datetime as dt import os import sys base = sys.argv[1] keep_daily_days = int(sys.argv[2]) keep_weekly_days = int(sys.argv[3]) now = dt.datetime.now().timestamp() if not os.path.isdir(base): print(f"Backup base not found: {base}") sys.exit(0) files = [] for root, _, names in os.walk(base): parts = set(os.path.normpath(root).split(os.sep)) preserve_latest = "latest" in parts for name in names: path = os.path.join(root, name) if not os.path.isfile(path): continue try: mtime = os.path.getmtime(path) except OSError: continue age_days = (now - mtime) / 86400 files.append((path, root, mtime, age_days, preserve_latest)) to_delete = set() # Delete anything older than weekly window (unless it's in a latest folder) for path, _, _, age_days, preserve_latest in files: if not preserve_latest and age_days > keep_weekly_days: to_delete.add(path) # For weekly window, keep one file per week per directory (latest by mtime) weekly_candidates = {} for path, root, mtime, age_days, preserve_latest in files: if preserve_latest: continue if keep_daily_days < age_days <= keep_weekly_days: iso = dt.datetime.fromtimestamp(mtime).isocalendar() key = (root, iso.year, iso.week) best = weekly_candidates.get(key) if best is None or mtime > best[1]: weekly_candidates[key] = (path, mtime) weekly_keep = {v[0] for v in weekly_candidates.values()} for path, _, _, age_days, preserve_latest in files: if preserve_latest: continue if keep_daily_days < age_days <= keep_weekly_days and path not in weekly_keep: to_delete.add(path) deleted = 0 for path in sorted(to_delete): try: os.remove(path) print(path) deleted += 1 except OSError: pass print(f"Retention complete for {base} (deleted {deleted} files)") PY echo "Policy: daily <= ${KEEP_DAILY_DAYS}d, weekly <= ${KEEP_WEEKLY_DAYS}d"