archive_purge.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. """Archive auto-purge endpoints (#1008 follow-up).
  2. Admin-only (``ARCHIVES_PURGE``). Provides:
  3. * ``GET /archives/purge/preview`` — live count for the admin slider
  4. * ``POST /archives/purge`` — one-shot manual bulk delete
  5. * ``GET/PUT /archives/purge/settings`` — auto-purge toggle + threshold
  6. """
  7. from __future__ import annotations
  8. import logging
  9. from fastapi import APIRouter, Depends, HTTPException, Query
  10. from sqlalchemy.ext.asyncio import AsyncSession
  11. from backend.app.core.auth import require_permission_if_auth_enabled
  12. from backend.app.core.database import get_db
  13. from backend.app.core.permissions import Permission
  14. from backend.app.models.user import User
  15. from backend.app.schemas.archive_purge import (
  16. ArchivePurgePreviewResponse,
  17. ArchivePurgeRequest,
  18. ArchivePurgeResponse,
  19. ArchivePurgeSettings,
  20. )
  21. from backend.app.services.archive_purge import (
  22. MAX_AUTO_PURGE_DAYS,
  23. MIN_AUTO_PURGE_DAYS,
  24. archive_purge_service,
  25. )
  26. logger = logging.getLogger(__name__)
  27. router = APIRouter(prefix="/archives", tags=["archives-purge"])
  28. @router.get("/purge/preview", response_model=ArchivePurgePreviewResponse)
  29. async def preview_archive_purge(
  30. older_than_days: int = Query(ge=1, le=3650),
  31. purge_stats: bool = Query(
  32. False,
  33. description=(
  34. "When False (default) the count reflects soft-delete mode — "
  35. "already-soft-deleted rows are excluded so the number matches "
  36. "what a fresh purge would actually touch. When True the count "
  37. "includes already-soft-deleted rows (eligible for promotion to "
  38. "hard-delete). #1390."
  39. ),
  40. ),
  41. db: AsyncSession = Depends(get_db),
  42. _: User | None = Depends(require_permission_if_auth_enabled(Permission.ARCHIVES_PURGE)),
  43. ):
  44. """Count + size of archives eligible for purge. Read-only."""
  45. result = await archive_purge_service.preview_purge(db, older_than_days=older_than_days, purge_stats=purge_stats)
  46. return ArchivePurgePreviewResponse(**result)
  47. @router.post("/purge", response_model=ArchivePurgeResponse)
  48. async def execute_archive_purge(
  49. body: ArchivePurgeRequest,
  50. db: AsyncSession = Depends(get_db),
  51. _: User | None = Depends(require_permission_if_auth_enabled(Permission.ARCHIVES_PURGE)),
  52. ):
  53. """Bulk-delete archives older than the threshold.
  54. Soft-delete by default (Quick Stats preserved). Set ``purge_stats=true``
  55. in the body to also drop the contribution from /stats — irreversible
  56. in that mode, same as the single-archive route's ``?purge_stats=true``.
  57. """
  58. deleted = await archive_purge_service.purge_older_than(
  59. db,
  60. older_than_days=body.older_than_days,
  61. purge_stats=body.purge_stats,
  62. )
  63. return ArchivePurgeResponse(deleted=deleted, purge_stats=body.purge_stats)
  64. @router.get("/purge/settings", response_model=ArchivePurgeSettings)
  65. async def get_archive_purge_settings(
  66. db: AsyncSession = Depends(get_db),
  67. _: User | None = Depends(require_permission_if_auth_enabled(Permission.ARCHIVES_PURGE)),
  68. ):
  69. cfg = await archive_purge_service.get_settings(db)
  70. return ArchivePurgeSettings(enabled=cfg["enabled"], days=cfg["days"], purge_stats=cfg["purge_stats"])
  71. @router.put("/purge/settings", response_model=ArchivePurgeSettings)
  72. async def update_archive_purge_settings(
  73. body: ArchivePurgeSettings,
  74. db: AsyncSession = Depends(get_db),
  75. _: User | None = Depends(require_permission_if_auth_enabled(Permission.ARCHIVES_PURGE)),
  76. ):
  77. if body.days < MIN_AUTO_PURGE_DAYS or body.days > MAX_AUTO_PURGE_DAYS:
  78. raise HTTPException(
  79. status_code=400,
  80. detail=f"days must be between {MIN_AUTO_PURGE_DAYS} and {MAX_AUTO_PURGE_DAYS}",
  81. )
  82. saved = await archive_purge_service.set_settings(
  83. db, enabled=body.enabled, days=body.days, purge_stats=body.purge_stats
  84. )
  85. return ArchivePurgeSettings(enabled=saved["enabled"], days=saved["days"], purge_stats=saved["purge_stats"])