| """ |
| 文件管理服务 |
| |
| 处理文件上传、下载和管理的业务逻辑 |
| """ |
|
|
| from datetime import datetime |
| from typing import List, Optional, Tuple |
|
|
| from ..core.adapters import get_database_adapter, get_storage_adapter |
| from ..models.schemas.file import ( |
| FileMetadata, |
| FileUploadResponse, |
| FileListResponse, |
| FileDeleteResponse, |
| ) |
|
|
|
|
| class FileService: |
| """ |
| 文件管理服务 |
| |
| 提供文件的完整生命周期管理: |
| - 上传文件 |
| - 下载文件 |
| - 获取元数据 |
| - 列出文件 |
| - 删除文件 |
| |
| Example: |
| >>> service = FileService() |
| >>> result = await service.upload_file(data, "audio.wav", "audio/wav", "training") |
| >>> content = await service.download_file(result.file.id) |
| >>> await service.delete_file(result.file.id) |
| """ |
| |
| def __init__(self): |
| """初始化服务""" |
| self._db = None |
| self._storage = None |
| |
| @property |
| def db(self): |
| """延迟获取数据库适配器""" |
| if self._db is None: |
| self._db = get_database_adapter() |
| return self._db |
| |
| @property |
| def storage(self): |
| """延迟获取存储适配器""" |
| if self._storage is None: |
| self._storage = get_storage_adapter() |
| return self._storage |
| |
| async def upload_file( |
| self, |
| file_data: bytes, |
| filename: str, |
| content_type: Optional[str] = None, |
| purpose: str = "training" |
| ) -> FileUploadResponse: |
| """ |
| 上传文件 |
| |
| Args: |
| file_data: 文件二进制数据 |
| filename: 原始文件名 |
| content_type: MIME 类型 |
| purpose: 文件用途 (training, reference, output) |
| |
| Returns: |
| FileUploadResponse |
| """ |
| |
| metadata = { |
| "content_type": content_type, |
| "purpose": purpose, |
| "size_bytes": len(file_data), |
| } |
| |
| |
| file_id = await self.storage.upload_file(file_data, filename, metadata) |
| |
| |
| full_metadata = await self.storage.get_file_metadata(file_id) |
| |
| |
| file_record = { |
| "id": file_id, |
| "filename": filename, |
| "content_type": content_type, |
| "size_bytes": len(file_data), |
| "purpose": purpose, |
| "duration_seconds": full_metadata.get("duration_seconds") if full_metadata else None, |
| "sample_rate": full_metadata.get("sample_rate") if full_metadata else None, |
| "uploaded_at": datetime.utcnow().isoformat(), |
| } |
| |
| await self.db.create_file_record(file_record) |
| |
| |
| file_metadata = FileMetadata( |
| id=file_id, |
| filename=filename, |
| content_type=content_type, |
| size_bytes=len(file_data), |
| purpose=purpose, |
| duration_seconds=file_record.get("duration_seconds"), |
| sample_rate=file_record.get("sample_rate"), |
| uploaded_at=datetime.utcnow(), |
| ) |
| |
| return FileUploadResponse( |
| success=True, |
| message="文件上传成功", |
| file=file_metadata, |
| ) |
| |
| async def download_file(self, file_id: str) -> Optional[Tuple[bytes, str, str]]: |
| """ |
| 下载文件 |
| |
| Args: |
| file_id: 文件ID |
| |
| Returns: |
| (文件数据, 文件名, 内容类型) 或 None |
| """ |
| |
| if not await self.storage.file_exists(file_id): |
| return None |
| |
| |
| metadata = await self.storage.get_file_metadata(file_id) |
| if not metadata: |
| return None |
| |
| |
| file_data = await self.storage.download_file(file_id) |
| |
| return ( |
| file_data, |
| metadata.get("filename", "file"), |
| metadata.get("content_type", "application/octet-stream"), |
| ) |
| |
| async def get_file(self, file_id: str) -> Optional[FileMetadata]: |
| """ |
| 获取文件元数据 |
| |
| Args: |
| file_id: 文件ID |
| |
| Returns: |
| FileMetadata 或 None |
| """ |
| |
| record = await self.db.get_file_record(file_id) |
| if record: |
| return self._record_to_metadata(record) |
| |
| |
| metadata = await self.storage.get_file_metadata(file_id) |
| if metadata: |
| return self._storage_metadata_to_file_metadata(metadata) |
| |
| return None |
| |
| async def list_files( |
| self, |
| purpose: Optional[str] = None, |
| limit: int = 50, |
| offset: int = 0 |
| ) -> FileListResponse: |
| """ |
| 获取文件列表 |
| |
| Args: |
| purpose: 按用途筛选 |
| limit: 每页数量 |
| offset: 偏移量 |
| |
| Returns: |
| FileListResponse |
| """ |
| |
| records = await self.db.list_file_records( |
| purpose=purpose, limit=limit, offset=offset |
| ) |
| total = await self.db.count_file_records(purpose=purpose) |
| |
| return FileListResponse( |
| items=[self._record_to_metadata(r) for r in records], |
| total=total, |
| limit=limit, |
| offset=offset, |
| ) |
| |
| async def delete_file(self, file_id: str) -> FileDeleteResponse: |
| """ |
| 删除文件 |
| |
| Args: |
| file_id: 文件ID |
| |
| Returns: |
| FileDeleteResponse |
| """ |
| |
| storage_deleted = await self.storage.delete_file(file_id) |
| |
| |
| db_deleted = await self.db.delete_file_record(file_id) |
| |
| if storage_deleted or db_deleted: |
| return FileDeleteResponse( |
| success=True, |
| message="文件删除成功", |
| file_id=file_id, |
| ) |
| else: |
| return FileDeleteResponse( |
| success=False, |
| message="文件不存在或已删除", |
| file_id=file_id, |
| ) |
| |
| async def file_exists(self, file_id: str) -> bool: |
| """ |
| 检查文件是否存在 |
| |
| Args: |
| file_id: 文件ID |
| |
| Returns: |
| 是否存在 |
| """ |
| return await self.storage.file_exists(file_id) |
| |
| def _record_to_metadata(self, record: dict) -> FileMetadata: |
| """将数据库记录转换为 FileMetadata""" |
| uploaded_at = record.get("uploaded_at") |
| if isinstance(uploaded_at, str): |
| uploaded_at = datetime.fromisoformat(uploaded_at) |
| elif uploaded_at is None: |
| uploaded_at = datetime.utcnow() |
| |
| return FileMetadata( |
| id=record["id"], |
| filename=record["filename"], |
| content_type=record.get("content_type"), |
| size_bytes=record.get("size_bytes", 0), |
| purpose=record.get("purpose", "training"), |
| duration_seconds=record.get("duration_seconds"), |
| sample_rate=record.get("sample_rate"), |
| uploaded_at=uploaded_at, |
| ) |
| |
| def _storage_metadata_to_file_metadata(self, metadata: dict) -> FileMetadata: |
| """将存储元数据转换为 FileMetadata""" |
| uploaded_at = metadata.get("uploaded_at") |
| if isinstance(uploaded_at, str): |
| uploaded_at = datetime.fromisoformat(uploaded_at) |
| elif uploaded_at is None: |
| uploaded_at = datetime.utcnow() |
| |
| return FileMetadata( |
| id=metadata.get("id", ""), |
| filename=metadata.get("filename", ""), |
| content_type=metadata.get("content_type"), |
| size_bytes=metadata.get("size_bytes", 0), |
| purpose=metadata.get("purpose", "training"), |
| duration_seconds=metadata.get("duration_seconds"), |
| sample_rate=metadata.get("sample_rate"), |
| uploaded_at=uploaded_at, |
| ) |
|
|