bbuilder-host / store.py
joddabod's picture
deploy bbuilder host
e2b893f verified
Raw History Blame Contribute Delete
3.4 kB
"""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/<id>/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