MEGAINFRA-WEBAPP/scripts/clear-site-rows.py

123 lines
3.7 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Wipe all PocketBase rows for one site.
Run BEFORE re-importing The Hills ext 12 from Buildsite for the year.
Destructive — requires explicit --yes flag. Backs up every row's full
record to /tmp before deletion so a mid-clear crash still leaves
something to inspect.
PB batch DELETE returns 404 on these collections, so this script
parallelizes single-record deletes via threads.
Usage:
python3 scripts/clear-site-rows.py <site_id> --yes
"""
import json
import sys
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from pb_env import http, PB, superuser_token
if len(sys.argv) < 3 or sys.argv[2] != "--yes":
sys.exit("REFUSING: pass --yes to confirm destructive wipe")
SITE_ID = sys.argv[1]
BACKUP_DIR = Path(f"/tmp/pb-clear-{SITE_ID}")
BACKUP_DIR.mkdir(exist_ok=True)
TOKEN = superuser_token()
def list_all(collection: str, site_filter: str) -> list[dict]:
items: list[dict] = []
page = 1
while True:
url = (
f"/api/collections/{collection}/records"
f"?filter={site_filter}&fields=*&perPage=500&page={page}"
)
code, body = http("GET", url, token=TOKEN)
if code != 200:
sys.exit(f"list {collection} page {page} failed: {code} {body}")
page_items = body.get("items", [])
items.extend(page_items)
if len(page_items) < 500:
break
page += 1
return items
def delete_one(collection: str, rid: str) -> tuple[str, int]:
req = urllib.request.Request(
f"{PB}/api/collections/{collection}/records/{rid}",
method="DELETE",
headers={"Authorization": TOKEN},
)
try:
with urllib.request.urlopen(req) as r:
return rid, r.status
except urllib.error.HTTPError as e:
return rid, e.code
def parallel_delete(collection: str, ids: list[str], workers: int = 16) -> tuple[int, list]:
deleted = 0
errors: list = []
with ThreadPoolExecutor(max_workers=workers) as ex:
futures = {ex.submit(delete_one, collection, rid): rid for rid in ids}
for fut in as_completed(futures):
rid, code = fut.result()
if code == 204:
deleted += 1
else:
errors.append({"id": rid, "code": code})
return deleted, errors
def wipe(collection: str) -> None:
filter_q = f"site='{SITE_ID}'"
print(f" listing {collection} for site {SITE_ID}...")
rows = list_all(collection, filter_q)
print(f" found {len(rows)} rows")
if not rows:
return
backup_path = BACKUP_DIR / f"{collection}.json"
backup_path.write_text(json.dumps(rows, indent=2, default=str))
print(f" backed up to {backup_path}")
ids = [r["id"] for r in rows]
deleted, errors = parallel_delete(collection, ids)
print(f" deleted {deleted}/{len(ids)} rows")
if errors:
err_path = BACKUP_DIR / f"{collection}-delete-errors.json"
err_path.write_text(json.dumps(errors, indent=2))
print(f" ERRORS written to {err_path}")
sys.exit(1)
print(f"=== CLEAR SITE {SITE_ID} ===")
print(f"backup dir: {BACKUP_DIR}")
# fuel_logs reference vehicles — delete children first.
wipe("fuel_logs")
wipe("vehicles")
print("=== verifying ===")
for coll in ("fuel_logs", "vehicles"):
code, body = http(
"GET",
f"/api/collections/{coll}/records?filter=site='{SITE_ID}'&fields=id&perPage=1",
token=TOKEN,
)
if code != 200:
sys.exit(f"verify {coll} failed: {code} {body}")
remaining = body.get("totalItems", 0)
print(f" {coll}: {remaining} remaining")
if remaining != 0:
sys.exit(f"!! {coll} still has {remaining} rows for site {SITE_ID}")
print("=== DONE ===")