Files
mcma-backend/app/workers/tasks/cleanup_task.py
T
Цвылев Александр Вадимович a263272935 feat(api): finish Group A stubbed endpoints
Implements previously-stubbed /api/v1 endpoints (hexagonal: ports -> repos
-> services -> routers wired in deps):

- playlists: GET /{id}/cover (serves stored cover, 404 when absent)
- settings: GET/PATCH /settings + GET/PUT /settings/scrobbling — lazy
  per-user row, write-only Fernet-encrypted scrobble session key; adds
  user_settings table + migration (chains off dc126696f5a6)
- storage: GET /duplicates, /broken, /missing-metadata + admin POST
  /cleanup (arq cleanup_storage worker; reconciles local refs only,
  guarded against a storage-outage mass delete)
- admin: GET /services, /sources, /settings + POST /reindex; PATCH
  /settings and /sources/{source} return 501 (config is env-managed)

Adds NotSupportedError (-> HTTP 501). Integration tests for each surface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 14:33:12 +03:00

41 lines
1.8 KiB
Python

"""arq task: reconcile the catalogue against storage.
Scans every *local* track and drops rows whose backing file has vanished from
storage (a "dangling" reference). Remote placeholders have no local file yet and
are skipped by the repository query. The filesystem checks (one ``exists()`` per
track) are heavy, so this runs off the request cycle (CLAUDE.md). Guarded against
a storage outage: if an implausibly large share of files look missing, it assumes
the backend is down and aborts without deleting anything.
"""
from typing import Any
from app.core.logging import get_logger
from app.infrastructure.db import session_scope
from app.infrastructure.db.repositories import SqlAlchemyTrackRepository
from app.infrastructure.storage.provider import get_file_storage
log = get_logger("worker.cleanup")
# If more than this fraction of tracks look missing, assume the storage backend
# is unavailable (not that the library really evaporated) and refuse to delete.
_OUTAGE_GUARD_FRACTION = 0.5
async def cleanup_storage(_ctx: dict[str, Any]) -> dict[str, Any]:
storage = get_file_storage()
async with session_scope() as session:
tracks = SqlAlchemyTrackRepository(session)
refs = await tracks.all_storage_refs()
missing = [track_id for track_id, uri in refs if not await storage.exists(uri)]
if refs and len(missing) > len(refs) * _OUTAGE_GUARD_FRACTION:
log.warning("cleanup_aborted_outage_guard", scanned=len(refs), missing=len(missing))
return {"scanned": len(refs), "removed": 0, "aborted": True}
for track_id in missing:
await tracks.delete(track_id)
log.info("cleanup_done", scanned=len(refs), removed=len(missing))
return {"scanned": len(refs), "removed": len(missing), "aborted": False}