diff --git a/apps/api/app/api/routes/asset_diagnosis.py b/apps/api/app/api/routes/asset_diagnosis.py index 76fd75967..74b09af61 100644 --- a/apps/api/app/api/routes/asset_diagnosis.py +++ b/apps/api/app/api/routes/asset_diagnosis.py @@ -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)) diff --git a/apps/api/app/api/routes/chunked_upload.py b/apps/api/app/api/routes/chunked_upload.py index d2ee4c957..4e06b83d7 100644 --- a/apps/api/app/api/routes/chunked_upload.py +++ b/apps/api/app/api/routes/chunked_upload.py @@ -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") diff --git a/apps/api/app/api/routes/health.py b/apps/api/app/api/routes/health.py index bb85ff5c7..d305d614b 100644 --- a/apps/api/app/api/routes/health.py +++ b/apps/api/app/api/routes/health.py @@ -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 { diff --git a/apps/api/app/api/routes/upload.py b/apps/api/app/api/routes/upload.py index 8220b77a8..8001d705d 100644 --- a/apps/api/app/api/routes/upload.py +++ b/apps/api/app/api/routes/upload.py @@ -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, diff --git a/apps/api/app/core/storage.py b/apps/api/app/core/storage.py index 868334efb..094c3bea8 100644 --- a/apps/api/app/core/storage.py +++ b/apps/api/app/core/storage.py @@ -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 diff --git a/packages/adapters/in_memory/asset_library_repository.py b/packages/adapters/in_memory/asset_library_repository.py index 1c218e805..361938881 100644 --- a/packages/adapters/in_memory/asset_library_repository.py +++ b/packages/adapters/in_memory/asset_library_repository.py @@ -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]