Backup Agent commited on
Commit
5a5ae07
·
1 Parent(s): b9d9e0f

feat: turn backup daemon into FastAPI vault server with HTTPS PAT git remote

Browse files
Files changed (3) hide show
  1. Dockerfile +10 -9
  2. main.py +64 -89
  3. requirements.txt +4 -0
Dockerfile CHANGED
@@ -1,17 +1,18 @@
1
  FROM python:3.10-slim
2
 
3
- # Install system dependencies (specifically git and ssh client)
4
  RUN apt-get update && apt-get install -y --no-install-recommends \
5
- git openssh-client && \
6
  rm -rf /var/lib/apt/lists/*
7
 
8
- # HF uid 1000 compliance
9
- RUN useradd -m -u 1000 user
10
- USER user
11
- ENV HOME=/home/user
12
 
13
- WORKDIR $HOME/app
 
14
 
15
- COPY --chown=user main.py .
16
 
17
- CMD ["python", "main.py"]
 
 
 
 
1
  FROM python:3.10-slim
2
 
3
+ # Install system dependencies (specifically git)
4
  RUN apt-get update && apt-get install -y --no-install-recommends \
5
+ git && \
6
  rm -rf /var/lib/apt/lists/*
7
 
8
+ WORKDIR /app
 
 
 
9
 
10
+ COPY requirements.txt .
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
 
13
+ COPY main.py .
14
 
15
+ EXPOSE 7860
16
+ ENV PORT=7860
17
+
18
+ CMD ["uvicorn", "main.py:app", "--host", "0.0.0.0", "--port", "7860"]
main.py CHANGED
@@ -2,37 +2,23 @@
2
  import os
3
  import shutil
4
  import time
5
- import threading
6
  import subprocess
7
- import urllib.request
8
  from pathlib import Path
 
 
 
 
 
 
 
9
 
10
  # Load Configs
11
  SPACE_2_URL = os.environ.get("SPACE_2_URL", "https://augment17-claude-code-backend.hf.space")
12
  BACKEND_API_KEY = os.environ.get("BACKEND_API_KEY", "")
13
- BACKUP_GIT_REPO = os.environ.get("BACKUP_GIT_REPO", "") # SSH target e.g. git@github.com:augment17/my-backup.git
14
- SSH_PRIVATE_KEY = os.environ.get("SSH_PRIVATE_KEY", "") # Secret private key string
15
  BACKUP_LOCAL_DIR = "/tmp/git_backup_repo"
16
  MAX_PART_SIZE_MB = 50
17
- SYNC_INTERVAL_SECONDS = 300
18
-
19
- def setup_ssh():
20
- """Setup SSH private key for GitHub authentications if provided."""
21
- if not SSH_PRIVATE_KEY:
22
- print("[SSH] No SSH_PRIVATE_KEY env variable found. Using default git authentication...")
23
- return
24
-
25
- ssh_dir = Path("/home/user/.ssh") if os.path.exists("/home/user") else Path("/tmp/.ssh")
26
- ssh_dir.mkdir(parents=True, exist_ok=True)
27
-
28
- key_file = ssh_dir / "id_rsa"
29
- key_file.write_text(SSH_PRIVATE_KEY.strip() + "\n")
30
- key_file.chmod(0o600)
31
-
32
- # Configure git to use this key and ignore strict host checks
33
- ssh_cmd = f"ssh -i {key_file} -o StrictHostKeyChecking=no"
34
- os.environ["GIT_SSH_COMMAND"] = ssh_cmd
35
- print("[SSH] Private key configured successfully.")
36
 
37
  def run_cmd(cmd, cwd=None):
38
  try:
@@ -41,75 +27,64 @@ def run_cmd(cmd, cwd=None):
41
  except subprocess.CalledProcessError:
42
  return False
43
 
44
- def backup_loop():
45
- print("[Backup] Daemon started.")
46
- while True:
47
- try:
48
- # 1. Clean local backup directory
49
- if os.path.exists(BACKUP_LOCAL_DIR):
50
- shutil.rmtree(BACKUP_LOCAL_DIR)
51
- os.makedirs(BACKUP_LOCAL_DIR, exist_ok=True)
52
-
53
- # 2. Download zip from Space 2
54
- archive_path = Path("/tmp/workspace_backup.zip")
55
- if archive_path.exists():
56
- archive_path.unlink()
57
 
58
- download_url = f"{SPACE_2_URL}/api/backup/download"
59
- headers = {"Authorization": f"Bearer {BACKEND_API_KEY}"}
60
-
61
- print(f"[Backup] Downloading snapshot from {SPACE_2_URL}...")
62
- req = urllib.request.Request(download_url, headers=headers)
63
- with urllib.request.urlopen(req) as response:
64
- with open(archive_path, 'wb') as out_file:
65
- shutil.copyfileobj(response, out_file)
66
- print("[Backup] Download completed successfully.")
67
-
68
- # 3. Handle Split if needed
69
- target_dest = Path(BACKUP_LOCAL_DIR) / "workspace_backup.zip"
70
- if archive_path.stat().st_size > (MAX_PART_SIZE_MB * 1024 * 1024):
71
- print(f"[Backup] File exceeds {MAX_PART_SIZE_MB}MB. Splitting...")
72
- run_cmd(f"split -b {MAX_PART_SIZE_MB}M {archive_path} {target_dest}.part")
73
- else:
74
- shutil.copy(archive_path, target_dest)
75
 
76
- # Clean up local zip
 
 
77
  archive_path.unlink()
78
 
79
- # 4. Git init and force push
80
- run_cmd("git init", cwd=BACKUP_LOCAL_DIR)
81
- run_cmd("git config user.name 'Backup Agent'", cwd=BACKUP_LOCAL_DIR)
82
- run_cmd("git config user.email 'backup@agent.internal'", cwd=BACKUP_LOCAL_DIR)
83
- run_cmd(f"git remote add origin {BACKUP_GIT_REPO}", cwd=BACKUP_LOCAL_DIR)
84
- run_cmd("git checkout -b main", cwd=BACKUP_LOCAL_DIR)
85
- run_cmd("git add -A", cwd=BACKUP_LOCAL_DIR)
86
- run_cmd('git commit -m "Auto-backup: ' + time.strftime("%Y-%m-%d %H:%M:%S") + '"', cwd=BACKUP_LOCAL_DIR)
87
-
88
- success = run_cmd("git push origin main --force", cwd=BACKUP_LOCAL_DIR)
89
- if success:
90
- print("[Backup] Sync completed successfully (git history purged).")
91
- else:
92
- print("[Backup Error] Git push failed.")
93
- except Exception as e:
94
- print(f"[Backup Error] Unexpected error: {e}")
95
- time.sleep(SYNC_INTERVAL_SECONDS)
96
 
97
- def ping_loop():
98
- print("[Pinger] Daemon started.")
99
- while True:
100
- try:
101
- # Keep Space 2 awake
102
- health_url = f"{SPACE_2_URL}/health"
103
- req = urllib.request.Request(health_url)
104
- with urllib.request.urlopen(req) as response:
105
- status = response.status
106
- print(f"[Pinger] Pinged Space 2 health: Status {status}")
107
- except Exception as e:
108
- print(f"[Pinger Error] Failed to ping Space 2: {e}")
109
- time.sleep(SYNC_INTERVAL_SECONDS)
 
 
 
 
 
 
 
 
 
 
 
110
 
111
- if __name__ == "__main__":
112
- setup_ssh()
113
- # Start loops in separate threads
114
- threading.Thread(target=ping_loop, daemon=True).start()
115
- backup_loop()
 
2
  import os
3
  import shutil
4
  import time
 
5
  import subprocess
 
6
  from pathlib import Path
7
+ from typing import Optional
8
+
9
+ from fastapi import FastAPI, Header, HTTPException
10
+ from pydantic import BaseModel
11
+ import httpx
12
+
13
+ app = FastAPI(title="Space 5 — Backup Daemon & Vault Server")
14
 
15
  # Load Configs
16
  SPACE_2_URL = os.environ.get("SPACE_2_URL", "https://augment17-claude-code-backend.hf.space")
17
  BACKEND_API_KEY = os.environ.get("BACKEND_API_KEY", "")
18
+ GITHUB_TOKEN = "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"
19
+ BACKUP_GIT_REPO = f"https://oauth2:{GITHUB_TOKEN}@github.com/shyota1/llm-second-brain.git"
20
  BACKUP_LOCAL_DIR = "/tmp/git_backup_repo"
21
  MAX_PART_SIZE_MB = 50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  def run_cmd(cmd, cwd=None):
24
  try:
 
27
  except subprocess.CalledProcessError:
28
  return False
29
 
30
+ class VaultPushRequest(BaseModel):
31
+ project_name: Optional[str] = "unknown"
32
+ summary: Optional[str] = ""
 
 
 
 
 
 
 
 
 
 
33
 
34
+ @app.post("/api/vault/push")
35
+ async def vault_push(req: VaultPushRequest):
36
+ """
37
+ Exposes the push trigger called by Space 2 after successful iterations.
38
+ Downloads the workspace snapshot and pushes to GitHub backup repository.
39
+ """
40
+ print(f"[Vault] Received push request for project '{req.project_name}'")
41
+ try:
42
+ # 1. Clean local backup directory
43
+ if os.path.exists(BACKUP_LOCAL_DIR):
44
+ shutil.rmtree(BACKUP_LOCAL_DIR)
45
+ os.makedirs(BACKUP_LOCAL_DIR, exist_ok=True)
 
 
 
 
 
46
 
47
+ # 2. Download zip from Space 2
48
+ archive_path = Path("/tmp/workspace_backup.zip")
49
+ if archive_path.exists():
50
  archive_path.unlink()
51
 
52
+ download_url = f"{SPACE_2_URL}/api/backup/download"
53
+ headers = {"Authorization": f"Bearer {BACKEND_API_KEY}"}
54
+
55
+ print(f"[Vault] Downloading snapshot from {SPACE_2_URL}…")
56
+ async with httpx.AsyncClient(timeout=60.0) as client:
57
+ response = await client.get(download_url, headers=headers)
58
+ if response.status_code != 200:
59
+ raise HTTPException(status_code=500, detail=f"Download failed with status {response.status_code}")
60
+ archive_path.write_bytes(response.content)
61
+ print("[Vault] Download completed.")
 
 
 
 
 
 
 
62
 
63
+ # 3. Copy snapshot to backup folder
64
+ target_dest = Path(BACKUP_LOCAL_DIR) / "workspace_backup.zip"
65
+ shutil.copy(archive_path, target_dest)
66
+ archive_path.unlink()
67
+
68
+ # 4. Git init and push
69
+ run_cmd("git init", cwd=BACKUP_LOCAL_DIR)
70
+ run_cmd("git config user.name 'Vault Backup Agent'", cwd=BACKUP_LOCAL_DIR)
71
+ run_cmd("git config user.email 'vault@agent.internal'", cwd=BACKUP_LOCAL_DIR)
72
+ run_cmd(f"git remote add origin {BACKUP_GIT_REPO}", cwd=BACKUP_LOCAL_DIR)
73
+ run_cmd("git checkout -b backup", cwd=BACKUP_LOCAL_DIR)
74
+ run_cmd("git add -A", cwd=BACKUP_LOCAL_DIR)
75
+ run_cmd(f'git commit -m "Auto-backup {req.project_name}: {time.strftime("%Y-%m-%d %H:%M:%S")}"', cwd=BACKUP_LOCAL_DIR)
76
+
77
+ success = run_cmd("git push origin backup --force", cwd=BACKUP_LOCAL_DIR)
78
+ if success:
79
+ print("[Vault] Backup synced to GitHub successfully.")
80
+ return {"status": "success", "project_name": req.project_name}
81
+ else:
82
+ print("[Vault Error] Git push failed.")
83
+ raise HTTPException(status_code=500, detail="Git push failed")
84
+ except Exception as e:
85
+ print(f"[Vault Error] Backup failed: {e}")
86
+ raise HTTPException(status_code=500, detail=str(e))
87
 
88
+ @app.get("/health")
89
+ def health():
90
+ return {"status": "healthy", "service": "vault-daemon"}
 
 
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi==0.115.12
2
+ uvicorn[standard]==0.34.2
3
+ httpx==0.27.2
4
+ pydantic==2.8.2