feat(backend): модуль «замена фона» и хранилище своих картинок
Отключаемый в админке модуль `virtual_background` (дефолт — выключен, чтобы
обновление не меняло продукт у тех, кто ничего не просил). Флаг едет клиенту
двумя путями: на публичные страницы входа — через `GET /public/settings`,
участнику комнаты — в join-ответе (`JoinOut`), потому что значение нужно на
руках ДО первого рендера комнаты, а `/admin/settings` доступен только админу.
Свои картинки пользователя (`/users/me/backgrounds`, GET/POST/DELETE):
файлы на диске (`backgrounds/{user_id}/{id}.{ext}`), в БД только путь — как у
аватаров, «чтобы не грузили БД». Лимит в 10 штук проверяется на сервере под
блокировкой строки пользователя: две одновременные загрузки иначе обе увидели
бы «уже девять» и обе прошли бы. Удаление сносит и запись, и файл; чужую
картинку по её id удалить нельзя — владелец в условии запроса.
Валидация загрузки (допустимые форматы, магические байты, реальный размер)
выделена из `services/avatars.py` в общий `services/images.py`: правила у
аватара и фона одни и те же, а разъехавшись, они дали бы дыру ровно там, ради
чего проверка и написана. Публичный API аватаров не изменился.
Сжимает картинку клиент (Pillow на бэкенде нет), но серверная валидация
остаётся полноценной — запрос может прийти и мимо интерфейса.
Новый ключ настройки вписан в `_MANAGED_KEYS` тестов: без этого включённый
в общей dev-БД модуль ронял чужие тесты, которые считают себя изолированными.
This commit is contained in:
45
backend/models/user_background.py
Normal file
45
backend/models/user_background.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""Модель UserBackground — своя картинка пользователя для замены фона видео.
|
||||
|
||||
В БД хранится только путь к файлу относительно `MEDIA_ROOT`
|
||||
(`backgrounds/{user_id}/{background_id}.{ext}`) — ровно тот же приём, что и у
|
||||
аватаров (`users.avatar_path`, `services/avatars.py`): «чтобы не грузили БД»
|
||||
(требование оператора). Сами файлы лежат на диске в томе `media`, который
|
||||
nginx раздаёт напрямую по `location /media/`.
|
||||
|
||||
Лимит на число картинок (`MAX_BACKGROUNDS_PER_USER`) проверяется в сервисе, а
|
||||
не ограничением БД: он про политику продукта, а не про целостность данных, и
|
||||
администратор может захотеть его поменять.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, String, func, text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from models.base import Base
|
||||
|
||||
|
||||
class UserBackground(Base):
|
||||
"""Загруженная пользователем картинка фона."""
|
||||
|
||||
__tablename__ = "user_backgrounds"
|
||||
__table_args__ = (
|
||||
# Выборка всегда одна и та же — «все фоны этого пользователя, свежие
|
||||
# сверху» (`services/backgrounds.py::list_backgrounds`), и она же
|
||||
# считает лимит при загрузке.
|
||||
Index("ix_user_backgrounds_user_created", "user_id", "created_at"),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()")
|
||||
)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
# Путь относительно `MEDIA_ROOT`: `backgrounds/{user_id}/{id}.{ext}`.
|
||||
path: Mapped[str] = mapped_column(String(512), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
Reference in New Issue
Block a user