"""分片上传接口:创建会话 / 查状态 / 传分片 / 完成拼接。 支持大文件分片上传与断点续传。前端先创建会话拿 upload_id,逐片上传, 可随时查 status 获取已传分片以补传缺失部分,最后 complete 触发拼接入库。 """ from __future__ import annotations from fastapi import APIRouter, Depends, Request from sqlalchemy.orm import Session from ..database import get_db from ..dao.upload_session_dao import UploadSessionDAO from ..dao.uploaded_file_dao import UploadedFileDAO from ..schemas.chunk import ( ChunkUploadResponse, CreateSessionRequest, CreateSessionResponse, SessionStatusResponse, ) from ..schemas.file import FileUploadResponse from ..services.chunk_upload_service import ChunkUploadService router = APIRouter(prefix="/api/files/chunk-uploads", tags=["chunk-upload"]) def _service(db: Session = Depends(get_db)) -> ChunkUploadService: return ChunkUploadService(UploadSessionDAO(db), UploadedFileDAO(db)) @router.post( "", response_model=CreateSessionResponse, summary="创建分片上传会话", description=( "客户端把文件切成固定大小的分片后,先调本接口创建会话。" "服务端生成 upload_id 返回,后续上传分片、查状态、完成拼接都需要它。" ), ) def create_session( body: CreateSessionRequest, service: ChunkUploadService = Depends(_service), ) -> CreateSessionResponse: return service.create_session(body) @router.get( "/{upload_id}/status", response_model=SessionStatusResponse, summary="查询会话状态(断点续传)", description="返回已上传分片下标集合,前端据此只补传缺失分片。", ) def session_status( upload_id: str, service: ChunkUploadService = Depends(_service), ) -> SessionStatusResponse: return service.get_status(upload_id) @router.post( "/{upload_id}/chunks/{index}", response_model=ChunkUploadResponse, summary="上传单个分片", description=( "请求体为单个分片的原始二进制。分片可乱序上传,重传同一分片会覆盖。" "返回当前已上传的分片下标集合。" ), ) async def upload_chunk( upload_id: str, index: int, request: Request, service: ChunkUploadService = Depends(_service), ) -> ChunkUploadResponse: uploaded = service.write_chunk(upload_id, index, await request.body()) return ChunkUploadResponse(upload_id=upload_id, index=index, uploaded_chunks=uploaded) @router.post( "/{upload_id}/complete", response_model=FileUploadResponse, summary="完成拼接并入库", description=( "服务端校验分片齐全后,按顺序拼接为完整文件、流式计算 sha256," "按 sha256 去重(命中则返回旧行不重复落盘),最后原子改名入库。" "重复调用幂等,返回同一 file_id。" ), ) def complete_session( upload_id: str, service: ChunkUploadService = Depends(_service), ) -> FileUploadResponse: return service.complete(upload_id)