Matthias Nott
2026-02-21 68b89251bd42af5eea293b9302b78df0ed87a86f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from app.auth import verify_token
from app.ops_runner import run_ops, run_ops_json, _BACKUP_TIMEOUT
router = APIRouter()
@router.get("/", summary="List local backups")
async def list_backups(
    _: str = Depends(verify_token),
) -> list[dict[str, Any]]:
    """
    Returns a list of local backup records from `ops backups --json`.
    """
    result = await run_ops_json(["backups"])
    if not result["success"]:
        raise HTTPException(
            status_code=500,
            detail=f"Failed to list backups: {result['error']}",
        )
    data = result["data"]
    if isinstance(data, list):
        return data
    if isinstance(data, dict):
        for key in ("backups", "data", "items"):
            if key in data and isinstance(data[key], list):
                return data[key]
        return [data]
    return []
@router.get("/offsite", summary="List offsite backups")
async def list_offsite_backups(
    _: str = Depends(verify_token),
) -> list[dict[str, Any]]:
    """
    Returns a list of offsite backup records, querying each project separately.
    """
    all_backups = []
    for project in ["mdf", "seriousletter"]:
        result = await run_ops_json(["offsite", "list", project])
        if result["success"] and isinstance(result["data"], list):
            for b in result["data"]:
                b["project"] = project
            all_backups.extend(result["data"])
    return all_backups
@router.post("/{project}/{env}", summary="Create a local backup")
async def create_backup(
    project: str,
    env: str,
    _: str = Depends(verify_token),
) -> dict[str, Any]:
    """
    Runs `ops backup {project} {env}` and returns the result.
    """
    result = await run_ops(["backup", project, env], timeout=_BACKUP_TIMEOUT)
    if not result["success"]:
        raise HTTPException(
            status_code=500,
            detail=f"Backup failed: {result['error'] or result['output']}",
        )
    return {
        "success": True,
        "output": result["output"],
        "project": project,
        "env": env,
    }
@router.post("/offsite/upload/{project}/{env}", summary="Upload backup to offsite")
async def upload_offsite(
    project: str,
    env: str,
    _: str = Depends(verify_token),
) -> dict[str, Any]:
    """
    Runs `ops offsite upload {project} {env}` and returns the result.
    """
    result = await run_ops(
        ["offsite", "upload", project, env], timeout=_BACKUP_TIMEOUT
    )
    if not result["success"]:
        raise HTTPException(
            status_code=500,
            detail=f"Offsite upload failed: {result['error'] or result['output']}",
        )
    return {
        "success": True,
        "output": result["output"],
        "project": project,
        "env": env,
    }
@router.post("/offsite/retention", summary="Apply offsite retention policy")
async def apply_retention(
    _: str = Depends(verify_token),
) -> dict[str, Any]:
    """
    Runs `ops offsite retention` and returns the result.
    """
    result = await run_ops(["offsite", "retention"], timeout=_BACKUP_TIMEOUT)
    if not result["success"]:
        raise HTTPException(
            status_code=500,
            detail=f"Retention policy failed: {result['error'] or result['output']}",
        )
    return {
        "success": True,
        "output": result["output"],
    }