saga-annotation / db_sync.py
Hodfa71's picture
Fix db_sync.py: use correct DB path /data/label_studio.sqlite3
e03d746 verified
Raw
History Blame Contribute Delete
4.95 kB
"""
db_sync.py — Persists the Label Studio SQLite DB across HF Space rebuilds
by syncing to/from the private TrustLLMeu/saga-db-backup dataset repo.
Usage:
python3 db_sync.py restore # pull DB from HF repo → /data/ls/
python3 db_sync.py backup # push /data/ls/label_studio.sqlite3 → HF repo
python3 db_sync.py watch # backup every INTERVAL seconds (run in background)
"""
import os
import shutil
import sqlite3
import sys
import tempfile
import time
HF_TOKEN = os.environ.get("HF_TOKEN", "")
BACKUP_REPO = "TrustLLMeu/saga-db-backup"
REMOTE_FILE = "label_studio.sqlite3"
INTERVAL = 300 # backup every 5 minutes
# Find the real LS DB — the one with the most users.
# LS may store its DB at /data/label_studio.sqlite3 (not /data/ls/label_studio.sqlite3).
def _find_db_path():
import glob as _g
candidates = (
_g.glob("/data/**/*.sqlite3", recursive=True) +
_g.glob("/label-studio/**/*.sqlite3", recursive=True)
)
best, best_n = "/data/ls/label_studio.sqlite3", -1
for p in candidates:
try:
c = sqlite3.connect(p)
n = c.execute("SELECT COUNT(*) FROM htx_user").fetchone()[0]
c.close()
if n > best_n:
best, best_n = p, n
except Exception:
pass
print(f"[db_sync] DB path: {best} ({best_n} users)", flush=True)
return best
DB_PATH = _find_db_path()
def _api():
from huggingface_hub import HfApi
if not HF_TOKEN:
raise RuntimeError("HF_TOKEN env var not set")
return HfApi(token=HF_TOKEN)
def _safe_copy(src, dst):
"""Copy a SQLite DB using the backup API so WAL data is included."""
with sqlite3.connect(src) as src_conn:
with sqlite3.connect(dst) as dst_conn:
src_conn.backup(dst_conn)
def restore():
"""Download DB from HF backup repo if it exists. Returns True if restored."""
try:
api = _api()
# Check if backup file exists in repo
files = api.list_repo_files(BACKUP_REPO, repo_type="dataset")
if REMOTE_FILE not in list(files):
print(f"[db_sync] No backup found in {BACKUP_REPO} — fresh start.", flush=True)
return False
print(f"[db_sync] Restoring DB from {BACKUP_REPO}...", flush=True)
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
# Download to a temp file first, then atomically replace
tmp = DB_PATH + ".restore_tmp"
path = api.hf_hub_download(
repo_id=BACKUP_REPO,
filename=REMOTE_FILE,
repo_type="dataset",
local_dir=os.path.dirname(tmp),
local_dir_use_symlinks=False,
)
shutil.move(path, DB_PATH)
size = os.path.getsize(DB_PATH)
print(f"[db_sync] Restored DB ({size:,} bytes).", flush=True)
return True
except Exception as e:
print(f"[db_sync] Restore failed: {e}", flush=True)
return False
def backup():
"""Upload current DB to HF backup repo using SQLite backup API (handles WAL)."""
if not os.path.exists(DB_PATH):
print(f"[db_sync] No DB at {DB_PATH} — skipping backup.", flush=True)
return False
tmp = None
try:
api = _api()
# Use SQLite backup API to create a consistent snapshot that includes WAL data.
# Uploading the raw .sqlite3 file misses uncommitted WAL transactions.
tmp = DB_PATH + ".upload_tmp"
_safe_copy(DB_PATH, tmp)
size = os.path.getsize(tmp)
# Verify the copy has data
with sqlite3.connect(tmp) as check:
n = check.execute("SELECT COUNT(*) FROM htx_user").fetchone()[0]
print(f"[db_sync] Backing up DB ({size:,} bytes, {n} users) → {BACKUP_REPO}...", flush=True)
api.upload_file(
path_or_fileobj=tmp,
path_in_repo=REMOTE_FILE,
repo_id=BACKUP_REPO,
repo_type="dataset",
commit_message=f"Auto-backup from HF Space ({n} users)",
)
print(f"[db_sync] Backup complete ({n} users).", flush=True)
return True
except Exception as e:
print(f"[db_sync] Backup failed: {e}", flush=True)
return False
finally:
if tmp and os.path.exists(tmp):
try:
os.unlink(tmp)
except Exception:
pass
def watch():
"""Run backup every INTERVAL seconds."""
print(f"[db_sync] Watch mode: backing up every {INTERVAL}s.", flush=True)
while True:
time.sleep(INTERVAL)
backup()
if __name__ == "__main__":
cmd = sys.argv[1] if len(sys.argv) > 1 else "backup"
if cmd == "restore":
ok = restore()
sys.exit(0 if ok else 1)
elif cmd == "backup":
ok = backup()
sys.exit(0 if ok else 1)
elif cmd == "watch":
watch()
else:
print(f"Usage: db_sync.py restore|backup|watch", flush=True)
sys.exit(1)