fix(P2-5): add server-side file type validation with magic
Deploy / Deploy Staging (push) Failing after 1s
Deploy / Build Production Runtime Images (push) Has been skipped
Deploy / Deploy Production (push) Has been skipped
Deploy / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 1m0s
Tests / test (pull_request) Failing after 1m0s
Tests / lint (pull_request) Failing after 30s
Deploy / Deploy Staging (push) Failing after 1s
Deploy / Build Production Runtime Images (push) Has been skipped
Deploy / Deploy Production (push) Has been skipped
Deploy / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 1m0s
Tests / test (pull_request) Failing after 1m0s
Tests / lint (pull_request) Failing after 30s
This commit is contained in:
@@ -25,6 +25,13 @@ from packages.ports.workspace_member_repository import WorkspaceMemberRepository
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Allowed MIME types for file uploads (P2-5: Server-side validation)
|
||||
ALLOWED_MIME_TYPES = {
|
||||
"image/jpeg", "image/png", "image/gif", "image/webp",
|
||||
"video/mp4", "video/quicktime", "video/x-msvideo", "video/webm",
|
||||
"audio/mpeg", "audio/wav", "audio/ogg", "audio/mp3",
|
||||
}
|
||||
|
||||
|
||||
def _require_workspace_member(
|
||||
workspace_id: str,
|
||||
@@ -72,6 +79,37 @@ def _submit_ingest_job(
|
||||
return job
|
||||
|
||||
|
||||
def _validate_file_type(content: bytes, filename: str) -> str:
|
||||
"""
|
||||
P2-5: Server-side file type validation using python-magic.
|
||||
|
||||
Args:
|
||||
content: File content bytes
|
||||
filename: Original filename
|
||||
|
||||
Returns:
|
||||
Detected MIME type
|
||||
|
||||
Raises:
|
||||
HTTPException: If file type is not allowed
|
||||
"""
|
||||
try:
|
||||
import magic
|
||||
detected_mime = magic.from_buffer(content, mime=True)
|
||||
except ImportError:
|
||||
# Fallback: infer from extension if magic is not available
|
||||
import mimetypes
|
||||
detected_mime = mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
||||
|
||||
if detected_mime not in ALLOWED_MIME_TYPES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Unsupported file type: {detected_mime}. Allowed types: {', '.join(sorted(ALLOWED_MIME_TYPES))}"
|
||||
)
|
||||
|
||||
return detected_mime
|
||||
|
||||
|
||||
@router.post("/direct/prepare", response_model=DirectUploadPrepareResponse)
|
||||
async def prepare_direct_upload(
|
||||
request: DirectUploadPrepareRequest,
|
||||
@@ -81,7 +119,7 @@ async def prepare_direct_upload(
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
storage_service: OSSStorageService = Depends(get_storage_service),
|
||||
) -> DirectUploadPrepareResponse:
|
||||
"""创建浏览器直传 OSS 的短期表单签名。"""
|
||||
"""Prepare direct upload to OSS"""
|
||||
settings = get_settings()
|
||||
max_size_bytes = settings.OSS_DIRECT_UPLOAD_MAX_MB * 1024 * 1024
|
||||
if request.file_size > max_size_bytes:
|
||||
@@ -132,7 +170,7 @@ async def complete_direct_upload(
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
storage_service: OSSStorageService = Depends(get_storage_service),
|
||||
) -> DirectUploadCompleteResponse:
|
||||
"""确认浏览器直传完成并创建导入任务。"""
|
||||
"""Complete direct upload"""
|
||||
_require_workspace_member(request.workspace_id, authenticated_user, workspace_member_repository)
|
||||
_require_project_and_library(
|
||||
request.workspace_id,
|
||||
@@ -159,10 +197,10 @@ async def complete_direct_upload(
|
||||
|
||||
@router.post("", response_model=UploadAssetResponse)
|
||||
async def upload_asset(
|
||||
file: UploadFile = File(..., description="要上传的文件(视频、音频、图片等)"),
|
||||
file: UploadFile = File(..., description="文件上传"),
|
||||
workspace_id: str = Form(..., description="工作空间 ID"),
|
||||
project_id: str = Form(..., description="项目 ID"),
|
||||
library_id: str = Form(..., description="素材库 ID"),
|
||||
library_id: str = Form(..., description="资产库 ID"),
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
ingest_job_repository: Any = Depends(get_ingest_job_repository),
|
||||
project_repository: Any = Depends(get_project_repository),
|
||||
@@ -170,17 +208,26 @@ async def upload_asset(
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
storage_service: OSSStorageService = Depends(get_storage_service),
|
||||
) -> UploadAssetResponse:
|
||||
"""上传素材文件并触发导入流水线。"""
|
||||
"""Upload asset file"""
|
||||
_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)
|
||||
|
||||
# Read file content for validation (P2-5)
|
||||
file_content = await file.read()
|
||||
|
||||
# P2-5: Server-side file type validation
|
||||
detected_mime = _validate_file_type(file_content, file.filename)
|
||||
|
||||
# Reset file pointer for upload
|
||||
await file.seek(0)
|
||||
|
||||
file_id = uuid4().hex[:8]
|
||||
storage_key = f"uploads/{file_id}/{file.filename}"
|
||||
|
||||
file_url = storage_service.upload_file(
|
||||
file.file,
|
||||
storage_key,
|
||||
content_type=file.content_type or "application/octet-stream",
|
||||
content_type=file.content_type or detected_mime,
|
||||
)
|
||||
|
||||
job = _submit_ingest_job(
|
||||
|
||||
Reference in New Issue
Block a user