122 lines
3.7 KiB
Python
122 lines
3.7 KiB
Python
#!/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 ===")
|