| |
| import os |
| import shutil |
| import time |
| import subprocess |
| from pathlib import Path |
| from typing import Optional |
|
|
| from fastapi import FastAPI, Header, HTTPException |
| from pydantic import BaseModel |
| import httpx |
|
|
| app = FastAPI(title="Space 5 — Backup Daemon & Vault Server") |
|
|
| |
| SPACE_2_URL = os.environ.get("SPACE_2_URL", "https://augment17-claude-code-backend.hf.space") |
| BACKEND_API_KEY = os.environ.get("BACKEND_API_KEY", "") |
| GITHUB_TOKEN = "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" |
| BACKUP_GIT_REPO = f"https://oauth2:{GITHUB_TOKEN}@github.com/shyota1/llm-second-brain.git" |
| BACKUP_LOCAL_DIR = "/tmp/git_backup_repo" |
| MAX_PART_SIZE_MB = 50 |
|
|
| def run_cmd(cmd, cwd=None): |
| try: |
| subprocess.run(cmd, shell=True, check=True, cwd=cwd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) |
| return True |
| except subprocess.CalledProcessError: |
| return False |
|
|
| class VaultPushRequest(BaseModel): |
| project_name: Optional[str] = "unknown" |
| summary: Optional[str] = "" |
|
|
| @app.post("/api/vault/push") |
| async def vault_push(req: VaultPushRequest): |
| """ |
| Exposes the push trigger called by Space 2 after successful iterations. |
| Downloads the workspace snapshot and pushes to GitHub backup repository. |
| """ |
| print(f"[Vault] Received push request for project '{req.project_name}'") |
| try: |
| |
| if os.path.exists(BACKUP_LOCAL_DIR): |
| shutil.rmtree(BACKUP_LOCAL_DIR) |
| os.makedirs(BACKUP_LOCAL_DIR, exist_ok=True) |
|
|
| |
| archive_path = Path("/tmp/workspace_backup.zip") |
| if archive_path.exists(): |
| archive_path.unlink() |
|
|
| download_url = f"{SPACE_2_URL}/api/backup/download" |
| headers = {"Authorization": f"Bearer {BACKEND_API_KEY}"} |
| |
| print(f"[Vault] Downloading snapshot from {SPACE_2_URL}…") |
| async with httpx.AsyncClient(timeout=60.0) as client: |
| response = await client.get(download_url, headers=headers) |
| if response.status_code != 200: |
| raise HTTPException(status_code=500, detail=f"Download failed with status {response.status_code}") |
| archive_path.write_bytes(response.content) |
| print("[Vault] Download completed.") |
|
|
| |
| target_dest = Path(BACKUP_LOCAL_DIR) / "workspace_backup.zip" |
| shutil.copy(archive_path, target_dest) |
| archive_path.unlink() |
|
|
| |
| run_cmd("git init", cwd=BACKUP_LOCAL_DIR) |
| run_cmd("git config user.name 'Vault Backup Agent'", cwd=BACKUP_LOCAL_DIR) |
| run_cmd("git config user.email 'vault@agent.internal'", cwd=BACKUP_LOCAL_DIR) |
| run_cmd(f"git remote add origin {BACKUP_GIT_REPO}", cwd=BACKUP_LOCAL_DIR) |
| run_cmd("git checkout -b backup", cwd=BACKUP_LOCAL_DIR) |
| run_cmd("git add -A", cwd=BACKUP_LOCAL_DIR) |
| run_cmd(f'git commit -m "Auto-backup {req.project_name}: {time.strftime("%Y-%m-%d %H:%M:%S")}"', cwd=BACKUP_LOCAL_DIR) |
| |
| success = run_cmd("git push origin backup --force", cwd=BACKUP_LOCAL_DIR) |
| if success: |
| print("[Vault] Backup synced to GitHub successfully.") |
| return {"status": "success", "project_name": req.project_name} |
| else: |
| print("[Vault Error] Git push failed.") |
| raise HTTPException(status_code=500, detail="Git push failed") |
| except Exception as e: |
| print(f"[Vault Error] Backup failed: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| @app.get("/health") |
| def health(): |
| return {"status": "healthy", "service": "vault-daemon"} |
|
|