| .. | .. |
|---|
| 1 | 1 | import json |
|---|
| 2 | +import os |
|---|
| 2 | 3 | from datetime import datetime, timezone |
|---|
| 3 | 4 | from typing import Any, AsyncGenerator |
|---|
| 4 | 5 | |
|---|
| 5 | 6 | from fastapi import APIRouter, Depends, HTTPException, Query |
|---|
| 6 | 7 | from fastapi.responses import StreamingResponse |
|---|
| 8 | +from starlette.responses import FileResponse |
|---|
| 7 | 9 | |
|---|
| 8 | 10 | from app.auth import verify_token |
|---|
| 9 | 11 | from app.ops_runner import ( |
|---|
| .. | .. |
|---|
| 304 | 306 | raise HTTPException(status_code=500, detail=f"Delete failed: {results}") |
|---|
| 305 | 307 | |
|---|
| 306 | 308 | return {"success": True, "project": project, "env": env, "name": name, "results": results} |
|---|
| 309 | + |
|---|
| 310 | + |
|---|
| 311 | +@router.get("/download/{project}/{env}/{name}", summary="Download a backup to browser") |
|---|
| 312 | +async def download_backup( |
|---|
| 313 | + project: str, |
|---|
| 314 | + env: str, |
|---|
| 315 | + name: str, |
|---|
| 316 | + _: str = Depends(verify_token), |
|---|
| 317 | +) -> FileResponse: |
|---|
| 318 | + """Serve a local backup file as a browser download.""" |
|---|
| 319 | + for param in (project, env, name): |
|---|
| 320 | + if "/" in param or "\\" in param or ".." in param: |
|---|
| 321 | + raise HTTPException(status_code=400, detail="Invalid path parameter") |
|---|
| 322 | + |
|---|
| 323 | + backup_path = f"/opt/data/backups/{project}/{env}/{name}" |
|---|
| 324 | + if not os.path.isfile(backup_path): |
|---|
| 325 | + raise HTTPException(status_code=404, detail="Backup file not found") |
|---|
| 326 | + |
|---|
| 327 | + media_type = "application/gzip" if name.endswith(".gz") else "application/octet-stream" |
|---|
| 328 | + return FileResponse( |
|---|
| 329 | + path=backup_path, |
|---|
| 330 | + media_type=media_type, |
|---|
| 331 | + filename=name, |
|---|
| 332 | + ) |
|---|