Asset / scripts /hermes_persist.py
Really-amin's picture
Replace placeholder Gradio app with HermesFace (Hermes Agent) Docker deployment
0790cf1
Raw
History Blame Contribute Delete
7.06 kB
#!/usr/bin/env python3
"""
HermesFace Full Directory Persistence for Hugging Face Spaces
=============================================================
Tar-gz snapshot of /opt/data with atomic upload, rotation, and integrity check.
Usage:
python3 hermes_persist.py save # backup
python3 hermes_persist.py load # restore latest
python3 hermes_persist.py status # show current backups
Env vars:
HF_TOKEN HF access token with write permission
HERMES_DATASET_REPO Target dataset repo (e.g. username/HermesFace-data)
HERMES_HOME Source directory (default: /opt/data)
MAX_BACKUPS Rotation size (default: 5)
"""
import hashlib
import json
import os
import shutil
import sys
import tarfile
import tempfile
from datetime import datetime
from pathlib import Path
from typing import Optional
from huggingface_hub import HfApi, hf_hub_download
from huggingface_hub.utils import RepositoryNotFoundError
class Config:
HERMES_HOME = Path(os.environ.get("HERMES_HOME", "/opt/data"))
DATASET_REPO = os.environ.get("HERMES_DATASET_REPO", "")
HF_TOKEN = os.environ.get("HF_TOKEN", "")
MAX_BACKUPS = int(os.environ.get("MAX_BACKUPS", "5"))
BACKUP_PREFIX = "backup-"
EXCLUDE_SUFFIXES = (".lock", ".tmp", ".pyc", ".socket", ".pid")
EXCLUDE_NAMES = {"__pycache__", "node_modules", ".DS_Store", ".git"}
SKIP_DIRS = {".cache", "logs/sys_logs"}
def _log(level: str, msg: str, **kv: object) -> None:
payload = {"timestamp": datetime.now().isoformat(), "level": level,
"module": "hermes-persist", "message": msg, **kv}
print(json.dumps(payload))
def _tar_filter(info: tarfile.TarInfo) -> Optional[tarfile.TarInfo]:
name = info.name
if any(name.endswith(suf) for suf in Config.EXCLUDE_SUFFIXES):
return None
parts = set(Path(name).parts)
if parts & Config.EXCLUDE_NAMES:
return None
rel = name.lstrip("./")
for skip in Config.SKIP_DIRS:
if rel == skip or rel.startswith(skip + "/"):
return None
return info
def _api() -> HfApi:
if not Config.HF_TOKEN:
raise RuntimeError("HF_TOKEN not set")
if not Config.DATASET_REPO:
raise RuntimeError("HERMES_DATASET_REPO not set")
return HfApi(token=Config.HF_TOKEN)
def _ensure_repo(api: HfApi) -> None:
try:
api.repo_info(repo_id=Config.DATASET_REPO, repo_type="dataset")
except RepositoryNotFoundError:
_log("INFO", "creating_dataset", repo=Config.DATASET_REPO)
api.create_repo(repo_id=Config.DATASET_REPO, repo_type="dataset", private=True)
def save() -> int:
if not Config.HERMES_HOME.exists():
_log("ERROR", "no_source_dir", path=str(Config.HERMES_HOME))
return 1
api = _api()
_ensure_repo(api)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
archive_name = f"state/{Config.BACKUP_PREFIX}{timestamp}.tar.gz"
with tempfile.TemporaryDirectory() as tmp:
tar_path = Path(tmp) / "hermes.tar.gz"
with tarfile.open(tar_path, "w:gz") as tf:
tf.add(str(Config.HERMES_HOME), arcname=".", filter=_tar_filter)
size = tar_path.stat().st_size
sha256 = hashlib.sha256(tar_path.read_bytes()).hexdigest()
_log("INFO", "uploading_backup", file=archive_name, bytes=size, sha256=sha256[:12])
api.upload_file(
path_or_fileobj=str(tar_path),
path_in_repo=archive_name,
repo_id=Config.DATASET_REPO,
repo_type="dataset",
commit_message=f"HermesFace backup {timestamp}",
)
try:
files = api.list_repo_files(repo_id=Config.DATASET_REPO, repo_type="dataset")
backups = sorted(
f for f in files
if f.startswith(f"state/{Config.BACKUP_PREFIX}") and f.endswith(".tar.gz")
)
if len(backups) > Config.MAX_BACKUPS:
for old in backups[: -Config.MAX_BACKUPS]:
_log("INFO", "rotating_backup", delete=old)
api.delete_file(
path_in_repo=old,
repo_id=Config.DATASET_REPO,
repo_type="dataset",
token=Config.HF_TOKEN,
)
except Exception as e:
_log("WARNING", "rotation_failed", error=str(e))
_log("INFO", "save_completed", file=archive_name)
return 0
def load() -> int:
api = _api()
try:
files = api.list_repo_files(repo_id=Config.DATASET_REPO, repo_type="dataset")
except RepositoryNotFoundError:
_log("ERROR", "repo_not_found", repo=Config.DATASET_REPO)
return 1
backups = sorted(
(f for f in files
if f.startswith(f"state/{Config.BACKUP_PREFIX}") and f.endswith(".tar.gz")),
reverse=True,
)
if not backups:
_log("WARNING", "no_backups_found")
return 1
Config.HERMES_HOME.mkdir(parents=True, exist_ok=True)
for backup in backups:
_log("INFO", "attempting_restore", file=backup)
try:
local = hf_hub_download(
repo_id=Config.DATASET_REPO,
repo_type="dataset",
filename=backup,
token=Config.HF_TOKEN,
)
# Local safety backup
snapshot = Config.HERMES_HOME.parent / f"hermes_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
if Config.HERMES_HOME.exists() and any(Config.HERMES_HOME.iterdir()):
shutil.copytree(Config.HERMES_HOME, snapshot, dirs_exist_ok=True)
with tarfile.open(local, "r:*") as tf:
tf.extractall(str(Config.HERMES_HOME))
_log("INFO", "restore_completed", file=backup)
return 0
except Exception as e:
_log("ERROR", "restore_failed", file=backup, error=str(e))
continue
_log("ERROR", "all_restore_attempts_failed")
return 1
def status() -> int:
api = _api()
try:
files = api.list_repo_files(repo_id=Config.DATASET_REPO, repo_type="dataset")
except RepositoryNotFoundError:
_log("WARNING", "repo_not_found", repo=Config.DATASET_REPO)
return 0
backups = sorted(
(f for f in files
if f.startswith(f"state/{Config.BACKUP_PREFIX}") and f.endswith(".tar.gz")),
reverse=True,
)
local_files = 0
if Config.HERMES_HOME.exists():
local_files = sum(1 for _ in Config.HERMES_HOME.rglob("*") if _.is_file())
print(json.dumps({
"repo": Config.DATASET_REPO,
"local_dir": str(Config.HERMES_HOME),
"local_files": local_files,
"remote_backups": backups,
"max_backups": Config.MAX_BACKUPS,
}, indent=2))
return 0
COMMANDS = {"save": save, "load": load, "status": status}
def main() -> None:
if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
print(f"Usage: {sys.argv[0]} {{{'|'.join(COMMANDS)}}}", file=sys.stderr)
sys.exit(2)
sys.exit(COMMANDS[sys.argv[1]]())
if __name__ == "__main__":
main()