feat: burn-after-read and expiry cleaner

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
肉末
2026-07-14 12:53:58 +08:00
parent 175a4ad0d1
commit 4d697692e0
3 changed files with 131 additions and 2 deletions
+24 -2
View File
@@ -1,11 +1,25 @@
import asyncio
from contextlib import asynccontextmanager
from fastapi import FastAPI
from app.api.public import router as public_router
from app.config import settings
from app.db import Base, engine
from app.db import Base, SessionLocal, engine
from app import models # noqa: F401 — register models with Base.metadata
from app.services import cleaner as cleaner_service
async def _cleaner_loop() -> None:
while True:
db = SessionLocal()
try:
cleaner_service.run_once(db)
except Exception:
pass
finally:
db.close()
await asyncio.sleep(60)
@asynccontextmanager
@@ -13,7 +27,15 @@ async def lifespan(_app: FastAPI):
settings.data_dir.mkdir(parents=True, exist_ok=True)
settings.uploads_dir.mkdir(parents=True, exist_ok=True)
Base.metadata.create_all(bind=engine)
yield
task = asyncio.create_task(_cleaner_loop())
try:
yield
finally:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
app = FastAPI(title="MincedPad", lifespan=lifespan)
+35
View File
@@ -0,0 +1,35 @@
"""Periodic cleanup of expired and burned items."""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import or_, select
from sqlalchemy.orm import Session
from app.models import Item
from app.services import items as items_service
def run_once(db: Session, *, now: datetime | None = None) -> int:
"""Delete expired rows (+ files) and burned rows (leftover files).
Returns the number of item rows removed.
"""
now = now or datetime.utcnow()
rows = list(
db.scalars(
select(Item).where(
or_(
Item.burned.is_(True),
(Item.expires_at.is_not(None) & (Item.expires_at <= now)),
)
)
).all()
)
for item in rows:
items_service.delete_item_file(item)
db.delete(item)
if rows:
db.commit()
return len(rows)