mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-06-25 17:23:59 +08:00
chore(deps): update Python version requirement and dependencies
This commit is contained in:
@@ -1 +1 @@
|
||||
3.13
|
||||
3.14
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user