- Conversion `async def` → `def` de TOUTES les routes dont le corps ne contient ni `await`, ni `async with`, ni `async for`, ni `asyncio` (scan automatique corps par corps sur app/ : 352 converties, 0 dangereuses, vérifié `asyncio`/`run_coroutine`/`.result()` absents). FastAPI exécute ces handlers dans son threadpool → tout leur SQLite (`get_conn()` + `conn.execute`) quitte l'event loop, sans changer une ligne de logique. - Répartition : api_v2 60, dashboard 40, collections 25, board 23, workspace 19, wiki 17, permissions 14, api 14, main.py 6, + 35 fichiers. - Les 4 routers prioritaires de l'audit sont couverts par ce lot : api_v2 60 + dashboard 40 + collections 25 + board 23 = 148 conversions (le reste de leurs routes attend la phase 2 : elles ont de vrais `await`). - Reste (phase 2) : les 311 routes avec de vrais `await` → enrouler les blocs DB dans `await anyio.to_thread.run_sync(...)` ; pas de wrapper partagé livré (rien ne l'appellerait — YAGNI jusqu'au premier usage). suite **1037/1037** (233 s) · `ruff check app tests` OK · docs à jour
101 lines
3.5 KiB
Python
101 lines
3.5 KiB
Python
"""FlowDeck — custom workspace emojis (v5.6.0).
|
|
|
|
Uploaded emoji images stored per-workspace and usable as page icons. Kept on a
|
|
prefix-less router so the paths stay ``/api/custom-emojis`` (the board router's
|
|
``/{owner}/{repo}`` HTML catch-all would otherwise shadow them).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import datetime
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, HTTPException, Request
|
|
|
|
from app.config import settings
|
|
from app.db import get_conn
|
|
|
|
router = APIRouter(tags=["emojis"])
|
|
|
|
_IMAGE_EXTS = {"png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "ico", "avif"}
|
|
|
|
|
|
def _upload_root() -> Path:
|
|
|
|
return Path(settings.data_dir)
|
|
|
|
|
|
def _active_ws(request: Request) -> int:
|
|
"""Workspace id from the active-workspace cookie, fallback 1."""
|
|
try:
|
|
ws_id = int(request.cookies.get("flowdeck_workspace", "") or 0)
|
|
if ws_id > 0:
|
|
return ws_id
|
|
except (ValueError, TypeError):
|
|
pass
|
|
return 1
|
|
|
|
|
|
@router.get("/api/custom-emojis")
|
|
def list_custom_emojis(request: Request):
|
|
"""List the current workspace's custom emojis."""
|
|
ws_id = _active_ws(request)
|
|
with get_conn() as conn:
|
|
rows = conn.execute(
|
|
"SELECT id, name, url FROM custom_emojis WHERE workspace_id=? ORDER BY created_at DESC",
|
|
(ws_id,),
|
|
).fetchall()
|
|
return {"status": "ok", "emojis": [dict(r) for r in rows]}
|
|
|
|
|
|
@router.post("/api/custom-emojis")
|
|
async def create_custom_emoji(request: Request):
|
|
"""Upload a custom emoji image (multipart: ``name`` + ``file``)."""
|
|
form = await request.form()
|
|
name = (form.get("name") or "").strip()[:40] or "emoji"
|
|
upload = form.get("file")
|
|
if upload is None or not hasattr(upload, "filename"):
|
|
raise HTTPException(400, "file field required")
|
|
original = (upload.filename or "emoji.png").replace("\\", "/").rsplit("/", 1)[-1]
|
|
safe = re.sub(r"[^A-Za-z0-9._-]", "_", original)[:80]
|
|
ext = safe.rsplit(".", 1)[-1].lower() if "." in safe else "png"
|
|
if ext not in _IMAGE_EXTS:
|
|
raise HTTPException(400, "Unsupported image format")
|
|
ws_id = _active_ws(request)
|
|
stamp = datetime.datetime.now(datetime.UTC).replace(tzinfo=None).strftime("%Y%m%d%H%M%S%f")
|
|
folder = _upload_root() / f"uploads/workspace_{ws_id}"
|
|
folder.mkdir(parents=True, exist_ok=True)
|
|
final = f"emoji_{stamp}_{safe}"
|
|
(folder / final).write_bytes(await upload.read())
|
|
url = f"/api/files/{ws_id}/{final}"
|
|
with get_conn() as conn:
|
|
cur = conn.execute(
|
|
"INSERT INTO custom_emojis (workspace_id, name, url) VALUES (?, ?, ?)",
|
|
(ws_id, name, url),
|
|
)
|
|
conn.commit()
|
|
emoji_id = cur.lastrowid
|
|
return {"status": "ok", "emoji": {"id": emoji_id, "name": name, "url": url}}
|
|
|
|
|
|
@router.delete("/api/custom-emojis/{emoji_id}")
|
|
def delete_custom_emoji(request: Request, emoji_id: int):
|
|
"""Delete a custom emoji (and its stored file)."""
|
|
ws_id = _active_ws(request)
|
|
with get_conn() as conn:
|
|
row = conn.execute(
|
|
"SELECT url FROM custom_emojis WHERE id=? AND workspace_id=?",
|
|
(emoji_id, ws_id),
|
|
).fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "emoji not found")
|
|
conn.execute("DELETE FROM custom_emojis WHERE id=?", (emoji_id,))
|
|
conn.commit()
|
|
try:
|
|
fname = (row["url"] or "").rsplit("/", 1)[-1]
|
|
if fname:
|
|
(_upload_root() / f"uploads/workspace_{ws_id}" / fname).unlink(missing_ok=True)
|
|
except OSError:
|
|
pass
|
|
return {"status": "ok", "id": emoji_id}
|