From 5d0247159b125bf035285d56c2b9bb58d6bb3029 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Thu, 26 Feb 2026 14:39:34 +0100
Subject: [PATCH] feat: backup download endpoint + skip-backup sync option

---
 app/routers/backups.py |   26 ++++++++++++++++++++++++++
 1 files changed, 26 insertions(+), 0 deletions(-)

diff --git a/app/routers/backups.py b/app/routers/backups.py
index badf0bf..2d84f30 100644
--- a/app/routers/backups.py
+++ b/app/routers/backups.py
@@ -1,9 +1,11 @@
 import json
+import os
 from datetime import datetime, timezone
 from typing import Any, AsyncGenerator
 
 from fastapi import APIRouter, Depends, HTTPException, Query
 from fastapi.responses import StreamingResponse
+from starlette.responses import FileResponse
 
 from app.auth import verify_token
 from app.ops_runner import (
@@ -304,3 +306,27 @@
         raise HTTPException(status_code=500, detail=f"Delete failed: {results}")
 
     return {"success": True, "project": project, "env": env, "name": name, "results": results}
+
+
+@router.get("/download/{project}/{env}/{name}", summary="Download a backup to browser")
+async def download_backup(
+    project: str,
+    env: str,
+    name: str,
+    _: str = Depends(verify_token),
+) -> FileResponse:
+    """Serve a local backup file as a browser download."""
+    for param in (project, env, name):
+        if "/" in param or "\\" in param or ".." in param:
+            raise HTTPException(status_code=400, detail="Invalid path parameter")
+
+    backup_path = f"/opt/data/backups/{project}/{env}/{name}"
+    if not os.path.isfile(backup_path):
+        raise HTTPException(status_code=404, detail="Backup file not found")
+
+    media_type = "application/gzip" if name.endswith(".gz") else "application/octet-stream"
+    return FileResponse(
+        path=backup_path,
+        media_type=media_type,
+        filename=name,
+    )

--
Gitblit v1.3.1