Skip to content

文件与存储 ​

整体设计 ​

文件与存储能力统一封装在 backend/app/framework/file/ 中,为上层业务提供与具体存储介质无关的能力。业务代码只依赖统一的 FileClient 接口,无需关心文件最终落在本地磁盘还是 S3 对象存储。

框架采用策略模式 + 工厂模式:每种存储类型是一个 FileClient 策略实现,业务层通过 FileClientFactory 按「文件配置编号(configId)」取得对应客户端。存储配置来自数据库 infra_file_config 表,由工厂按 configId 缓存客户端实例,避免重复创建底层连接。

核心文件:

  • base.py:FileClient 抽象接口、AbstractFileClient 模板方法、FileClientProperties 配置属性、FileUploadResult / FileTestResult 结果对象
  • factory.py:FileClientFactory 单例工厂
  • client_local.py:本地磁盘策略实现 LocalFileClient
  • client_s3.py:S3 协议策略实现 S3FileClient
  • util.py:与存储无关的安全校验工具 FileUploadUtil

核心概念 ​

FileClient 策略接口

所有存储客户端都实现统一接口,业务层只面向接口编程:

  • upload(content, path, content_type):写入文件内容,返回 FileUploadResult(访问 URL + 存储路径)
  • delete(path):按路径删除物理文件
  • get_content(path):读回文件二进制内容(下载)
  • get_url(path):根据存储配置计算可访问 URL
  • test():连通性测试(真实写入并读回一个测试文件后清理)
  • presign_put_url(path) / presign_get_url(url, expiration_seconds):预签名地址,仅 S3 支持,本地存储会抛 NotImplementedError

FileClientProperties 配置属性

由 file_config.config 字典(JSON)构造,包装成强类型属性。已兼容前端驼峰(如 basePath、accessKey)与小写加下划线两种命名。关键字段:

  • storage:存储类型,对应 InfraFileStorageEnum(LOCAL=10 / S3=20)
  • 本地磁盘:base_path(存储根路径,限定在 STATIC_ROOT 之内)
  • S3:access_key / secret_key / endpoint / bucket / domain / region / enable_path_style_access / enable_public_access

FileClientFactory 工厂

单例工厂,按 configId 缓存已创建的客户端。调用 create_or_update_client(properties) 创建或更新客户端(更新配置时重建底层连接),调用 get_client(config_id) 取缓存实例。未注册某存储类型时抛出明确提示,需在工厂的 _STORAGE_CLIENT_CLASS 注册表中补充。

AbstractFileClient 模板方法

抽取各存储通用的初始化与 format_file_url 逻辑,子类只需实现与具体存储相关的 IO 行为。format_file_url 负责拼接访问 URL:配置了 domain 时以其为前缀;否则回退到统一的静态访问前缀 ${ROOT_PATH}/${STATIC_URL}/${base_path}/${path}。

存储类型 ​

本地磁盘 LocalFileClient

文件写入服务器本地磁盘(基于 settings.STATIC_ROOT,缺省子目录为 settings.UPLOAD_DIR_NAME)。

  • 根目录必须处于 STATIC_ROOT 之内,避免文件落到静态目录外无法访问(_root 属性做校验)
  • 每次 IO 都做路径穿越校验(_resolve),拦截 ../ 等非法路径
  • 静态目录通过 app_init.py 中的 StaticFiles 挂载,因此本地文件可直接通过 URL 访问

S3 对象存储 S3FileClient

