From 4171dd4420f496c3b4934eddee66440f2d4607f2 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 9 Jul 2026 10:28:53 +0800 Subject: [PATCH] =?UTF-8?q?fix(upload):=20=E4=BF=AE=E5=A4=8D=E5=88=86?= =?UTF-8?q?=E7=89=87=E4=B8=8A=E4=BC=A0=E8=B7=AF=E7=94=B1=E9=A1=BA=E5=BA=8F?= =?UTF-8?q?=EF=BC=8C=E7=A1=AE=E4=BF=9Dcomplete/status=E7=AB=AF=E7=82=B9?= =?UTF-8?q?=E5=8F=AF=E8=AE=BF=E9=97=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将动态路由 /{upload_id}/{chunk_index} 移到固定路由 /{upload_id}/status 和 /{upload_id}/complete 之后。FastAPI按顺序匹配路由,之前动态路由 会捕获 complete/status 路径,导致这些端点无法正常访问。 --- apps/api/app/api/routes/chunked_upload.py | 148 +++++++++++----------- 1 file changed, 75 insertions(+), 73 deletions(-) diff --git a/apps/api/app/api/routes/chunked_upload.py b/apps/api/app/api/routes/chunked_upload.py index b9537c6e6..9b327069b 100644 --- a/apps/api/app/api/routes/chunked_upload.py +++ b/apps/api/app/api/routes/chunked_upload.py @@ -274,79 +274,6 @@ async def init_chunked_upload( ) -@router.post("/{upload_id}/{chunk_index}") -async def upload_chunk( - upload_id: str, - chunk_index: int, - chunk: UploadFile = File(..., description="Chunk data"), - authenticated_user: AuthenticatedUser = Depends(get_current_user), -) -> dict[str, Any]: - """Upload a single chunk""" - # Load metadata - meta = _load_upload_meta(upload_id) - - # Check expiry - expires_at = datetime.fromisoformat(meta["expires_at"]) - if expires_at.tzinfo is None: - expires_at = expires_at.replace(tzinfo=timezone.utc) - - if expires_at < datetime.now(timezone.utc): - raise HTTPException(status_code=status.HTTP_410_GONE, detail="Upload has expired") - - # Validate chunk index - if chunk_index < 0 or chunk_index >= meta["total_chunks"]: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Invalid chunk index. Must be between 0 and {meta['total_chunks'] - 1}", - ) - - # Atomic check and record to prevent race conditions - if not _atomic_check_and_record(upload_id, chunk_index): - return {"message": "Chunk already uploaded", "chunk_index": chunk_index} - - # Read chunk data - chunk_data = await chunk.read() - - # Validate chunk size (last chunk can be smaller than chunk_size) - expected_size = DEFAULT_CHUNK_SIZE - if chunk_index == meta["total_chunks"] - 1: - expected_size = meta["file_size"] - (chunk_index * DEFAULT_CHUNK_SIZE) - - if len(chunk_data) != expected_size: - # Rollback the recorded chunk - meta_path = _get_upload_meta_path(upload_id) - with open(meta_path, "r+", encoding="utf-8") as f: - fcntl.flock(f.fileno(), fcntl.LOCK_EX) - try: - meta = json.load(f) - if chunk_index in meta["uploaded_chunks"]: - meta["uploaded_chunks"].remove(chunk_index) - f.seek(0) - json.dump(meta, f, ensure_ascii=False, indent=2) - f.truncate() - finally: - fcntl.flock(f.fileno(), fcntl.LOCK_UN) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Chunk size mismatch. Expected {expected_size}, got {len(chunk_data)}", - ) - - # Save chunk - chunk_path = _get_chunk_dir(upload_id) / f"chunk_{chunk_index:06d}" - with open(chunk_path, "wb") as f: - f.write(chunk_data) - - # Reload metadata for response - meta = _load_upload_meta(upload_id) - - return { - "message": "Chunk uploaded successfully", - "chunk_index": chunk_index, - "uploaded_chunks": len(meta["uploaded_chunks"]), - "total_chunks": meta["total_chunks"], - } - - @router.get("/{upload_id}/status", response_model=ChunkedUploadStatusResponse) async def get_upload_status( upload_id: str, @@ -493,3 +420,78 @@ async def complete_chunked_upload( meta_path = _get_upload_meta_path(upload_id) if meta_path.exists(): meta_path.unlink() + + +@router.post("/{upload_id}/{chunk_index}") +async def upload_chunk( + upload_id: str, + chunk_index: int, + chunk: UploadFile = File(..., description="Chunk data"), + authenticated_user: AuthenticatedUser = Depends(get_current_user), +) -> dict[str, Any]: + """Upload a single chunk""" + # Load metadata + meta = _load_upload_meta(upload_id) + + # Check expiry + expires_at = datetime.fromisoformat(meta["expires_at"]) + if expires_at.tzinfo is None: + expires_at = expires_at.replace(tzinfo=timezone.utc) + + if expires_at < datetime.now(timezone.utc): + raise HTTPException(status_code=status.HTTP_410_GONE, detail="Upload has expired") + + # Validate chunk index + if chunk_index < 0 or chunk_index >= meta["total_chunks"]: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid chunk index. Must be between 0 and {meta['total_chunks'] - 1}", + ) + + # Atomic check and record to prevent race conditions + if not _atomic_check_and_record(upload_id, chunk_index): + return {"message": "Chunk already uploaded", "chunk_index": chunk_index} + + # Read chunk data + chunk_data = await chunk.read() + + # Validate chunk size (last chunk can be smaller than chunk_size) + expected_size = DEFAULT_CHUNK_SIZE + if chunk_index == meta["total_chunks"] - 1: + expected_size = meta["file_size"] - (chunk_index * DEFAULT_CHUNK_SIZE) + + if len(chunk_data) != expected_size: + # Rollback the recorded chunk + meta_path = _get_upload_meta_path(upload_id) + with open(meta_path, "r+", encoding="utf-8") as f: + fcntl.flock(f.fileno(), fcntl.LOCK_EX) + try: + meta = json.load(f) + if chunk_index in meta["uploaded_chunks"]: + meta["uploaded_chunks"].remove(chunk_index) + f.seek(0) + json.dump(meta, f, ensure_ascii=False, indent=2) + f.truncate() + finally: + fcntl.flock(f.fileno(), fcntl.LOCK_UN) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Chunk size mismatch. Expected {expected_size}, got {len(chunk_data)}", + ) + + # Save chunk + chunk_path = _get_chunk_dir(upload_id) / f"chunk_{chunk_index:06d}" + with open(chunk_path, "wb") as f: + f.write(chunk_data) + + # Reload metadata for response + meta = _load_upload_meta(upload_id) + + return { + "message": "Chunk uploaded successfully", + "chunk_index": chunk_index, + "uploaded_chunks": len(meta["uploaded_chunks"]), + "total_chunks": meta["total_chunks"], + } + + -- 2.54.0