fix: 上传端点 500 根因修复 (list_by_project → find_by_project) + OSS 诊断能力 #120
@@ -202,7 +202,7 @@ def get_project_asset_diagnosis(
|
||||
if project is None:
|
||||
raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
|
||||
|
||||
libraries = asset_library_repository.list_by_project(project_id)
|
||||
libraries = asset_library_repository.find_by_project(project_id)
|
||||
assets: list[Asset] = []
|
||||
for library in libraries:
|
||||
assets.extend(asset_repository.list_by_library(library.id))
|
||||
|
||||
@@ -101,7 +101,7 @@ def _require_project_and_library(
|
||||
if project is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
|
||||
libraries = asset_library_repository.list_by_project(project_id)
|
||||
libraries = asset_library_repository.find_by_project(project_id)
|
||||
if not any(item.id == library_id for item in libraries):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Asset library not found")
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ async def readiness_check():
|
||||
checks = {
|
||||
"database": await _check_database(),
|
||||
"redis": await _check_redis(),
|
||||
"oss": _check_oss(),
|
||||
}
|
||||
all_healthy = all(check["status"] == "healthy" for check in checks.values())
|
||||
response = {
|
||||
@@ -97,6 +98,38 @@ async def _check_redis() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _check_oss() -> dict:
|
||||
try:
|
||||
from app.core.storage import get_storage_service
|
||||
|
||||
svc = get_storage_service()
|
||||
if not svc.access_key_id or not svc.access_key_secret:
|
||||
return {
|
||||
"status": "unhealthy",
|
||||
"type": "oss",
|
||||
"message": "OSS credentials not configured (OSS_ACCESS_KEY_ID / OSS_ACCESS_KEY_SECRET missing)",
|
||||
}
|
||||
if svc.bucket is None:
|
||||
return {
|
||||
"status": "unhealthy",
|
||||
"type": "oss",
|
||||
"message": "OSS SDK (oss2) not installed or bucket client init failed",
|
||||
}
|
||||
# Try a lightweight OSS API call to verify connectivity & credentials
|
||||
svc.bucket.get_bucket_info()
|
||||
return {
|
||||
"status": "healthy",
|
||||
"type": "oss",
|
||||
"message": f"OSS connected: endpoint={svc.endpoint} bucket={svc.bucket_name}",
|
||||
}
|
||||
except Exception as error:
|
||||
return {
|
||||
"status": "unhealthy",
|
||||
"type": "oss",
|
||||
"message": f"OSS check failed: {type(error).__name__}: {error}",
|
||||
}
|
||||
|
||||
|
||||
async def _check_migrations() -> dict:
|
||||
if settings.USE_IN_MEMORY_DB:
|
||||
return {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -24,6 +25,8 @@ from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, s
|
||||
|
||||
from packages.application import GetProjectUseCase, SubmitIngestJobCommand, SubmitIngestJobUseCase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 允许上传的文件 MIME 类型
|
||||
@@ -70,7 +73,7 @@ def _require_project_and_library(
|
||||
if project is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
|
||||
libraries = asset_library_repository.list_by_project(project_id)
|
||||
libraries = asset_library_repository.find_by_project(project_id)
|
||||
if not any(item.id == library_id for item in libraries):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Asset library not found")
|
||||
|
||||
@@ -131,7 +134,14 @@ async def prepare_direct_upload(
|
||||
expires_seconds=settings.OSS_DIRECT_UPLOAD_EXPIRE_SECONDS,
|
||||
)
|
||||
except RuntimeError as error:
|
||||
logger.error("OSS not configured for direct upload prepare: %s", error)
|
||||
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(error)) from error
|
||||
except Exception as error:
|
||||
logger.exception("Unexpected error in direct upload prepare: %s", error)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to prepare upload: {type(error).__name__}",
|
||||
) from error
|
||||
|
||||
return DirectUploadPrepareResponse(
|
||||
upload_url=str(payload["url"]),
|
||||
@@ -162,7 +172,15 @@ async def complete_direct_upload(
|
||||
normalized_key = storage_service._normalize_storage_key(request.storage_key)
|
||||
if not normalized_key.startswith("uploads/"):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid upload key")
|
||||
if not storage_service.file_exists(normalized_key):
|
||||
try:
|
||||
file_exists = storage_service.file_exists(normalized_key)
|
||||
except Exception as error:
|
||||
logger.exception("OSS error checking file existence for key=%s: %s", normalized_key, error)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Storage service unavailable",
|
||||
) from error
|
||||
if not file_exists:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Uploaded file not found")
|
||||
|
||||
job = _submit_ingest_job(
|
||||
@@ -201,11 +219,21 @@ async def upload_asset(
|
||||
safe_filename = file.filename.replace("/", "_").replace("\\", "_") if file.filename else "unknown"
|
||||
storage_key = f"uploads/{file_id}/{safe_filename}"
|
||||
|
||||
file_url = storage_service.upload_file(
|
||||
file.file,
|
||||
storage_key,
|
||||
content_type=validated_content_type,
|
||||
)
|
||||
try:
|
||||
file_url = storage_service.upload_file(
|
||||
file.file,
|
||||
storage_key,
|
||||
content_type=validated_content_type,
|
||||
)
|
||||
except RuntimeError as error:
|
||||
logger.error("OSS not configured for upload: %s", error)
|
||||
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(error)) from error
|
||||
except Exception as error:
|
||||
logger.exception("Unexpected error uploading file to OSS: %s", error)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to upload file: {type(error).__name__}",
|
||||
) from error
|
||||
|
||||
job = _submit_ingest_job(
|
||||
project_id=project_id,
|
||||
|
||||
@@ -28,17 +28,38 @@ class OSSStorageService:
|
||||
self.local_url_prefix = os.getenv("GENERATED_FILES_URL_PREFIX", "/generated-files")
|
||||
self.bucket = None
|
||||
|
||||
if settings.OSS_ACCESS_KEY_ID and settings.OSS_ACCESS_KEY_SECRET:
|
||||
has_key_id = bool(settings.OSS_ACCESS_KEY_ID)
|
||||
has_key_secret = bool(settings.OSS_ACCESS_KEY_SECRET)
|
||||
|
||||
if has_key_id and has_key_secret:
|
||||
if oss2 is not None:
|
||||
auth = oss2.Auth(
|
||||
settings.OSS_ACCESS_KEY_ID,
|
||||
settings.OSS_ACCESS_KEY_SECRET,
|
||||
)
|
||||
self.bucket = oss2.Bucket(
|
||||
auth,
|
||||
settings.OSS_ENDPOINT,
|
||||
settings.OSS_BUCKET_NAME,
|
||||
)
|
||||
try:
|
||||
auth = oss2.Auth(
|
||||
settings.OSS_ACCESS_KEY_ID,
|
||||
settings.OSS_ACCESS_KEY_SECRET,
|
||||
)
|
||||
self.bucket = oss2.Bucket(
|
||||
auth,
|
||||
settings.OSS_ENDPOINT,
|
||||
settings.OSS_BUCKET_NAME,
|
||||
)
|
||||
logger.info(
|
||||
"OSS initialized: endpoint=%s bucket=%s",
|
||||
settings.OSS_ENDPOINT,
|
||||
settings.OSS_BUCKET_NAME,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error("Failed to initialize OSS bucket client: %s", error)
|
||||
else:
|
||||
logger.error("oss2 SDK is not installed — OSS operations will fail")
|
||||
else:
|
||||
missing = []
|
||||
if not has_key_id:
|
||||
missing.append("OSS_ACCESS_KEY_ID")
|
||||
if not has_key_secret:
|
||||
missing.append("OSS_ACCESS_KEY_SECRET")
|
||||
logger.error("OSS credentials not configured — missing: %s", ", ".join(missing))
|
||||
|
||||
self.access_key_id = settings.OSS_ACCESS_KEY_ID
|
||||
self.access_key_secret = settings.OSS_ACCESS_KEY_SECRET
|
||||
self.endpoint = settings.OSS_ENDPOINT
|
||||
|
||||
@@ -16,7 +16,7 @@ class InMemoryAssetLibraryRepository:
|
||||
def get(self, library_id: str) -> AssetLibrary | None:
|
||||
return self._libraries.get(library_id)
|
||||
|
||||
def list_by_project(self, project_id: str, kind: AssetLibraryKind | None = None) -> list[AssetLibrary]:
|
||||
def find_by_project(self, project_id: str, kind: AssetLibraryKind | None = None) -> list[AssetLibrary]:
|
||||
items = [library for library in self._libraries.values() if library.project_id == project_id]
|
||||
if kind is not None:
|
||||
items = [library for library in items if library.kind == kind]
|
||||
|
||||
Reference in New Issue
Block a user