feat: storage management — disk usage, cleanup API, scheduled cleanup task
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
|
from app.api.deps import get_current_user
|
||||||
|
from app.models.user import User, UserRole
|
||||||
|
from app.services.storage import get_disk_usage
|
||||||
|
from app.tasks.cleanup import cleanup_old_data_task
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/settings", tags=["settings"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/storage")
|
||||||
|
async def storage_info(user: User = Depends(get_current_user)):
|
||||||
|
return get_disk_usage()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/cleanup")
|
||||||
|
async def trigger_cleanup(
|
||||||
|
retention_days: int | None = None,
|
||||||
|
user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
if user.role != UserRole.admin:
|
||||||
|
raise HTTPException(403, "Admin access required")
|
||||||
|
cleanup_old_data_task.delay(retention_days)
|
||||||
|
return {"message": "Cleanup started"}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.config import settings
|
||||||
|
from app.models.export_file import ExportFile
|
||||||
|
from app.models.recording import Recording
|
||||||
|
from app.models.transcription import Transcription
|
||||||
|
|
||||||
|
|
||||||
|
def get_disk_usage() -> dict:
|
||||||
|
upload_size = _dir_size(settings.upload_dir)
|
||||||
|
export_size = _dir_size(settings.export_dir)
|
||||||
|
return {
|
||||||
|
"uploads_bytes": upload_size,
|
||||||
|
"exports_bytes": export_size,
|
||||||
|
"total_bytes": upload_size + export_size,
|
||||||
|
"total_gb": round((upload_size + export_size) / (1024 ** 3), 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _dir_size(path: str) -> int:
|
||||||
|
total = 0
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return 0
|
||||||
|
for dirpath, _, filenames in os.walk(path):
|
||||||
|
for f in filenames:
|
||||||
|
fp = os.path.join(dirpath, f)
|
||||||
|
total += os.path.getsize(fp)
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_old_data(db: Session, retention_days: int | None = None):
|
||||||
|
days = retention_days or settings.retention_days
|
||||||
|
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
|
||||||
|
|
||||||
|
old_recordings = db.execute(
|
||||||
|
select(Recording).where(Recording.created_at < cutoff)
|
||||||
|
).scalars().all()
|
||||||
|
|
||||||
|
deleted_count = 0
|
||||||
|
for recording in old_recordings:
|
||||||
|
if os.path.exists(recording.file_path):
|
||||||
|
os.remove(recording.file_path)
|
||||||
|
if recording.transcription:
|
||||||
|
for protocol in recording.transcription.protocols:
|
||||||
|
for export_file in protocol.export_files:
|
||||||
|
if os.path.exists(export_file.file_path):
|
||||||
|
os.remove(export_file.file_path)
|
||||||
|
db.delete(recording)
|
||||||
|
deleted_count += 1
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
return {"deleted_recordings": deleted_count}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from app.tasks.celery_app import celery_app
|
||||||
|
from app.tasks.transcription import SyncSession
|
||||||
|
from app.services.storage import cleanup_old_data
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app.task(name="app.tasks.cleanup.cleanup_old_data")
|
||||||
|
def cleanup_old_data_task(retention_days: int | None = None):
|
||||||
|
with SyncSession() as db:
|
||||||
|
result = cleanup_old_data(db, retention_days)
|
||||||
|
return result
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from app.services.storage import get_disk_usage, _dir_size
|
||||||
|
|
||||||
|
|
||||||
|
def test_dir_size_empty(tmp_path):
|
||||||
|
assert _dir_size(str(tmp_path)) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_dir_size_with_files(tmp_path):
|
||||||
|
f = tmp_path / "test.bin"
|
||||||
|
f.write_bytes(b"\x00" * 1024)
|
||||||
|
assert _dir_size(str(tmp_path)) == 1024
|
||||||
|
|
||||||
|
|
||||||
|
def test_dir_size_nonexistent():
|
||||||
|
assert _dir_size("/nonexistent/path/12345") == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_disk_usage():
|
||||||
|
with patch("app.services.storage._dir_size", return_value=1024 * 1024 * 512):
|
||||||
|
result = get_disk_usage()
|
||||||
|
assert result["total_bytes"] == 1024 * 1024 * 512 * 2
|
||||||
|
assert "total_gb" in result
|
||||||
Reference in New Issue
Block a user