You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
10 KiB
10 KiB
AttachmentService 集成 FileStorageService
日期:2026-01-28
类型:功能集成
影响范围:AttachmentService
变更概述
将 FileStorageService 集成到 AttachmentService 中,替换临时文件存储逻辑,实现文件去重、引用计数管理和预签名 URL 生成。
集成内容
1. 初始化 FileStorageService
修改前:
class AttachmentService:
def __init__(self, session: AsyncSession):
self.repository = AttachmentRepository(session)
self.session = session
修改后:
from app.services.file_storage_service import FileStorageService
class AttachmentService:
def __init__(self, session: AsyncSession):
self.repository = AttachmentRepository(session)
self.session = session
self.file_storage = FileStorageService(session)
2. 文件上传集成
修改前(临时实现):
# 暂时使用简单的文件存储逻辑
import hashlib
import os
checksum = hashlib.sha256(content).hexdigest()
extension = os.path.splitext(file.filename)[1]
file_url = f"/uploads/attachments/{checksum}{extension}" # 临时 URL
storage_path = f"attachments/{category}/{user_id}/{checksum}{extension}"
修改后(FileStorageService):
# 使用 FileStorageService 上传(带去重)
import os
file_meta = await self.file_storage.upload_file(
file_content=content,
filename=file.filename,
content_type=file.content_type,
category=f'attachment_{category}',
user_id=user_id
)
extension = file_meta.extension
checksum = file_meta.checksum
file_url = file_meta.file_url
storage_path = file_meta.storage_path
关键变更:
- 使用
FileStorageService.upload_file()上传文件 - 自动计算 SHA256 校验和
- 自动检查文件是否已存在(去重)
- 如果文件已存在,增加引用计数并返回已有文件信息
- 如果文件不存在,上传到 MinIO 并记录到 file_checksums 表
3. 存储提供商更新
修改前:
attachment = Attachment(
# ...
storage_provider='local', # 临时使用 local
storage_path=storage_path
)
修改后:
attachment = Attachment(
# ...
storage_provider=file_meta.storage_provider, # 从 FileStorageService 获取
storage_path=storage_path
)
4. 预签名 URL 生成
修改前(临时实现):
# 暂时返回文件 URL
return attachment.file_url
修改后(FileStorageService):
# 生成预签名URL
return await self.file_storage.get_presigned_url(
attachment.storage_path,
expires=expires
)
关键变更:
- 使用
FileStorageService.get_presigned_url()生成临时访问链接 - 支持自定义过期时间(默认 3600 秒)
- 返回 MinIO 预签名 URL
5. 文件删除集成
修改前(临时实现):
# 软删除数据库记录
await self.repository.soft_delete(attachment_id)
# TODO: 减少文件引用计数
# await self.file_storage.decrease_reference_count(attachment.checksum)
修改后(FileStorageService):
# 软删除数据库记录
await self.repository.soft_delete(attachment_id)
# 减少文件引用计数(通过 checksum 查找 FileChecksum 记录)
file_checksum = await self.file_storage.get_by_checksum(attachment.checksum)
if file_checksum:
await self.file_storage.decrease_reference_count(file_checksum.id)
logger.info(f"附件删除成功: {attachment_id}")
关键变更:
- 使用
FileStorageService.get_by_checksum()查找文件记录 - 使用
FileStorageService.decrease_reference_count()减少引用计数 - 如果引用计数为 0,FileStorageService 会自动删除 MinIO 中的文件
功能对比
| 功能 | 修改前(临时实现) | 修改后(FileStorageService) |
|---|---|---|
| 文件上传 | 本地计算 checksum,生成临时 URL | 上传到 MinIO,自动去重 |
| 文件去重 | ❌ 不支持 | ✅ 基于 SHA256 自动去重 |
| 引用计数 | ❌ 不支持 | ✅ 自动管理引用计数 |
| 存储位置 | 本地文件系统 | MinIO 对象存储 |
| 下载链接 | 直接返回文件 URL | 生成预签名 URL(临时访问) |
| 文件删除 | 仅删除数据库记录 | 减少引用计数,无引用时删除文件 |
| 存储提供商 | 硬编码 'local' | 从配置读取(minio/s3/oss) |
工作流程
上传流程
用户上传文件
↓
AttachmentService.upload_attachment()
↓
验证用户、关联实体、权限、文件类型、文件大小
↓
FileStorageService.upload_file()
├─ 计算 SHA256 校验和
├─ 查询 file_checksums 表
├─ 如果存在 → 增加引用计数,返回已有文件信息
└─ 如果不存在 → 上传到 MinIO,插入新记录
↓
创建 Attachment 记录
↓
返回附件信息
下载流程
用户请求下载
↓
AttachmentService.get_download_url()
↓
验证权限
↓
增加下载计数
↓
FileStorageService.get_presigned_url()
├─ 调用 MinIO API
└─ 生成临时访问链接(有效期 3600 秒)
↓
返回预签名 URL
删除流程
用户删除附件
↓
AttachmentService.delete_attachment()
↓
验证权限
↓
软删除 Attachment 记录
↓
FileStorageService.decrease_reference_count()
├─ 减少引用计数
├─ 如果引用计数 > 0 → 更新记录
└─ 如果引用计数 = 0 → 删除 MinIO 文件 + 删除 file_checksums 记录
↓
完成删除
文件去重示例
场景:多个用户上传相同文件
用户 A 上传 avatar.jpg:
# 1. 计算 checksum = "abc123..."
# 2. 查询 file_checksums 表 → 不存在
# 3. 上传到 MinIO: attachment_image/user_a/abc123.jpg
# 4. 插入 file_checksums 记录:
# - checksum: "abc123..."
# - file_url: "http://minio/jointo/attachment_image/user_a/abc123.jpg"
# - reference_count: 1
# 5. 创建 Attachment 记录(user_a)
用户 B 上传相同的 avatar.jpg:
# 1. 计算 checksum = "abc123..."(相同)
# 2. 查询 file_checksums 表 → 存在!
# 3. 增加引用计数: reference_count = 2
# 4. 返回已有文件信息(不重复上传)
# 5. 创建 Attachment 记录(user_b),使用相同的 file_url
结果:
- MinIO 中只存储一份文件
- file_checksums 表中 reference_count = 2
- 两个 Attachment 记录指向同一个文件
用户 A 删除附件:
# 1. 软删除 Attachment 记录(user_a)
# 2. 减少引用计数: reference_count = 1
# 3. 引用计数 > 0,保留 MinIO 文件
用户 B 删除附件:
# 1. 软删除 Attachment 记录(user_b)
# 2. 减少引用计数: reference_count = 0
# 3. 引用计数 = 0,删除 MinIO 文件
# 4. 删除 file_checksums 记录
优势
1. 存储空间节省
- 相同文件只存储一次
- 大幅减少存储成本
- 适用于头像、封面等常见场景
2. 上传速度提升
- 如果文件已存在,跳过上传
- 仅更新引用计数
- 用户体验更好
3. 安全性提升
- 使用预签名 URL 临时访问
- 支持自定义过期时间
- 避免文件 URL 泄露
4. 维护性提升
- 统一的文件存储逻辑
- 自动管理引用计数
- 支持定时清理无引用文件
测试验证
1. 代码诊断
✅ server/app/services/attachment_service.py: No diagnostics found
2. 功能测试
测试用例 1:上传新文件
POST /api/v1/attachments/upload
- 上传 avatar.jpg
- 验证文件上传到 MinIO
- 验证 file_checksums 表插入记录
- 验证 Attachment 记录创建
测试用例 2:上传重复文件
POST /api/v1/attachments/upload
- 上传相同的 avatar.jpg
- 验证不重复上传到 MinIO
- 验证 reference_count 增加
- 验证 Attachment 记录创建
测试用例 3:获取下载链接
GET /api/v1/attachments/{id}/download-url
- 验证返回预签名 URL
- 验证 URL 可以访问
- 验证 URL 在过期时间后失效
测试用例 4:删除附件
DELETE /api/v1/attachments/{id}
- 删除 Attachment 记录
- 验证 reference_count 减少
- 如果 reference_count = 0,验证 MinIO 文件删除
配置要求
环境变量
确保 .env 文件包含以下配置:
# MinIO 配置
MINIO_ENDPOINT=localhost:6185
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
MINIO_BUCKET_NAME=jointo
MINIO_SECURE=false
MINIO_PUBLIC_URL=http://localhost:6185
# 存储提供商
STORAGE_PROVIDER=minio
Docker Compose
确保 MinIO 服务已启动:
docker-compose up -d jointo-server-minio
后续工作
1. 定时清理任务
创建 Celery 定时任务清理无引用文件:
@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} 个无引用文件")
2. 单元测试
创建 server/tests/unit/test_attachment_service.py:
- 测试文件上传(去重)
- 测试预签名 URL 生成
- 测试文件删除(引用计数)
3. 集成测试
创建 server/tests/integration/test_attachment_api.py:
- 测试完整的上传流程
- 测试文件去重场景
- 测试下载链接生成
- 测试删除流程
4. 性能监控
添加性能监控指标:
- 文件去重率
- 存储空间节省
- 上传速度提升
- 引用计数分布
文件变更
修改文件
server/app/services/attachment_service.py- 导入 FileStorageService
- 初始化 file_storage 实例
- 集成文件上传逻辑
- 集成预签名 URL 生成
- 集成文件删除逻辑
相关文档
变更日期:2026-01-28
集成完成:✅ AttachmentService 已完全集成 FileStorageService