| | """ |
| | 文件管理 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() |
| |
|
| | |
| | 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", |
| | } |
| |
|
| | |
| | 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 |
| |
|