- FastAPI: preset assets 01–08, GET list/static, PUT /me/avatar/preset, safer uploaded-avatar path validation, preset_avatars + HTTP tests. - Expo: personal-info (library + presets), profile tab avatar, resolveApiMediaUrl, auth hooks cache sync, Web multipart helper, partial-save messaging + profile i18n. - Includes existing edits to conversation screen and voice use-player. Co-authored-by: Cursor <cursoragent@cursor.com>
69 lines
2.1 KiB
Python
69 lines
2.1 KiB
Python
"""服务端托管的预设头像(白名单文件名)。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
|
||
_PRESETS_DIR = Path(__file__).resolve().parent / "avatar_presets"
|
||
|
||
# 与仓库内静态文件一致:01.png … 08.png
|
||
ALLOWED_PRESET_FILENAMES: frozenset[str] = frozenset(f"{i:02d}.png" for i in range(1, 9))
|
||
|
||
PRESET_IDS: tuple[str, ...] = tuple(f"{i:02d}" for i in range(1, 9))
|
||
|
||
|
||
def preset_filename_for_id(preset_id: str) -> str | None:
|
||
"""preset_id 形如 \"01\",返回 \"01.png\";非法则 None。"""
|
||
stripped = preset_id.strip()
|
||
name = f"{stripped}.png"
|
||
if name in ALLOWED_PRESET_FILENAMES:
|
||
return name
|
||
return None
|
||
|
||
|
||
def avatar_url_for_preset_filename(filename: str) -> str:
|
||
return f"/api/auth/avatar-presets/{filename}"
|
||
|
||
|
||
def list_preset_items() -> list[tuple[str, str]]:
|
||
"""(preset_id, avatar_url) 列表,供 GET /avatar-presets。"""
|
||
return [
|
||
(pid, avatar_url_for_preset_filename(f"{pid}.png")) for pid in PRESET_IDS
|
||
]
|
||
|
||
|
||
def preset_file_path(filename: str) -> Path | None:
|
||
if filename not in ALLOWED_PRESET_FILENAMES:
|
||
return None
|
||
path = (_PRESETS_DIR / filename).resolve()
|
||
try:
|
||
path.relative_to(_PRESETS_DIR.resolve())
|
||
except ValueError:
|
||
return None
|
||
return path
|
||
|
||
|
||
def _avatar_upload_stem_allowed(stem: str) -> bool:
|
||
"""允许 UUID、数字 ID、含连字符/下划线的安全文件名主干。"""
|
||
if not stem or len(stem) > 128:
|
||
return False
|
||
return all(c.isalnum() or c in "-_" for c in stem)
|
||
|
||
|
||
def safe_avatar_upload_path(filename: str, avatar_dir: Path) -> Path | None:
|
||
"""用户上传头像文件名形如 {user_id}.jpg,防路径穿越。"""
|
||
if "/" in filename or "\\" in filename or ".." in filename:
|
||
return None
|
||
if not filename.endswith(".jpg"):
|
||
return None
|
||
stem = filename[:-4]
|
||
if not _avatar_upload_stem_allowed(stem):
|
||
return None
|
||
base = avatar_dir.resolve()
|
||
path = (avatar_dir / filename).resolve()
|
||
try:
|
||
path.relative_to(base)
|
||
except ValueError:
|
||
return None
|
||
return path
|