#!/usr/bin/env python3 """Atomic Hugging Face Dataset persistence for HermesFace state. The public helpers retain the legacy single-file API, while the production sync path commits one validated secure-backup archive plus metadata in a single Dataset commit. """ from __future__ import annotations import hashlib import json import logging import os import shutil import sys import tempfile import time from datetime import datetime, timezone from pathlib import Path, PurePosixPath from typing import Any, Callable, Dict, List, Optional, TypeVar from huggingface_hub import CommitOperationAdd, HfApi, hf_hub_download from huggingface_hub.utils import HfHubHTTPError, RepositoryNotFoundError T = TypeVar("T") _RETRYABLE_STATUS = {408, 425, 429, 500, 502, 503, 504} logging.basicConfig( level=logging.INFO, format='{"timestamp": "%(asctime)s", "level": "%(levelname)s", "module": "atomic-save", "message": "%(message)s"}', ) logger = logging.getLogger(__name__) def _is_retryable_hf_error(exc: Exception) -> bool: """True for transient Hugging Face Hub / network failures.""" if isinstance(exc, HfHubHTTPError): response = getattr(exc, "response", None) status = getattr(response, "status_code", None) if status in _RETRYABLE_STATUS: return True message = str(exc).lower() return any(token in message for token in ("502", "503", "504", "bad gateway", "gateway timeout")) def _safe_dataset_path(value: str) -> str: pure = PurePosixPath(str(value).replace("\\", "/")) if pure.is_absolute() or not pure.parts or any(part in {"", ".", ".."} for part in pure.parts): raise ValueError(f"unsafe dataset path: {value!r}") return pure.as_posix() def validate_backup_source_paths(source_paths: List[str], state_dir: Optional[str] = None) -> List[Path]: """Resolve and validate explicitly allowlisted backup source files.""" try: from backup_allowlist import BackupAllowlistError, is_path_allowed, scan_file_for_secrets except ImportError: from scripts.backup_allowlist import ( # type: ignore BackupAllowlistError, is_path_allowed, scan_file_for_secrets, ) root = Path(state_dir or os.environ.get("HERMES_HOME", "/opt/data")).resolve() validated: List[Path] = [] for raw in source_paths: unresolved = Path(raw) if unresolved.is_symlink(): raise BackupAllowlistError(f"Refusing symlink backup source: {unresolved.name}") path = unresolved.resolve() try: relative = path.relative_to(root) except ValueError as exc: raise BackupAllowlistError(f"Refusing backup outside HERMES_HOME: {path.name}") from exc if not path.is_file(): raise BackupAllowlistError(f"Refusing non-file backup source: {relative.as_posix()}") if not is_path_allowed(relative.as_posix()): raise BackupAllowlistError(f"Refusing non-allowlisted backup source: {relative.as_posix()}") reason = scan_file_for_secrets(path) if reason: raise BackupAllowlistError( f"Refusing backup: secret-like content in {relative.as_posix()} ({reason})" ) validated.append(path) return validated class AtomicDatasetSaver: def __init__( self, repo_id: str, dataset_path: str = "state", *, api: Optional[HfApi] = None, token: Optional[str] = None, ): self.repo_id = repo_id self.dataset_path = _safe_dataset_path(dataset_path) self.token = token self.api = api or HfApi(token=token) self.max_retries = 3 self.base_delay = 1.0 self.max_backups = 3 logger.info(f"init repo_id={repo_id} dataset_path={self.dataset_path}") def _call_with_retry(self, label: str, fn: Callable[[], T]) -> T: """Retry transient Hub API failures with bounded exponential backoff.""" attempt = 0 while True: try: return fn() except Exception as exc: attempt += 1 if attempt > self.max_retries or not _is_retryable_hf_error(exc): raise delay = min(30.0, self.base_delay * (2 ** (attempt - 1))) logger.warning( f"{label}_retry attempt={attempt}/{self.max_retries} " f"delay_s={delay:.1f} error={type(exc).__name__}" ) time.sleep(delay) def calculate_checksum(self, file_path: Path) -> str: digest = hashlib.sha256() with file_path.open("rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def _repo_info(self): try: return self._call_with_retry( "repo_info", lambda: self.api.repo_info(repo_id=self.repo_id, repo_type="dataset"), ) except RepositoryNotFoundError: return None def create_backup(self, current_commit: Optional[str]) -> Optional[str]: """Copy the current authoritative state files to a timestamped remote backup.""" if not current_commit: return None try: files = self._call_with_retry( "list_repo_files", lambda: self.api.list_repo_files( repo_id=self.repo_id, repo_type="dataset", revision=current_commit ), ) prefix = f"{self.dataset_path}/" state_files = [path for path in files if path.startswith(prefix)] if not state_files: return None timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") backup_path = f"backups/state_{timestamp}" logger.info(f"creating_backup path={backup_path} files={len(state_files)}") with tempfile.TemporaryDirectory(prefix="hermes-hf-backup-") as tmpdir: root = Path(tmpdir) operations = [] for remote_path in state_files: local = self._call_with_retry( "hf_hub_download", lambda remote_path=remote_path: hf_hub_download( repo_id=self.repo_id, repo_type="dataset", filename=remote_path, revision=current_commit, token=self.token, ), ) relative = PurePosixPath(remote_path).relative_to(PurePosixPath(self.dataset_path)) destination = root.joinpath(*relative.parts) destination.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(local, destination) operations.append( CommitOperationAdd( path_in_repo=f"{backup_path}/{relative.as_posix()}", path_or_fileobj=str(destination), ) ) if not operations: return None info = self._call_with_retry( "create_backup_commit", lambda: self.api.create_commit( repo_id=self.repo_id, repo_type="dataset", operations=operations, commit_message=f"Backup state before update - {timestamp}", parent_commit=current_commit, ), ) logger.info(f"backup_created commit={info.oid}") return info.oid except Exception as exc: # A failed remote backup must prevent replacing the current remote state. logger.error(f"backup_failed error={exc}") raise def save_secure_archive_atomic( self, archive_path: Path, state_data: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """Validate and atomically commit one secure backup archive and metadata.""" try: from secure_backup import extract_validated except ImportError: from scripts.secure_backup import extract_validated # type: ignore archive = Path(archive_path) if archive.is_symlink() or not archive.is_file(): raise ValueError("secure backup archive must be a regular file") # Validate member paths, member types, manifest, hashes, and SQLite before # any Hugging Face API call is permitted. with tempfile.TemporaryDirectory(prefix="hermes-archive-validate-") as tmpdir: manifest = extract_validated(archive, Path(tmpdir) / "validated") archive_sha256 = self.calculate_checksum(archive) operation_id = f"save_{int(time.time())}" repo_info = self._repo_info() current_commit = getattr(repo_info, "sha", None) if repo_info else None backup_commit = self.create_backup(current_commit) parent_commit = backup_commit or current_commit metadata = { "format": "hermes-secure-backup", "format_version": 1, "timestamp": datetime.now(timezone.utc).isoformat(), "operation_id": operation_id, "archive": "secure_state.tar.gz", "archive_sha256": archive_sha256, "manifest_sha256": hashlib.sha256( json.dumps(manifest, sort_keys=True, separators=(",", ":")).encode("utf-8") ).hexdigest(), "manifest": manifest, "backup_commit": backup_commit, "state_data": state_data or {}, } with tempfile.TemporaryDirectory(prefix="hermes-hf-save-") as tmpdir: metadata_path = Path(tmpdir) / "metadata.json" metadata_path.write_text( json.dumps(metadata, sort_keys=True, separators=(",", ":")), encoding="utf-8", ) operations = [ CommitOperationAdd( path_in_repo=f"{self.dataset_path}/secure_state.tar.gz", path_or_fileobj=str(archive), ), CommitOperationAdd( path_in_repo=f"{self.dataset_path}/metadata.json", path_or_fileobj=str(metadata_path), ), ] info = self._call_with_retry( "create_commit", lambda: self.api.create_commit( repo_id=self.repo_id, repo_type="dataset", operations=operations, commit_message=f"Atomic secure state update - {operation_id}", parent_commit=parent_commit, ), ) result = { "success": True, "operation_id": operation_id, "commit_id": info.oid, "backup_commit": backup_commit, "timestamp": metadata["timestamp"], "archive_sha256": archive_sha256, "files_count": len(manifest.get("files", [])), } logger.info(f"atomic_secure_save_completed {result}") return result def save_state_atomic( self, state_data: Dict[str, Any], source_paths: List[str] ) -> Dict[str, Any]: """Legacy allowlisted individual-file commit retained for compatibility.""" operation_id = f"save_{int(time.time())}" logger.info(f"starting_atomic_save op={operation_id} sources={source_paths}") validated_sources = validate_backup_source_paths(source_paths) repo_info = self._repo_info() current_commit = getattr(repo_info, "sha", None) if repo_info else None backup_commit = self.create_backup(current_commit) parent_commit = backup_commit or current_commit with tempfile.TemporaryDirectory(prefix="hermes-state-save-") as tmpdir: state_dir = Path(tmpdir) / "state" state_dir.mkdir(parents=True, exist_ok=True) metadata = { "timestamp": datetime.now(timezone.utc).isoformat(), "operation_id": operation_id, "checksum": hashlib.sha256( json.dumps(state_data, sort_keys=True).encode("utf-8") ).hexdigest(), "backup_commit": backup_commit, "state_data": state_data, } metadata_path = state_dir / "metadata.json" metadata_path.write_text(json.dumps(metadata, indent=2), encoding="utf-8") operations = [ CommitOperationAdd( path_in_repo=f"{self.dataset_path}/metadata.json", path_or_fileobj=str(metadata_path), ) ] for source in validated_sources: destination = state_dir / source.name shutil.copy2(source, destination) operations.append( CommitOperationAdd( path_in_repo=f"{self.dataset_path}/{source.name}", path_or_fileobj=str(destination), ) ) info = self._call_with_retry( "create_commit", lambda: self.api.create_commit( repo_id=self.repo_id, repo_type="dataset", operations=operations, commit_message=f"Atomic state update - {operation_id}", parent_commit=parent_commit, ), ) return { "success": True, "operation_id": operation_id, "commit_id": info.oid, "backup_commit": backup_commit, "timestamp": metadata["timestamp"], "files_count": len(validated_sources), } def main() -> None: if len(sys.argv) < 3: print(json.dumps({ "error": "Usage: python save_to_dataset_atomic.py [source_path2...]", "status": "error", }, indent=2)) sys.exit(1) repo_id = sys.argv[1] source_paths = sys.argv[2:] for path in source_paths: if not os.path.exists(path): print(json.dumps({"error": f"Source path does not exist: {path}", "status": "error"}, indent=2)) sys.exit(1) state_data = { "environment": "production", "version": "1.0.0", "platform": "huggingface-spaces", "app": "hermesface", "timestamp": datetime.now(timezone.utc).isoformat(), } try: result = AtomicDatasetSaver(repo_id).save_state_atomic(state_data, source_paths) print(json.dumps(result, indent=2)) except Exception as exc: print(json.dumps({"error": str(exc), "status": "error"}, indent=2)) sys.exit(1) if __name__ == "__main__": main()