""" 文件管理 API 文件上传、下载和管理 API 端点 API 列表: - POST /files 上传文件 - GET /files 获取文件列表 - GET /files/{file_id} 下载文件(或获取元数据) - DELETE /files/{file_id} 删除文件 """ from typing import Optional from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile from fastapi.responses import Response from ....models.schemas.file import ( FileMetadata, FileUploadResponse, FileListResponse, FileDeleteResponse, ) from ....models.schemas.common import ErrorResponse from ....services.file_service import FileService from ...deps import get_file_service router = APIRouter() # 允许的音频 MIME 类型 ALLOWED_AUDIO_TYPES = { "audio/wav", "audio/wave", "audio/x-wav", "audio/mpeg", "audio/mp3", "audio/mp4", "audio/aac", "audio/ogg", "audio/flac", "audio/x-flac", "audio/webm", } # 最大文件大小 (500MB) MAX_FILE_SIZE = 500 * 1024 * 1024 @router.post( "", response_model=FileUploadResponse, summary="上传文件", description=""" 上传音频文件用于训练。 **支持的音频格式**: - WAV - MP3 - FLAC - OGG - AAC - WebM **文件大小限制**: 500MB **用途类型**: - `training`: 训练音频(默认) - `reference`: 参考音频 - `output`: 输出文件 """, responses={ 200: {"model": FileUploadResponse, "description": "文件上传成功"}, 400: {"model": ErrorResponse, "description": "文件格式或大小不合法"}, }, ) async def upload_file( file: UploadFile = File(..., description="要上传的音频文件"), purpose: str = Query( "training", description="文件用途: training, reference, output" ), service: FileService = Depends(get_file_service), ) -> FileUploadResponse: """ 上传文件 """ # 验证用途 if purpose not in ("training", "reference", "output"): raise HTTPException( status_code=400, detail="无效的用途类型,有效值: training, reference, output" ) # 验证文件类型(可选,允许不明确类型的文件) content_type = file.content_type if content_type and content_type not in ALLOWED_AUDIO_TYPES: # 检查文件扩展名 filename = file.filename or "" ext = filename.lower().split(".")[-1] if "." in filename else "" allowed_exts = {"wav", "mp3", "flac", "ogg", "aac", "webm", "m4a"} if ext not in allowed_exts: raise HTTPException( status_code=400, detail=f"不支持的文件类型: {content_type}。支持的格式: WAV, MP3, FLAC, OGG, AAC, WebM" ) # 读取文件内容 file_data = await file.read() # 验证文件大小 if len(file_data) > MAX_FILE_SIZE: raise HTTPException( status_code=400, detail=f"文件过大,最大允许 {MAX_FILE_SIZE // (1024*1024)}MB" ) # 验证文件不为空 if len(file_data) == 0: raise HTTPException( status_code=400, detail="文件为空" ) # 上传文件 return await service.upload_file( file_data=file_data, filename=file.filename or "audio", content_type=content_type, purpose=purpose, ) @router.get( "", response_model=FileListResponse, summary="获取文件列表", description="获取已上传的文件列表,支持按用途筛选和分页。", ) async def list_files( purpose: Optional[str] = Query( None, description="按用途筛选: training, reference, output" ), limit: int = Query(50, ge=1, le=100, description="每页数量"), offset: int = Query(0, ge=0, description="偏移量"), service: FileService = Depends(get_file_service), ) -> FileListResponse: """ 获取文件列表 """ return await service.list_files(purpose=purpose, limit=limit, offset=offset) @router.get( "/{file_id}", summary="下载文件或获取元数据", description=""" 根据请求类型返回文件内容或元数据。 - 默认返回文件内容(用于下载) - 添加 `?metadata=true` 参数只返回元数据 """, responses={ 200: { "description": "文件内容(下载时)或元数据(metadata=true 时)", }, 404: {"model": ErrorResponse, "description": "文件不存在"}, }, ) async def get_file( file_id: str, metadata: bool = Query(False, description="只返回元数据"), service: FileService = Depends(get_file_service), ): """ 下载文件或获取元数据 """ if metadata: # 只返回元数据 file_metadata = await service.get_file(file_id) if not file_metadata: raise HTTPException(status_code=404, detail="文件不存在") return file_metadata else: # 下载文件 result = await service.download_file(file_id) if not result: raise HTTPException(status_code=404, detail="文件不存在") file_data, filename, content_type = result return Response( content=file_data, media_type=content_type, headers={ "Content-Disposition": f'attachment; filename="{filename}"', "Content-Length": str(len(file_data)), }, ) @router.delete( "/{file_id}", response_model=FileDeleteResponse, summary="删除文件", description="删除指定的文件。", responses={ 200: {"model": FileDeleteResponse, "description": "删除结果"}, 404: {"model": ErrorResponse, "description": "文件不存在"}, }, ) async def delete_file( file_id: str, service: FileService = Depends(get_file_service), ) -> FileDeleteResponse: """ 删除文件 """ result = await service.delete_file(file_id) if not result.success: raise HTTPException(status_code=404, detail="文件不存在或已删除") return result