171 lines
5.6 KiB
Python
171 lines
5.6 KiB
Python
from __future__ import annotations
|
|
|
|
import imghdr
|
|
from io import BytesIO
|
|
from pathlib import Path
|
|
from tempfile import gettempdir
|
|
from time import time
|
|
from uuid import uuid4
|
|
|
|
from fastapi import UploadFile
|
|
from PIL import Image, ImageFilter
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.user import User
|
|
|
|
|
|
ALLOWED_IMAGE_TYPES = {"jpeg", "png"}
|
|
UPLOAD_ROOT = Path(__file__).resolve().parent.parent / "static" / "uploads"
|
|
MAX_PROFILE_IMAGES = 6
|
|
MAX_ACTIVITY_IMAGE_SIZE = 5 * 1024 * 1024
|
|
|
|
|
|
def _validate_image(content: bytes) -> str:
|
|
image_type = imghdr.what(None, h=content)
|
|
if image_type not in ALLOWED_IMAGE_TYPES:
|
|
raise ValueError("仅支持 jpg/png 图片")
|
|
return image_type
|
|
|
|
|
|
def _write_temp_file(content: bytes, suffix: str) -> str:
|
|
temp_path = Path(gettempdir()) / f"dating_{uuid4().hex}{suffix}"
|
|
temp_path.write_bytes(content)
|
|
return str(temp_path)
|
|
|
|
|
|
def _save_optimized_variants(content: bytes) -> tuple[str, str, str]:
|
|
_validate_image(content)
|
|
|
|
with Image.open(BytesIO(content)) as image:
|
|
image = image.convert("RGB")
|
|
image.thumbnail((1280, 1280))
|
|
|
|
main_buffer = BytesIO()
|
|
image.save(main_buffer, format="JPEG", quality=88)
|
|
main_path = _write_temp_file(main_buffer.getvalue(), ".jpg")
|
|
|
|
blur_image = image.copy().filter(ImageFilter.GaussianBlur(radius=15))
|
|
blur_buffer = BytesIO()
|
|
blur_image.save(blur_buffer, format="JPEG", quality=80)
|
|
blur_path = _write_temp_file(blur_buffer.getvalue(), "_blur.jpg")
|
|
|
|
timestamp = str(int(time()))
|
|
return main_path, blur_path, timestamp
|
|
|
|
|
|
def _persist_avatar_files(user_id: int, timestamp: str, main_path: str, blur_path: str) -> tuple[str, str]:
|
|
user_dir = UPLOAD_ROOT / "avatars" / str(user_id)
|
|
user_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
avatar_name = f"{timestamp}.jpg"
|
|
blur_name = f"{timestamp}_blur.jpg"
|
|
avatar_path = user_dir / avatar_name
|
|
blur_target_path = user_dir / blur_name
|
|
|
|
avatar_path.write_bytes(Path(main_path).read_bytes())
|
|
blur_target_path.write_bytes(Path(blur_path).read_bytes())
|
|
|
|
return avatar_name, blur_name
|
|
|
|
|
|
def _append_profile_image(existing_images: list[str] | None, avatar_url: str) -> list[str]:
|
|
items = list(existing_images or [])
|
|
items = [item for item in items if item and item != avatar_url]
|
|
items.insert(0, avatar_url)
|
|
return items[:MAX_PROFILE_IMAGES]
|
|
|
|
|
|
async def upload_user_avatar(session: AsyncSession, user: User, file: UploadFile) -> dict:
|
|
content = await file.read()
|
|
_validate_upload_content(content)
|
|
|
|
main_path, blur_path, timestamp = _save_optimized_variants(content)
|
|
|
|
try:
|
|
avatar_name, blur_name = _persist_avatar_files(user.id, timestamp, main_path, blur_path)
|
|
avatar_url = f"/static/uploads/avatars/{user.id}/{avatar_name}"
|
|
blur_url = f"/static/uploads/avatars/{user.id}/{blur_name}"
|
|
|
|
user.avatar_url = avatar_url
|
|
user.avatar_blur_url = blur_url
|
|
user.profile_images = _append_profile_image(user.profile_images, avatar_url)
|
|
|
|
session.add(user)
|
|
await session.commit()
|
|
await session.refresh(user)
|
|
|
|
return {
|
|
"avatar_url": user.avatar_url,
|
|
"avatar_blur_url": user.avatar_blur_url,
|
|
"profile_images": user.profile_images or [],
|
|
"local_files": {
|
|
"avatar": avatar_name,
|
|
"blur": blur_name,
|
|
},
|
|
}
|
|
finally:
|
|
Path(main_path).unlink(missing_ok=True)
|
|
Path(blur_path).unlink(missing_ok=True)
|
|
|
|
|
|
def _validate_upload_content(content: bytes) -> None:
|
|
if not content:
|
|
raise ValueError("上传文件不能为空")
|
|
if len(content) > MAX_ACTIVITY_IMAGE_SIZE:
|
|
raise ValueError("图片大小不能超过 5MB")
|
|
|
|
|
|
def _store_generic_image(content: bytes, folder: str, filename: str) -> str:
|
|
_validate_image(content)
|
|
target_dir = UPLOAD_ROOT / folder
|
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
target_path = target_dir / filename
|
|
with Image.open(BytesIO(content)) as image:
|
|
image = image.convert("RGB")
|
|
image.thumbnail((1600, 1600))
|
|
image.save(target_path, format="JPEG", quality=88)
|
|
return str(target_path)
|
|
|
|
|
|
async def upload_activity_image(file: UploadFile, *, folder: str = "activities") -> dict:
|
|
content = await file.read()
|
|
_validate_upload_content(content)
|
|
|
|
timestamp = str(int(time()))
|
|
filename = f"{timestamp}_{uuid4().hex}.jpg"
|
|
stored_path = _store_generic_image(content, folder, filename)
|
|
url_path = f"/static/uploads/{folder}/{filename}"
|
|
return {
|
|
"image_url": url_path,
|
|
"local_file": filename,
|
|
"stored_path": stored_path,
|
|
}
|
|
|
|
|
|
async def upload_user_profile_image(session: AsyncSession, user: User, file: UploadFile) -> dict:
|
|
content = await file.read()
|
|
_validate_upload_content(content)
|
|
|
|
main_path, blur_path, timestamp = _save_optimized_variants(content)
|
|
|
|
try:
|
|
avatar_name, _ = _persist_avatar_files(user.id, f"profile_{timestamp}", main_path, blur_path)
|
|
image_url = f"/static/uploads/avatars/{user.id}/{avatar_name}"
|
|
user.profile_images = _append_profile_image(user.profile_images, image_url)
|
|
if not user.avatar_url:
|
|
user.avatar_url = image_url
|
|
if not user.avatar_blur_url:
|
|
user.avatar_blur_url = image_url
|
|
|
|
session.add(user)
|
|
await session.commit()
|
|
await session.refresh(user)
|
|
|
|
return {
|
|
"image_url": image_url,
|
|
"profile_images": user.profile_images or [],
|
|
}
|
|
finally:
|
|
Path(main_path).unlink(missing_ok=True)
|
|
Path(blur_path).unlink(missing_ok=True)
|