外观
文件与存储
整体设计
文件与存储能力统一封装在 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:本地磁盘策略实现LocalFileClientclient_s3.py:S3 协议策略实现S3FileClientutil.py:与存储无关的安全校验工具FileUploadUtil
核心概念
FileClient 策略接口
所有存储客户端都实现统一接口,业务层只面向接口编程:
upload(content, path, content_type):写入文件内容,返回FileUploadResult(访问 URL + 存储路径)delete(path):按路径删除物理文件get_content(path):读回文件二进制内容(下载)get_url(path):根据存储配置计算可访问 URLtest():连通性测试(真实写入并读回一个测试文件后清理)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。