Files
tech-service-portal/app/media.py
T

68 lines
2.6 KiB
Python

"""Картинки портала: иконки оборудования, фото сотрудников, карта завода, фото к заданиям.
Хранятся в подкаталоге media/ каталога фото. Имя файла — случайное (hex), путь в БД проверяется регуляркой,
так что выйти за пределы каталога через подставленное значение нельзя."""
import re
from pathlib import Path
from fastapi import HTTPException, Request
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from . import settings_store as ss
MAGIC = [(b"\xff\xd8\xff", "image/jpeg", ".jpg"), (b"\x89PNG\r\n\x1a\n", "image/png", ".png")]
CT = {".jpg": "image/jpeg", ".png": "image/png", ".webp": "image/webp"}
NAME_RE = re.compile(r"[0-9a-f]{32}\.(jpg|png|webp)")
KINDS = ("equipment", "users", "map", "tasks")
def sniff(data: bytes):
for sig, ct, ext in MAGIC:
if data.startswith(sig):
return ct, ext
if data[:4] == b"RIFF" and data[8:12] == b"WEBP":
return "image/webp", ".webp"
return None
def root(db: Session, kind: str) -> Path:
assert kind in KINDS
p = ss.photo_root(db) / "media" / kind
p.mkdir(parents=True, exist_ok=True)
return p
async def read_image(db: Session, request: Request, max_mb: int | None = None) -> tuple[bytes, str, str]:
"""Тело запроса — сам файл (image/jpeg|png|webp), без multipart."""
limit = (max_mb or int(ss.get(db, "max_photo_mb"))) * 1024 * 1024
data = b""
async for chunk in request.stream():
data += chunk
if len(data) > limit:
raise HTTPException(413, f"Файл больше {limit // 1024 // 1024} МБ")
kind = sniff(data)
if not kind:
raise HTTPException(415, "Допустимы JPEG, PNG, WebP")
return data, kind[0], kind[1]
def save(db: Session, kind: str, data: bytes, ext: str) -> str:
name = ss.new_name() + ext
(root(db, kind) / name).write_bytes(data)
return name
def remove(db: Session, kind: str, name: str | None):
if name and NAME_RE.fullmatch(name):
(root(db, kind) / name).unlink(missing_ok=True)
def respond(db: Session, kind: str, name: str | None) -> FileResponse:
if not name or not NAME_RE.fullmatch(name):
raise HTTPException(404, "Картинка не загружена")
f = root(db, kind) / name
if not f.is_file():
raise HTTPException(404, "Файл отсутствует на сервере")
return FileResponse(f, media_type=CT[f.suffix], headers={"Cache-Control": "private, max-age=86400"})