feat:新增大屏存取发布接口
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user