基于 aiobotocore 的异步实现,兼容任意符合 S3 协议的服务:MinIO、阿里云 OSS、腾讯云 COS、七牛云、华为云 OBS 等。

  • 支持普通 upload / get_content / delete
  • 支持预签名:presign_put_url 用于前端直传、presign_get_url 用于生成带时效的私有读取地址
  • domain 未配置时按 bucket + endpoint 推断访问域名(MinIO 为 {endpoint}/{bucket},其余云厂商为 https://{bucket}.{endpoint})
  • enable_public_access 为真(默认)时,presign_get_url 直接返回公开访问地址而非预签名

文件校验与安全 ​

FileUploadUtil 提供与存储类型无关的安全校验,供策略客户端与业务 service 复用:

  • sanitize_filename:清理文件名,移除 <>:"/\|?* 等危险字符与冗余点号,防止伪造路径
  • check_path_traversal / sanitize_target_path:拦截 ../、\0 等路径穿越,对业务目录前缀做清洗
  • is_dangerous_extension / validate_file_extension:基于 settings.DANGEROUS_EXTENSIONS 黑名单(.py、.php、.exe、.sh、.sql 等可执行/脚本类扩展名)拦截危险文件
  • detect_file_type:通过文件魔数(JPEG/PNG/GIF、ZIP、PDF、Office 等)识别真实类型
  • validate_file_content_type:比对声明扩展名与真实类型,不一致仅告警
  • check_file_size:基于 settings.MAX_FILE_SIZE(默认 50MB)统一校验大小

业务层用法 ​

业务模块通过 InfraFileService(基础设施文件服务)或直接调用框架方法接入存储能力:

  • upload(file, directory):上传文件并落库,自动按主配置选择客户端、生成唯一路径(业务目录/日期目录/UUID.扩展名)、返回含 url 的记录
  • presigned_url(name, directory):获取预签名地址;本地存储降级为后端直传地址
  • download_service(file_path) / delete_by_path_service(file_path):与存储无关、脱离请求上下文的通用下载/删除(供任务队列等场景复用,内部自建短会话读取主配置)
  • get_file_content(config_id, path):按 configId + 路径取回二进制内容,用于公开下载接口 GET /{configId}/get/**,可直接用于 <img src> 等场景
  • 列表/详情查询会通过 _fill_urls 根据存储配置动态生成访问 URL(不落库)

其他业务模块直接复用示例:

  • 知识库文档:InfraFileService(self.auth).upload(file, directory="ai/knowledge"),删除时调用 delete_by_path_service(path)
  • 用户头像、聊天附件等:同样基于主存储客户端,屏蔽底层差异

配置说明 ​

相关配置位于 app/config/settings.py:

  • STATIC_ENABLE / STATIC_URL / STATIC_ROOT:静态文件服务开关、路由与目录,本地存储依赖此挂载
  • UPLOAD_DIR_NAME:本地存储默认基础路径(位于 STATIC_ROOT 下)
  • MAX_FILE_SIZE:最大文件大小(默认 50MB),对所有落库上传生效
  • DANGEROUS_EXTENSIONS:危险扩展名黑名单,命中即拒绝上传
  • ROOT_PATH:API 前缀,参与本地文件访问 URL 拼接

实际使用的存储类型、主配置与连接参数在「基础设施 - 文件配置」中维护(管理端界面见 基础设施-《文件与存储》)。系统中需至少一条 master=true 的主配置,否则上传会提示「未配置主存储」。可在管理端对每条配置执行「测试」以验证连通性(底层调用 client.test(),真实读写后清理)。

前端使用 ​

前端上传能力集中在 apps/web-ele/src/components/upload/,通过 useUpload(directory) 钩子统一暴露 httpRequest,内置两种上传模式:

  • 后端直传(SERVER):默认模式,调用 POST /infra/file/upload,由后端选择主存储并写入
  • 前端直连(CLIENT):当 VITE_UPLOAD_TYPE=client 时,依次调用 GET /infra/file/presigned-url 获取预签名地址 → 直接 PUT 到 S3 → 再 POST /infra/file/create 异步登记文件信息(仅 S3 存储支持)

业务组件(图片上传、文件上传、富文本图片、表单设计器上传规则等)均基于 useUpload 封装,按业务目录(如 infra/file、system/avatar、ai/knowledge)区分存储位置。相关 API 见 api/infra/file/index.ts 与 api/infra/file-config/index.ts。