12 KiB
FileStorageService 代码实现
日期:2026-01-28
类型:功能实现
影响范围:FileStorageService 完整实现
变更概述
完成 FileStorageService 的完整代码实现,包括 Model、Schema、Repository、Service、API 层,以及对象存储集成和数据库迁移。
实现内容
1. Model 层
文件:server/app/models/file_checksum.py
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 类:
- FileChecksumBase:基础 Schema
- FileChecksumCreate:创建请求
- FileChecksumUpdate:更新请求
- FileChecksumResponse:响应 Schema
- FileMetadata:文件元数据(上传返回)
3. Repository 层
文件:server/app/repositories/file_checksum_repository.py
实现了 7 个数据访问方法:
create()- 创建记录get_by_id()- 根据 ID 查询get_by_checksum()- 根据校验和查询(核心去重方法)update()- 更新记录delete()- 删除记录get_unused_files()- 获取无引用的过期文件list_by_storage_provider()- 根据存储提供商查询
关键特性:
- 使用 AsyncSession
- 使用
select()替代query() - 所有方法使用 async/await
4. Service 层
文件:server/app/services/file_storage_service.py
实现了 6 个业务逻辑方法:
-
upload_file()- 上传文件(带去重)- 计算 SHA256 校验和
- 检查文件是否已存在
- 如果存在,增加引用计数
- 如果不存在,上传到对象存储并记录
-
get_by_checksum()- 根据校验和查询文件 -
get_presigned_url()- 获取预签名 URL -
increase_reference_count()- 增加引用计数 -
decrease_reference_count()- 减少引用计数- 如果引用计数为 0,删除文件
-
cleanup_unused_files()- 清理无引用的过期文件
关键特性:
- 使用 AsyncSession
- 使用 logger 记录日志
- 规范化错误处理
- 完整的类型注解和文档字符串
5. API 层
文件:server/app/api/v1/file_storage.py
实现了 4 个 REST API 端点:
POST /file-storage/upload- 上传文件(带去重)GET /file-storage/checksum/{checksum}- 根据校验和查询文件GET /file-storage/presigned-url- 获取预签名 URLPOST /file-storage/cleanup- 清理无引用文件
关键特性:
- 使用 FastAPI 依赖注入
- 规范化错误处理
- 完整的请求参数验证
6. 对象存储服务
文件:server/app/core/storage.py
实现了 StorageService 类,支持 MinIO/S3:
upload_bytes()- 上传字节数据get_presigned_url()- 获取预签名 URLdelete_file()- 删除文件file_exists()- 检查文件是否存在_ensure_bucket_exists()- 确保 bucket 存在
关键特性:
- 自动创建 bucket
- 规范化错误处理
- 使用 logger 记录日志
- 抛出自定义异常 StorageError
7. 数据库迁移
文件:server/alembic/versions/20260128_2130_create_file_checksums_table.py
创建 file_checksums 表:
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
新增配置项:
# 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
新增异常类:
class StorageError(Exception):
"""对象存储异常"""
pass
10. 模型导出
文件:server/app/models/__init__.py
新增导出:
from app.models.file_checksum import FileChecksum
__all__ = [
# ...
"FileChecksum",
]
11. 路由注册
文件:server/app/api/v1/router.py
注册 file_storage 路由:
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. 文件去重机制
# 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. 引用计数管理
# 增加引用计数
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. 对象存储集成
# 上传文件
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:上传文件
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 集成
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. 代码诊断
✅ 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. 数据库迁移
# 在容器内执行迁移
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:
@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
文件变更
新建文件
server/app/models/file_checksum.py- FileChecksum 模型server/app/schemas/file_checksum.py- FileChecksum Schemaserver/app/repositories/file_checksum_repository.py- FileChecksum Repositoryserver/app/services/file_storage_service.py- FileStorageServiceserver/app/api/v1/file_storage.py- FileStorage APIserver/app/core/storage.py- StorageServiceserver/alembic/versions/20260128_2130_create_file_checksums_table.py- 数据库迁移
修改文件
server/app/models/__init__.py- 导出 FileChecksumserver/app/api/v1/router.py- 注册 file_storage 路由server/app/core/config.py- 添加 MinIO 配置server/app/core/exceptions.py- 添加 StorageError
相关文档
变更日期:2026-01-28
代码行数:670 行
文件数量:11 个(7 新建 + 4 修改)