| """ |
| 文件管理 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 |
|
|