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.
 

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 类:

  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 表:

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

文件变更

新建文件

  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

相关文档


变更日期:2026-01-28
代码行数:670 行
文件数量:11 个(7 新建 + 4 修改)