Портал технической службы: наряды и чек-листы, задачи, отказы и неисправности, анализ надёжности, карты, склады, приложение для Android

This commit is contained in:
darkorion
2026-10-13 09:09:53 +03:00
commit c298f5a830
247 changed files with 23200 additions and 0 deletions
+67
View File
@@ -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"})