G_AI / app /server /middleware.py
superxu520's picture
fix: 修复速率限制器死锁风险、临时文件清理兼容性和图片目录初始化问题
d4a9626
Raw History Blame Contribute Delete
4.98 kB
import asyncio
import hashlib
import hmac
import os
import tempfile
import time
from pathlib import Path
from fastapi import Depends, FastAPI, HTTPException, Request, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import ORJSONResponse
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from loguru import logger
from ..utils import g_config
# Persistent directory for storing generated images
# Support environment variable override for Docker/HF deployments
# Directory will be created on first access to avoid import-time failures
_IMAGE_STORE_DIR: Path | None = None
def get_image_store_dir() -> Path:
"""Returns a persistent directory for storing images. Creates it if it doesn't exist."""
global _IMAGE_STORE_DIR
if _IMAGE_STORE_DIR is None:
_IMAGE_STORE_DIR = Path(os.getenv("GEMINI_IMAGE_STORE_PATH", tempfile.gettempdir())) / "ai_generated_images"
try:
_IMAGE_STORE_DIR.mkdir(parents=True, exist_ok=True)
except OSError as e:
logger.error(f"Failed to create image store directory at {_IMAGE_STORE_DIR}: {e}")
logger.warning("Falling back to system temp directory")
_IMAGE_STORE_DIR = Path(tempfile.gettempdir()) / "ai_generated_images"
_IMAGE_STORE_DIR.mkdir(parents=True, exist_ok=True)
return _IMAGE_STORE_DIR
def get_image_token(filename: str) -> str:
"""Generate a HMAC-SHA256 token for a filename using the API key."""
secret = g_config.server.api_key
if not secret:
return ""
msg = filename.encode("utf-8")
secret_bytes = secret.encode("utf-8")
return hmac.new(secret_bytes, msg, hashlib.sha256).hexdigest()
def verify_image_token(filename: str, token: str | None) -> bool:
"""Verify the provided token against the filename."""
expected = get_image_token(filename)
if not expected:
return True # No auth required
if not token:
return False
return hmac.compare_digest(token, expected)
def cleanup_expired_images(retention_days: int) -> int:
"""Delete images in image store directory older than retention_days."""
if retention_days <= 0:
return 0
image_store_dir = get_image_store_dir()
now = time.time()
retention_seconds = retention_days * 24 * 60 * 60
cutoff = now - retention_seconds
count = 0
try:
for file_path in image_store_dir.iterdir():
if not file_path.is_file():
continue
try:
if file_path.stat().st_mtime < cutoff:
file_path.unlink()
count += 1
except Exception as e:
logger.warning(f"Failed to delete expired image {file_path}: {e}")
except FileNotFoundError:
logger.debug(f"Image store directory does not exist yet: {image_store_dir}")
except Exception as e:
logger.warning(f"Failed to cleanup expired images: {e}")
if count > 0:
logger.info(f"Cleaned up {count} expired images.")
return count
def global_exception_handler(request: Request, exc: Exception):
if isinstance(exc, HTTPException):
return ORJSONResponse(
status_code=exc.status_code,
content={"error": {"message": exc.detail}},
)
return ORJSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"error": {"message": str(exc)}},
)
async def get_temp_dir():
"""
Async context manager for temporary directory.
Ensures cleanup even if request handling fails.
"""
temp_dir = tempfile.TemporaryDirectory()
try:
yield Path(temp_dir.name)
finally:
# Run cleanup in thread pool to avoid blocking (Python 3.9+)
try:
await asyncio.to_thread(temp_dir.cleanup)
except AttributeError:
# Fallback for Python < 3.9
await asyncio.get_running_loop().run_in_executor(None, temp_dir.cleanup)
def verify_api_key(
credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer(auto_error=False)),
):
if not g_config.server.api_key:
return ""
if credentials is None or credentials.scheme.lower() != "bearer":
raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail="Invalid or missing token")
api_key = credentials.credentials
if api_key != g_config.server.api_key:
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="Wrong API key")
return api_key
def add_exception_handler(app: FastAPI):
app.add_exception_handler(Exception, global_exception_handler)
def add_cors_middleware(app: FastAPI):
if g_config.cors.enabled:
cors = g_config.cors
app.add_middleware(
CORSMiddleware,
allow_origins=cors.allow_origins,
allow_credentials=cors.allow_credentials,
allow_methods=cors.allow_methods,
allow_headers=cors.allow_headers,
)