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