Files
ops/backups/scripts/retention.sh

103 lines
2.8 KiB
Bash

#!/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
failed = 0
considered = 0
for path in sorted(to_delete):
considered += 1
try:
os.remove(path)
print(path)
deleted += 1
except OSError as exc:
print(f"ERROR deleting {path}: {exc}", file=sys.stderr)
failed += 1
# Remove now-empty directories except anything under a latest/ subtree.
removed_dirs = 0
for root, dirs, _ in os.walk(base, topdown=False):
parts = set(os.path.normpath(root).split(os.sep))
if "latest" in parts:
continue
if os.path.normpath(root) == os.path.normpath(base):
continue
for d in list(dirs):
dir_path = os.path.join(root, d)
if "latest" in set(os.path.normpath(dir_path).split(os.sep)):
continue
try:
if not os.listdir(dir_path):
os.rmdir(dir_path)
removed_dirs += 1
except OSError:
pass
print(f"Retention complete for {base} (considered {considered}, deleted {deleted}, removed_empty_dirs {removed_dirs})")
if failed:
print(f"Retention encountered {failed} delete errors", file=sys.stderr)
sys.exit(1)
PY
echo "Policy: daily <= ${KEEP_DAILY_DAYS}d, weekly <= ${KEEP_WEEKLY_DAYS}d"