feat:新增大屏存取发布接口

This commit is contained in:
2026-10-11 08:27:30 -07:00
parent dadd797391
commit afc07ba695
23 changed files with 2672 additions and 36 deletions
+63
View File
@@ -0,0 +1,63 @@
"""大屏封面图:放本地磁盘,文件名就是封面 ID;每次上传换新 ID,旧图删掉
图片类 URL 要能被 <img src> 匿名访问,靠不可猜的 ID 兜底
"""
import re
import secrets
from pathlib import Path
from app.core.exceptions import BizException
MAX_BYTES = 1024 * 1024
# 按文件头判断类型,不信任客户端声明的 Content-Type
MAGIC = {b"\x89PNG\r\n\x1a\n": "image/png", b"\xff\xd8\xff": "image/jpeg"}
# ID 进了文件路径,读写前必须过这个格式检查,防路径穿越
ID_PATTERN = re.compile(r"th_[A-Za-z0-9_-]{16,64}")
def sniff(head: bytes) -> str | None:
for magic, media_type in MAGIC.items():
if head.startswith(magic):
return media_type
return None
class ThumbnailStorage:
def __init__(self, root: Path):
self._root = root
def _path(self, thumbnail_id: str) -> Path | None:
if not ID_PATTERN.fullmatch(thumbnail_id):
return None
return self._root / thumbnail_id
def save(self, data: bytes) -> str:
if len(data) > MAX_BYTES:
raise BizException(413, "封面图不能超过 1 MB")
if sniff(data[:8]) is None:
raise BizException(422, "封面图只支持 PNG 或 JPEG")
thumbnail_id = f"th_{secrets.token_urlsafe(16)}"
self._root.mkdir(parents=True, exist_ok=True)
self._path(thumbnail_id).write_bytes(data)
return thumbnail_id
def open(self, thumbnail_id: str) -> tuple[Path, str] | None:
"""文件路径和图片类型;ID 不合法、文件不存在或内容不是图片时返回 None"""
path = self._path(thumbnail_id)
if path is None or not path.is_file():
return None
with path.open("rb") as file:
media_type = sniff(file.read(8))
return None if media_type is None else (path, media_type)
def copy(self, thumbnail_id: str) -> str | None:
path = self._path(thumbnail_id)
if path is None or not path.is_file():
return None
return self.save(path.read_bytes())
def delete(self, thumbnail_id: str | None) -> None:
path = None if thumbnail_id is None else self._path(thumbnail_id)
if path is not None:
path.unlink(missing_ok=True)