Портал технической службы: наряды и чек-листы, задачи, отказы и неисправности, анализ надёжности, карты, склады, приложение для Android
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
"""Картинки портала: иконки оборудования, фото сотрудников, карта завода, фото к заданиям.
|
||||
|
||||
Хранятся в подкаталоге 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"})
|
||||
Reference in New Issue
Block a user