chore(deps): update Python version requirement and dependencies

This commit is contained in:
shiyu
2025-12-25 15:48:28 +08:00
parent d88e95a9af
commit ff1c06ad18
7 changed files with 393 additions and 668 deletions

View File

@@ -1 +1 @@
3.13
3.14

View File

@@ -5,11 +5,11 @@ from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Annotated
import bcrypt
import jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jwt.exceptions import InvalidTokenError
from passlib.context import CryptContext
from domain.auth.types import (
PasswordResetConfirm,
@@ -97,12 +97,15 @@ class PasswordResetStore:
class AuthService:
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
algorithm = ALGORITHM
access_token_expire_minutes = ACCESS_TOKEN_EXPIRE_MINUTES
password_reset_token_expire_minutes = PASSWORD_RESET_TOKEN_EXPIRE_MINUTES
@staticmethod
def _to_bytes(value: str) -> bytes:
return value.encode("utf-8")
@classmethod
async def get_secret_key(cls) -> str:
return await ConfigService.get_secret_key("SECRET_KEY", None)
@@ -113,11 +116,17 @@ class AuthService:
@classmethod
def verify_password(cls, plain_password: str, hashed_password: str) -> bool:
return cls.pwd_context.verify(plain_password, hashed_password)
try:
return bcrypt.checkpw(cls._to_bytes(plain_password), hashed_password.encode("utf-8"))
except (ValueError, TypeError):
return False
@classmethod
def get_password_hash(cls, password: str) -> str:
return cls.pwd_context.hash(password)
encoded = cls._to_bytes(password)
if len(encoded) > 72:
raise HTTPException(status_code=400, detail="密码过长")
return bcrypt.hashpw(encoded, bcrypt.gensalt()).decode("utf-8")
@classmethod
async def get_user_db(cls, username_or_email: str) -> UserInDB | None:

View File

@@ -5,7 +5,7 @@ from fastapi import HTTPException
from fastapi.responses import Response
from domain.tasks.service import TaskService
from domain.virtual_fs.thumbnail import is_raw_filename
from domain.virtual_fs.thumbnail import is_raw_filename, raw_bytes_to_jpeg
from .listing import VirtualFSListingMixin
@@ -82,32 +82,9 @@ class VirtualFSFileOpsMixin(VirtualFSListingMixin):
if not rel or rel.endswith("/"):
raise HTTPException(400, detail="Path is a directory")
if is_raw_filename(rel):
import io
import rawpy
from PIL import Image
try:
raw_data = await cls.read_file(path)
try:
with rawpy.imread(io.BytesIO(raw_data)) as raw:
try:
thumb = raw.extract_thumb()
except rawpy.LibRawNoThumbnailError:
thumb = None
if thumb is not None and thumb.format in [rawpy.ThumbFormat.JPEG, rawpy.ThumbFormat.BITMAP]:
im = Image.open(io.BytesIO(thumb.data))
else:
rgb = raw.postprocess(use_camera_wb=False, use_auto_wb=True, output_bps=8)
im = Image.fromarray(rgb)
except Exception as exc:
print(f"rawpy processing failed: {exc}")
raise exc
buf = io.BytesIO()
im.save(buf, "JPEG", quality=90)
content = buf.getvalue()
content = raw_bytes_to_jpeg(raw_data, filename=rel)
return Response(content=content, media_type="image/jpeg")
except Exception as exc:
raise HTTPException(500, detail=f"RAW file processing failed: {exc}")

View File

@@ -5,7 +5,13 @@ from fastapi import HTTPException, UploadFile
from fastapi.responses import Response
from domain.config.service import ConfigService
from domain.virtual_fs.thumbnail import get_or_create_thumb, is_image_filename, is_raw_filename, is_video_filename
from domain.virtual_fs.thumbnail import (
get_or_create_thumb,
is_image_filename,
is_raw_filename,
is_video_filename,
raw_bytes_to_jpeg,
)
from .temp_link import VirtualFSTempLinkMixin
@@ -16,19 +22,9 @@ class VirtualFSRouteMixin(VirtualFSTempLinkMixin):
full_path = cls._normalize_path(full_path)
if is_raw_filename(full_path):
import io
import rawpy
from PIL import Image
try:
raw_data = await cls.read_file(full_path)
with rawpy.imread(io.BytesIO(raw_data)) as raw:
rgb = raw.postprocess(use_camera_wb=True, output_bps=8)
im = Image.fromarray(rgb)
buf = io.BytesIO()
im.save(buf, "JPEG", quality=90)
content = buf.getvalue()
content = raw_bytes_to_jpeg(raw_data, filename=full_path)
return Response(content=content, media_type="image/jpeg")
except FileNotFoundError:
raise HTTPException(404, detail="File not found")

View File

@@ -2,10 +2,13 @@ import asyncio
import inspect
import io
import hashlib
import subprocess
import tempfile
from contextlib import suppress
from pathlib import Path
from typing import Tuple
from PIL import Image
from fastapi import HTTPException
ALLOWED_EXT = {"jpg", "jpeg", "png", "webp", "gif", "bmp",
@@ -58,7 +61,6 @@ def _ensure_cache_dir(p: Path):
def _image_to_webp(im, w: int, h: int, fit: str) -> Tuple[bytes, str]:
from PIL import Image
if im.mode not in ("RGB", "RGBA"):
im = im.convert("RGBA" if im.mode in ("P", "LA") else "RGB")
if fit == 'cover':
@@ -81,30 +83,91 @@ def _image_to_webp(im, w: int, h: int, fit: str) -> Tuple[bytes, str]:
return buf.getvalue(), 'image/webp'
def generate_thumb(data: bytes, w: int, h: int, fit: str, is_raw: bool = False) -> Tuple[bytes, str]:
from PIL import Image
if is_raw:
def _load_image_with_pillow(data: bytes):
im = Image.open(io.BytesIO(data))
im.load()
return im
def _load_raw_with_ffmpeg(data: bytes, filename: str | None) -> "Image.Image":
src_path: str | None = None
dst_path: str | None = None
try:
with tempfile.NamedTemporaryFile(suffix=Path(filename or "").suffix or ".raw", delete=False) as src_tmp:
src_tmp.write(data)
src_path = src_tmp.name
dst_tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
dst_path = dst_tmp.name
dst_tmp.close()
cmd = [
"ffmpeg",
"-y",
"-hide_banner",
"-loglevel", "error",
"-i", src_path,
"-frames:v", "1",
dst_path,
]
try:
import rawpy
with rawpy.imread(io.BytesIO(data)) as raw:
try:
thumb = raw.extract_thumb()
except rawpy.LibRawNoThumbnailError:
thumb = None
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True,
)
except FileNotFoundError as e:
raise RuntimeError("未找到 ffmpeg可执行文件需要在 PATH 中") from e
except subprocess.CalledProcessError as e:
stderr = (e.stderr or b"").decode().strip()
stdout = (e.stdout or b"").decode().strip()
message = stderr or stdout or "ffmpeg 转换 RAW 失败"
raise RuntimeError(message) from e
if thumb is not None and thumb.format in [rawpy.ThumbFormat.JPEG, rawpy.ThumbFormat.BITMAP]:
im = Image.open(io.BytesIO(thumb.data))
else:
rgb = raw.postprocess(
use_camera_wb=False, use_auto_wb=True, output_bps=8)
im = Image.fromarray(rgb)
except Exception as e:
print(f"rawpy processing failed: {e}")
raise e
with open(dst_path, "rb") as f:
img_bytes = f.read()
im = Image.open(io.BytesIO(img_bytes))
im.load()
return im
finally:
if dst_path:
with suppress(FileNotFoundError):
Path(dst_path).unlink()
if src_path:
with suppress(FileNotFoundError):
Path(src_path).unlink()
else:
im = Image.open(io.BytesIO(data))
def load_image_from_bytes(data: bytes, *, filename: str | None = None, is_raw: bool = False):
if not is_raw:
return _load_image_with_pillow(data)
first_error: Exception | None = None
try:
return _load_image_with_pillow(data)
except Exception as exc:
first_error = exc
try:
return _load_raw_with_ffmpeg(data, filename)
except Exception as exc:
msg = f"RAW 解码失败: ffmpeg 处理异常 {exc}"
if first_error:
msg = f"RAW 解码失败: Pillow 异常 {first_error}; ffmpeg 异常 {exc}"
raise RuntimeError(msg) from exc
def raw_bytes_to_jpeg(data: bytes, filename: str | None = None) -> bytes:
im = load_image_from_bytes(data, filename=filename, is_raw=True)
if im.mode != "RGB":
im = im.convert("RGB")
buf = io.BytesIO()
im.save(buf, "JPEG", quality=90)
return buf.getvalue()
def generate_thumb(data: bytes, w: int, h: int, fit: str, is_raw: bool = False, filename: str | None = None) -> Tuple[bytes, str]:
im = load_image_from_bytes(data, filename=filename, is_raw=is_raw)
return _image_to_webp(im, w, h, fit)
@@ -434,7 +497,7 @@ async def get_or_create_thumb(adapter, adapter_id: int, root: str, rel: str, w:
read_data = await adapter.read_file(root, rel)
try:
thumb_bytes, mime = generate_thumb(
read_data, w, h, fit, is_raw=is_raw_filename(rel))
read_data, w, h, fit, is_raw=is_raw_filename(rel), filename=rel)
except Exception as e:
print(e)
raise HTTPException(

View File

@@ -3,24 +3,21 @@ name = "foxel"
version = "1"
description = "foxel.cc"
readme = "README.md"
requires-python = ">=3.13"
requires-python = ">=3.14"
dependencies = [
"aioboto3>=15.2.0",
"aiofiles>=25.1.0",
"fastapi>=0.116.1",
"passlib[bcrypt]>=1.7.4",
"bcrypt>=3.2.2,<6.0",
"pillow>=11.3.0",
"pyjwt>=2.10.1",
"pysocks>=1.7.1",
"python-dotenv>=1.1.1",
"python-multipart>=0.0.20",
"qdrant-client>=1.15.1",
"rawpy>=0.25.1",
"telethon>=1.41.2",
"tortoise-orm>=0.25.2",
"uvicorn>=0.37.0",
"pymilvus[milvus-lite]>=2.6.2",
"aioboto3>=15.5.0",
"bcrypt>=5.0.0",
"fastapi>=0.127.0",
"paramiko>=4.0.0",
"pydantic[email]>=2.11.7",
"pillow>=12.0.0",
"pydantic[email]>=2.12.5",
"pyjwt>=2.10.1",
"pymilvus[milvus-lite]>=2.6.5",
"pysocks>=1.7.1",
"python-dotenv>=1.2.1",
"python-multipart>=0.0.21",
"qdrant-client>=1.16.2",
"telethon>=1.42.0",
"tortoise-orm>=0.25.3",
"uvicorn>=0.40.0",
]

855
uv.lock generated

File diff suppressed because it is too large Load Diff