"""Картинки портала: иконки оборудования, фото сотрудников, карта завода, фото к заданиям. Хранятся в подкаталоге 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"})