P2-1 & P2-5: 移除 upload.py 中重复的 _require_workspace_member,添加服务端 MIME type 验证

This commit is contained in:
2026-06-26 18:35:21 +08:00
parent 6204d2fa45
commit 0b3cb8487c
+45 -14
View File
@@ -2,6 +2,7 @@ from typing import Any
from uuid import uuid4
from app.auth import AuthenticatedUser, get_current_user
from app.api.routes.permissions import require_workspace_member
from app.config import get_settings
from app.core.celery_app import celery_app
from app.core.storage import OSSStorageService, get_storage_service
@@ -25,15 +26,38 @@ from packages.ports.workspace_member_repository import WorkspaceMemberRepository
router = APIRouter()
# 允许上传的文件 MIME 类型
ALLOWED_MIME_TYPES = frozenset({
# 视频
"video/mp4", "video/mpeg", "video/quicktime", "video/x-msvideo",
"video/webm", "video/x-matroska", "video/3gpp",
# 音频
"audio/mpeg", "audio/wav", "audio/ogg", "audio/flac", "audio/aac",
"audio/mp3", "audio/x-m4a", "audio/webm",
# 图片
"image/jpeg", "image/png", "image/gif", "image/webp", "image/bmp",
"image/svg+xml", "image/tiff",
})
def _require_workspace_member(
workspace_id: str,
authenticated_user: AuthenticatedUser,
workspace_member_repository: WorkspaceMemberRepository,
) -> None:
member = workspace_member_repository.find_by_workspace_and_user(workspace_id, authenticated_user.user.id)
if member is None:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Workspace access denied")
def _validate_mime_type(content_type: str | None) -> str:
"""验证并返回标准化的 MIME 类型,如果无效则抛出异常。"""
if not content_type:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Content-Type header is required",
)
# 处理带参数的类型,如 "video/mp4; charset=utf-8"
base_type = content_type.split(";")[0].strip().lower()
if base_type not in ALLOWED_MIME_TYPES:
raise HTTPException(
status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
detail=f"File type '{base_type}' is not supported. Allowed types: video, audio, and image files.",
)
return base_type
def _require_project_and_library(
@@ -90,7 +114,10 @@ async def prepare_direct_upload(
detail=f"File exceeds upload limit ({settings.OSS_DIRECT_UPLOAD_MAX_MB}MB)",
)
_require_workspace_member(request.workspace_id, authenticated_user, workspace_member_repository)
# P2-5: 服务端验证 MIME 类型
validated_content_type = _validate_mime_type(request.content_type)
require_workspace_member(request.workspace_id, authenticated_user, workspace_member_repository)
_require_project_and_library(
request.workspace_id,
request.project_id,
@@ -105,7 +132,7 @@ async def prepare_direct_upload(
try:
payload = storage_service.create_direct_upload_post(
storage_key=storage_key,
content_type=request.content_type or "application/octet-stream",
content_type=validated_content_type,
max_size_bytes=max_size_bytes,
expires_seconds=settings.OSS_DIRECT_UPLOAD_EXPIRE_SECONDS,
)
@@ -133,7 +160,7 @@ async def complete_direct_upload(
storage_service: OSSStorageService = Depends(get_storage_service),
) -> DirectUploadCompleteResponse:
"""确认浏览器直传完成并创建导入任务。"""
_require_workspace_member(request.workspace_id, authenticated_user, workspace_member_repository)
require_workspace_member(request.workspace_id, authenticated_user, workspace_member_repository)
_require_project_and_library(
request.workspace_id,
request.project_id,
@@ -171,16 +198,20 @@ async def upload_asset(
storage_service: OSSStorageService = Depends(get_storage_service),
) -> UploadAssetResponse:
"""上传素材文件并触发导入流水线。"""
_require_workspace_member(workspace_id, authenticated_user, workspace_member_repository)
require_workspace_member(workspace_id, authenticated_user, workspace_member_repository)
_require_project_and_library(workspace_id, project_id, library_id, project_repository, asset_library_repository)
# P2-5: 服务端验证 MIME 类型
validated_content_type = _validate_mime_type(file.content_type)
file_id = uuid4().hex[:8]
storage_key = f"uploads/{file_id}/{file.filename}"
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=file.content_type or "application/octet-stream",
content_type=validated_content_type,
)
job = _submit_ingest_job(