"""Durable storage for bots, backed by a private Hugging Face dataset. Space disks are ephemeral: a rebuild, a sleep/wake cycle or a maintenance restart wipes everything under /home/user. Without this module the host would silently lose every bot it was given. On boot we pull the dataset into DATA_DIR; on every deploy/start/stop we push the affected bot back up. The dataset is PRIVATE — it holds Nerimity bot tokens in bots//secret.json. """ from __future__ import annotations import os import shutil import threading import traceback from pathlib import Path from huggingface_hub import HfApi, snapshot_download DATA_DIR = Path(os.environ.get("BB_DATA_DIR", "/home/user/data")) BOTS_DIR = DATA_DIR / "bots" DATASET_ID = os.environ.get("BB_DATASET", "joddabod/bbuilder-store") HF_TOKEN = os.environ.get("HF_TOKEN") # Only these ever leave the container. Notably excludes node_modules. SYNC_FILES = ("bot.js", "bb-runtime.js", "package.json", "project.bbproj", "meta.json", "secret.json") _push_lock = threading.Lock() def available() -> bool: return bool(HF_TOKEN) def _api() -> HfApi: return HfApi(token=HF_TOKEN) def pull_all() -> str: """Restore every bot from the dataset into DATA_DIR. Returns a human-readable summary.""" if not available(): return "no HF_TOKEN — running without durable storage (bots will not survive restarts)" try: snap = snapshot_download( repo_id=DATASET_ID, repo_type="dataset", token=HF_TOKEN, allow_patterns=["bots/**"], ) except Exception as exc: # noqa: BLE001 return f"restore skipped: {exc}" src = Path(snap) / "bots" if not src.exists(): return "store is empty (no bots yet)" BOTS_DIR.mkdir(parents=True, exist_ok=True) restored = 0 for bot_dir in src.iterdir(): if not bot_dir.is_dir(): continue dest = BOTS_DIR / bot_dir.name dest.mkdir(parents=True, exist_ok=True) for name in SYNC_FILES: f = bot_dir / name if f.exists(): shutil.copy2(f, dest / name) if (dest / "secret.json").exists(): (dest / "secret.json").chmod(0o600) restored += 1 return f"restored {restored} bot(s) from {DATASET_ID}" def push_bot(bot_id: str) -> None: """Mirror one bot's directory up to the dataset. Best-effort, never fatal.""" if not available(): return src = BOTS_DIR / bot_id if not src.is_dir(): return with _push_lock: try: _api().upload_folder( repo_id=DATASET_ID, repo_type="dataset", folder_path=str(src), path_in_repo=f"bots/{bot_id}", allow_patterns=list(SYNC_FILES), commit_message=f"sync {bot_id}", ) except Exception: # noqa: BLE001 traceback.print_exc() def push_bot_async(bot_id: str) -> None: threading.Thread(target=push_bot, args=(bot_id,), daemon=True).start() def delete_bot(bot_id: str) -> None: if not available(): return try: _api().delete_folder( path_in_repo=f"bots/{bot_id}", repo_id=DATASET_ID, repo_type="dataset", commit_message=f"delete {bot_id}", ) except Exception: # noqa: BLE001 pass