Files
vosonandCursor f5b7011c45 Ship Steam login, D1 player sync, and cached「我」dashboard.
Players get a fast TTL-backed homepage (local profile / Cloudflare D1) with dense UI polish; login unlocks /home without blocking on every OpenDota refresh.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 01:24:30 +08:00

178 lines
5.7 KiB
Python

"""Create R2 bucket, bind Pages, redeploy Worker using keyzoo-injected CF creds."""
from __future__ import annotations
import json
import os
import subprocess
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
WORKER = Path(__file__).resolve().parent / "player-sync"
ACCT = "510534f7f6284344aadaf2f5a0794d48"
BUCKET = "climperor-player-data"
PAGES = "climperor-relations"
QUEUE = "climperor-player-sync"
D1_ID = "9eeb24ba-acc5-4520-b4e7-754ea776394e"
API = "https://api.cloudflare.com/client/v4"
RESOURCES = Path(__file__).resolve().parent / ".resources.json"
def creds() -> tuple[str, str]:
email = os.environ.get("CLOUDFLARE_EMAIL") or os.environ.get(
"KEYZOO_ASSET_META_USERNAME"
)
key = os.environ.get("CLOUDFLARE_API_KEY") or os.environ.get(
"KEYZOO_ASSET_SECRET_GLOBAL_API_KEY"
)
if not email or not key:
raise SystemExit("missing CF credentials")
return email, key
def api(method: str, path: str, body: dict | None = None, retries: int = 6) -> dict:
email, key = creds()
data = None if body is None else json.dumps(body).encode("utf-8")
last = None
for i in range(retries):
req = urllib.request.Request(
API + path,
data=data,
method=method,
headers={
"X-Auth-Email": email,
"X-Auth-Key": key,
"Content-Type": "application/json",
"User-Agent": "climperor-enable-r2",
},
)
try:
with urllib.request.urlopen(req, timeout=60) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
raw = e.read().decode("utf-8", errors="replace")
last = f"{e.code}: {raw[:200]}"
if e.code in (521, 522, 523, 524, 525, 429, 503) and i + 1 < retries:
wait = 4 + i * 3
print(f"retry {i+1}/{retries} after {e.code}, sleep {wait}s", flush=True)
time.sleep(wait)
continue
raise SystemExit(f"CF API {method} {path} -> {last}") from e
except Exception as e:
last = str(e)
if i + 1 < retries:
wait = 4 + i * 3
print(f"retry {i+1}/{retries} after {type(e).__name__}, sleep {wait}s", flush=True)
time.sleep(wait)
continue
raise SystemExit(f"CF API {method} {path} -> {last}") from e
raise SystemExit(f"CF API failed: {last}")
def ensure_r2() -> None:
try:
api("GET", f"/accounts/{ACCT}/r2/buckets/{BUCKET}", retries=3)
print(f"r2 exists: {BUCKET}", flush=True)
return
except SystemExit:
pass
payload = api("POST", f"/accounts/{ACCT}/r2/buckets", {"name": BUCKET})
if not payload.get("success"):
err = str(payload.get("errors") or "")
if "already exists" in err.lower() or "10004" in err:
print(f"r2 exists: {BUCKET}", flush=True)
return
raise SystemExit(f"r2 create failed: {payload.get('errors')}")
print(f"r2 created: {BUCKET}", flush=True)
def bind_pages() -> None:
path = f"/accounts/{ACCT}/pages/projects/{PAGES}"
project = api("GET", path)["result"]
dc = project.get("deployment_configs") or {}
prod = dict(dc.get("production") or {})
preview = dict(dc.get("preview") or {})
fail_open = prod.get("fail_open")
if fail_open is None:
fail_open = preview.get("fail_open")
if fail_open is None:
fail_open = False
def one(base: dict) -> dict:
out = {
k: v
for k, v in base.items()
if k not in ("d1_databases", "queue_producers", "r2_buckets", "fail_open")
}
d1 = dict(base.get("d1_databases") or {})
d1["DB"] = {"id": D1_ID}
out["d1_databases"] = d1
producers = dict(base.get("queue_producers") or {})
producers["SYNC_QUEUE"] = {"name": QUEUE}
out["queue_producers"] = producers
buckets = dict(base.get("r2_buckets") or {})
buckets["MATCHES"] = {"name": BUCKET}
out["r2_buckets"] = buckets
out["fail_open"] = bool(fail_open)
return out
api(
"PATCH",
path,
{
"deployment_configs": {
"production": one(prod),
"preview": one(preview),
}
},
)
print("pages bindings: DB + SYNC_QUEUE + MATCHES", flush=True)
def update_resources() -> None:
data = {}
if RESOURCES.is_file():
data = json.loads(RESOURCES.read_text(encoding="utf-8"))
data["account_id"] = ACCT
data["r2"] = {"name": BUCKET, "ready": True}
data.setdefault("d1", {"name": "climperor-users", "id": D1_ID})
data.setdefault("queue", {"name": QUEUE})
data.setdefault("pages_project", PAGES)
data.setdefault("worker", "climperor-player-sync")
RESOURCES.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
print(f"updated {RESOURCES}", flush=True)
def deploy_worker() -> None:
email, key = creds()
env = os.environ.copy()
env["CLOUDFLARE_EMAIL"] = email
env["CLOUDFLARE_API_KEY"] = key
env["CLOUDFLARE_ACCOUNT_ID"] = ACCT
print("deploying worker…", flush=True)
proc = subprocess.run(
"npx --yes wrangler@3 deploy",
cwd=str(WORKER),
env=env,
shell=True,
)
if proc.returncode != 0:
raise SystemExit(f"worker deploy failed: {proc.returncode}")
print("worker deployed", flush=True)
def main() -> int:
ensure_r2()
bind_pages()
update_resources()
deploy_worker()
return 0
if __name__ == "__main__":
raise SystemExit(main())