Files
RoleForge/app/services/avatar_storage.py
Sergey Antropoff b2d3b6b803 Профиль и аккаунт
- API и страницы профиля (редактирование, смена пароля, аватар), публичные карточки.
- Сайдбар: блок пользователя, пункт Users для admin/root, исправлен порядок
  инициализации (показ admin-only после initAuthSession, currentUser).
- GET /auth/me: ответ через ProfileMeResponse, исправлена валидация (is_founder bool).

Команды и роли
- Маршруты и UI команд; при редактировании роли: видимость Team, выбор команды
  в модалке, только команды с активным членством; API team_id в details/ update.
- GET /api/v1/teams?membership=active для списка «своих» команд.
- Форма роли: сегмент Team, панель выбора команды только при Team и не при
  с
2026-05-05 08:15:21 +03:00

81 lines
2.4 KiB
Python

"""Store originals and resized avatar images on disk (longest side 54 / 128 / 256)."""
from __future__ import annotations
import io
import shutil
from pathlib import Path
from PIL import Image, ImageOps
AVATAR_SIZES: tuple[int, ...] = (54, 128, 256)
MAX_AVATAR_BYTES = 5 * 1024 * 1024
ALLOWED_EXTENSIONS = frozenset({".jpg", ".jpeg", ".png", ".gif", ".webp"})
def avatar_dir(base: Path, user_id: str) -> Path:
return base / user_id
def normalize_extension(filename: str) -> str:
ext = Path(filename).suffix.lower()
if ext not in ALLOWED_EXTENSIONS:
return ".jpg"
return ext
def delete_avatar_files(base: Path, user_id: str) -> None:
d = avatar_dir(base, user_id)
if d.is_dir():
shutil.rmtree(d, ignore_errors=True)
def process_and_store_avatar(base: Path, user_id: str, raw: bytes, original_filename: str) -> str:
"""
Save original bytes and JPEG thumbnails for each size (square crop, center).
Returns stored original extension including dot (e.g. '.png').
"""
if len(raw) > MAX_AVATAR_BYTES:
raise ValueError("Avatar file is too large (max 5 MB).")
ext = normalize_extension(original_filename)
uid_dir = avatar_dir(base, user_id)
uid_dir.mkdir(parents=True, exist_ok=True)
original_path = uid_dir / f"original{ext}"
original_path.write_bytes(raw)
try:
im = Image.open(io.BytesIO(raw))
im.load()
except Exception as exc: # noqa: BLE001
raise ValueError("Invalid image file.") from exc
if im.mode not in ("RGB", "RGBA"):
im = im.convert("RGBA") if "A" in im.getbands() else im.convert("RGB")
for size in AVATAR_SIZES:
thumb = ImageOps.fit(im, (size, size), Image.Resampling.LANCZOS, centering=(0.5, 0.5))
out = thumb.convert("RGB")
out.save(uid_dir / f"{size}.jpg", "JPEG", quality=88, optimize=True)
return ext
def avatar_urls(user_id: str, avatar_ext: str | None, cache_key: str | None) -> dict[str, str | None]:
if not avatar_ext:
return {
"avatar_54": None,
"avatar_128": None,
"avatar_256": None,
"avatar_original": None,
}
v = f"?v={cache_key}" if cache_key else ""
b = f"/media/avatars/{user_id}"
return {
"avatar_54": f"{b}/54.jpg{v}",
"avatar_128": f"{b}/128.jpg{v}",
"avatar_256": f"{b}/256.jpg{v}",
"avatar_original": f"{b}/original{avatar_ext}{v}",
}