liumaolin
feat(api): implement local training MVP with adapter pattern
e054d0c
"""
文件管理 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