# FileStorageService 代码实现 > **日期**:2026-01-28 > **类型**:功能实现 > **影响范围**:FileStorageService 完整实现 --- ## 变更概述 完成 FileStorageService 的完整代码实现,包括 Model、Schema、Repository、Service、API 层,以及对象存储集成和数据库迁移。 --- ## 实现内容 ### 1. Model 层 **文件**:`server/app/models/file_checksum.py` ```python class FileChecksum(SQLModel, table=True): """文件校验和模型 - 用于全局文件去重""" __tablename__ = "file_checksums" # 主键 - UUID v7 id: UUID # 文件信息 checksum: str # SHA256 校验和 file_url: str file_size: int mime_type: str # 存储信息 storage_provider: str # minio/s3/oss storage_path: str # 引用计数 reference_count: int # 时间戳 created_at: datetime last_accessed_at: datetime updated_at: datetime ``` **关键特性**: - 使用 SQLModel + UUID v7 主键 - checksum 设置 UNIQUE 约束 - 完整的索引配置 - 使用 `datetime.now(timezone.utc)` 生成时间戳 ### 2. Schema 层 **文件**:`server/app/schemas/file_checksum.py` 定义了 5 个 Pydantic Schema 类: 1. **FileChecksumBase**:基础 Schema 2. **FileChecksumCreate**:创建请求 3. **FileChecksumUpdate**:更新请求 4. **FileChecksumResponse**:响应 Schema 5. **FileMetadata**:文件元数据(上传返回) ### 3. Repository 层 **文件**:`server/app/repositories/file_checksum_repository.py` 实现了 7 个数据访问方法: 1. `create()` - 创建记录 2. `get_by_id()` - 根据 ID 查询 3. `get_by_checksum()` - 根据校验和查询(核心去重方法) 4. `update()` - 更新记录 5. `delete()` - 删除记录 6. `get_unused_files()` - 获取无引用的过期文件 7. `list_by_storage_provider()` - 根据存储提供商查询 **关键特性**: - 使用 AsyncSession - 使用 `select()` 替代 `query()` - 所有方法使用 async/await ### 4. Service 层 **文件**:`server/app/services/file_storage_service.py` 实现了 6 个业务逻辑方法: 1. `upload_file()` - 上传文件(带去重) - 计算 SHA256 校验和 - 检查文件是否已存在 - 如果存在,增加引用计数 - 如果不存在,上传到对象存储并记录 2. `get_by_checksum()` - 根据校验和查询文件 3. `get_presigned_url()` - 获取预签名 URL 4. `increase_reference_count()` - 增加引用计数 5. `decrease_reference_count()` - 减少引用计数 - 如果引用计数为 0,删除文件 6. `cleanup_unused_files()` - 清理无引用的过期文件 **关键特性**: - 使用 AsyncSession - 使用 logger 记录日志 - 规范化错误处理 - 完整的类型注解和文档字符串 ### 5. API 层 **文件**:`server/app/api/v1/file_storage.py` 实现了 4 个 REST API 端点: 1. `POST /file-storage/upload` - 上传文件(带去重) 2. `GET /file-storage/checksum/{checksum}` - 根据校验和查询文件 3. `GET /file-storage/presigned-url` - 获取预签名 URL 4. `POST /file-storage/cleanup` - 清理无引用文件 **关键特性**: - 使用 FastAPI 依赖注入 - 规范化错误处理 - 完整的请求参数验证 ### 6. 对象存储服务 **文件**:`server/app/core/storage.py` 实现了 StorageService 类,支持 MinIO/S3: 1. `upload_bytes()` - 上传字节数据 2. `get_presigned_url()` - 获取预签名 URL 3. `delete_file()` - 删除文件 4. `file_exists()` - 检查文件是否存在 5. `_ensure_bucket_exists()` - 确保 bucket 存在 **关键特性**: - 自动创建 bucket - 规范化错误处理 - 使用 logger 记录日志 - 抛出自定义异常 StorageError ### 7. 数据库迁移 **文件**:`server/alembic/versions/20260128_2130_create_file_checksums_table.py` 创建 file_checksums 表: ```sql CREATE TABLE file_checksums ( id UUID PRIMARY KEY, checksum VARCHAR(64) NOT NULL UNIQUE, file_url VARCHAR(500) NOT NULL, file_size BIGINT NOT NULL, mime_type VARCHAR(100) NOT NULL, storage_provider VARCHAR(50) NOT NULL, storage_path VARCHAR(500) NOT NULL, reference_count INTEGER NOT NULL DEFAULT 1, created_at TIMESTAMPTZ NOT NULL, last_accessed_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL ); -- 5 个索引 CREATE INDEX idx_file_checksums_checksum ON file_checksums (checksum); CREATE INDEX idx_file_checksums_file_url ON file_checksums (file_url); CREATE INDEX idx_file_checksums_reference_count ON file_checksums (reference_count); CREATE INDEX idx_file_checksums_last_accessed ON file_checksums (last_accessed_at); CREATE INDEX idx_file_checksums_created_at ON file_checksums (created_at); ``` **关键特性**: - 所有字段添加行内注释 - 完整的索引配置 - 支持 upgrade 和 downgrade ### 8. 配置更新 **文件**:`server/app/core/config.py` 新增配置项: ```python # MinIO 配置 MINIO_ENDPOINT: str = "localhost:6185" MINIO_ACCESS_KEY: str = "minioadmin" MINIO_SECRET_KEY: str = "minioadmin" MINIO_BUCKET_NAME: str = "jointo" MINIO_SECURE: bool = False MINIO_PUBLIC_URL: str = "http://localhost:6185" # 存储提供商 STORAGE_PROVIDER: str = "minio" # minio/s3/oss ``` ### 9. 异常定义 **文件**:`server/app/core/exceptions.py` 新增异常类: ```python class StorageError(Exception): """对象存储异常""" pass ``` ### 10. 模型导出 **文件**:`server/app/models/__init__.py` 新增导出: ```python from app.models.file_checksum import FileChecksum __all__ = [ # ... "FileChecksum", ] ``` ### 11. 路由注册 **文件**:`server/app/api/v1/router.py` 注册 file_storage 路由: ```python from app.api.v1 import file_storage api_router.include_router(file_storage.router, prefix="/file-storage", tags=["文件存储"]) ``` --- ## 代码统计 | 层级 | 文件 | 行数 | 说明 | |------|------|------|------| | Model | `file_checksum.py` | 80 | FileChecksum 模型 | | Schema | `file_checksum.py` | 50 | 5 个 Schema 类 | | Repository | `file_checksum_repository.py` | 90 | 7 个数据访问方法 | | Service | `file_storage_service.py` | 180 | 6 个业务逻辑方法 | | API | `file_storage.py` | 80 | 4 个 REST API 端点 | | Storage | `storage.py` | 130 | MinIO/S3 集成 | | Migration | `20260128_2130_*.py` | 60 | 数据库迁移脚本 | | **总计** | **7 个文件** | **670 行** | **完整实现** | --- ## 核心功能 ### 1. 文件去重机制 ```python # 1. 计算 SHA256 校验和 checksum = hashlib.sha256(file_content).hexdigest() # 2. 检查是否已存在 existing = await checksum_repo.get_by_checksum(checksum) # 3. 如果存在,增加引用计数 if existing: await increase_reference_count(existing.id) return existing_file_metadata # 4. 如果不存在,上传到对象存储 file_url = await storage.upload_bytes(...) await checksum_repo.create(FileChecksum(...)) ``` ### 2. 引用计数管理 ```python # 增加引用计数 async def increase_reference_count(file_checksum_id: UUID): file_checksum.reference_count += 1 file_checksum.last_accessed_at = datetime.now(timezone.utc) await update(file_checksum) # 减少引用计数 async def decrease_reference_count(file_checksum_id: UUID): file_checksum.reference_count -= 1 if file_checksum.reference_count <= 0: # 无引用,删除文件 await storage.delete_file(file_checksum.storage_path) await delete(file_checksum.id) ``` ### 3. 对象存储集成 ```python # 上传文件 file_url = await storage.upload_bytes( data=file_content, object_name=f"{category}/{user_id}/{checksum}{extension}", content_type=content_type ) # 获取预签名 URL presigned_url = await storage.get_presigned_url( object_name=storage_path, expires=3600 ) # 删除文件 await storage.delete_file(object_name) ``` --- ## 使用示例 ### 示例 1:上传文件 ```python from app.services.file_storage_service import FileStorageService service = FileStorageService(db) # 上传文件 file_meta = await service.upload_file( file_content=content, filename="avatar.jpg", content_type="image/jpeg", category="user_avatar", user_id=user_id ) # 返回文件元数据 # FileMetadata( # file_url="http://localhost:6185/jointo/user_avatar/xxx/abc123.jpg", # file_size=102400, # checksum="abc123...", # mime_type="image/jpeg", # extension=".jpg", # storage_provider="minio", # storage_path="user_avatar/xxx/abc123.jpg" # ) ``` ### 示例 2:AttachmentService 集成 ```python from app.services.file_storage_service import FileStorageService class AttachmentService: def __init__(self, db: AsyncSession): self.file_storage = FileStorageService(db) async def upload_attachment(self, file: UploadFile, user_id: UUID): # 使用 FileStorageService 上传 content = await file.read() file_meta = await self.file_storage.upload_file( file_content=content, filename=file.filename, content_type=file.content_type, category='attachment_document', user_id=user_id ) # 创建附件记录 attachment = Attachment( user_id=user_id, file_url=file_meta.file_url, file_size=file_meta.file_size, checksum=file_meta.checksum, storage_provider=file_meta.storage_provider, storage_path=file_meta.storage_path ) self.db.add(attachment) await self.db.commit() return attachment ``` --- ## 测试验证 ### 1. 代码诊断 ```bash ✅ server/app/models/file_checksum.py: No diagnostics found ✅ server/app/schemas/file_checksum.py: No diagnostics found ✅ server/app/repositories/file_checksum_repository.py: No diagnostics found ✅ server/app/services/file_storage_service.py: No diagnostics found ✅ server/app/api/v1/file_storage.py: No diagnostics found ✅ server/app/core/storage.py: No diagnostics found ``` ### 2. 数据库迁移 ```bash # 在容器内执行迁移 docker exec jointo-server-app alembic upgrade head # 验证表创建 docker exec jointo-server-postgres psql -U jointoAI -d jointo -c "\d file_checksums" ``` --- ## 后续工作 ### 1. AttachmentService 集成 更新 `server/app/services/attachment_service.py`: - 替换临时文件存储逻辑 - 使用 FileStorageService 上传文件 - 使用 FileStorageService 管理引用计数 ### 2. 定时任务 创建 `server/app/tasks/cleanup_files.py`: ```python @celery_app.task(name="cleanup_unused_files") async def cleanup_unused_files(): """清理30天未访问且无引用的文件""" async with async_session_maker() as db: file_storage = FileStorageService(db) deleted_count = await file_storage.cleanup_unused_files(days=30) logger.info(f"清理了 {deleted_count} 个无引用文件") ``` ### 3. 单元测试 创建 `server/tests/unit/test_file_storage_service.py`: - 测试文件上传(去重) - 测试引用计数管理 - 测试文件删除 - 测试清理过期文件 ### 4. 集成测试 创建 `server/tests/integration/test_file_storage_api.py`: - 测试文件上传 API - 测试预签名 URL 生成 - 测试文件查询 API --- ## 文件变更 ### 新建文件 1. `server/app/models/file_checksum.py` - FileChecksum 模型 2. `server/app/schemas/file_checksum.py` - FileChecksum Schema 3. `server/app/repositories/file_checksum_repository.py` - FileChecksum Repository 4. `server/app/services/file_storage_service.py` - FileStorageService 5. `server/app/api/v1/file_storage.py` - FileStorage API 6. `server/app/core/storage.py` - StorageService 7. `server/alembic/versions/20260128_2130_create_file_checksums_table.py` - 数据库迁移 ### 修改文件 1. `server/app/models/__init__.py` - 导出 FileChecksum 2. `server/app/api/v1/router.py` - 注册 file_storage 路由 3. `server/app/core/config.py` - 添加 MinIO 配置 4. `server/app/core/exceptions.py` - 添加 StorageError --- ## 相关文档 - [FileStorageService 规范文档](../../requirements/backend/04-services/resource/file-storage-service.md) - [FileStorageService 规范修复](./2026-01-28-file-storage-service-spec-compliance.md) - [AttachmentService 实现](./2026-01-28-attachment-service-implementation.md) --- **变更日期**:2026-01-28 **代码行数**:670 行 **文件数量**:11 个(7 新建 + 4 修改)