103 lines
2.8 KiB
Bash
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"
|