"""Operational endpoints: diagnostics and backups (US07-05). Backups can be taken and verified here because both are safe, additive, and the operator needs them from the same screen that shows the disk filling up. **Restore is deliberately not an endpoint.** It replaces the state of the running application with an older one, so it belongs to a stopped installation and a person at a terminal: ``python -m photo_pipeline restore``. An HTTP call that can silently roll the library back to last week is a hole, not a feature. """ from __future__ import annotations from fastapi import APIRouter, Query, Request from fastapi.responses import JSONResponse from pydantic import BaseModel from photo_pipeline.services import diagnostics from photo_pipeline.services.backup import DEFAULT_KEEP, BackupError, BackupService router = APIRouter(tags=["operations"]) class CreateBackupRequest(BaseModel): reason: str = "manual" keep: int = DEFAULT_KEEP def _service(request: Request) -> BackupService: return BackupService(request.app.state.config) def _error(status: int, code: str, message: str) -> JSONResponse: return JSONResponse(status_code=status, content={"error": {"code": code, "message": message}}) @router.get("/diagnostics") def read_diagnostics(request: Request) -> dict: return diagnostics.report(request.app.state.config) @router.get("/backups") def list_backups(request: Request) -> dict: return {"backups": _service(request).list()} @router.post("/backups", status_code=201) def create_backup(body: CreateBackupRequest, request: Request): try: return _service(request).create(reason=body.reason, keep=body.keep) except BackupError as error: return _error(422, "backup_failed", str(error)) @router.get("/backups/{name}/verify") def verify_backup(name: str, request: Request): service = _service(request) # The name comes from the browser, so it names a backup — it is never joined # into a path until it has been matched against one that exists (US07-02). if name not in {entry["name"] for entry in service.list()}: return _error(404, "not_found", f"unknown backup {name}") return {"name": name, **service.verify(service.root / name).as_dict()} @router.post("/backups/prune") def prune_backups(request: Request, keep: int = Query(DEFAULT_KEEP, ge=1)): try: return {"removed": _service(request).prune(keep=keep)} except BackupError as error: return _error(422, "invalid_retention", str(error))