- 配置加载从三层(默认值→confs→环境变量)改为两层(confs 强依赖→环境变量覆盖) - confs 连接失败直接 sys.exit(1),不再静默使用内置默认值 - _dnfs_config 初始值改为空字符串白名单模板,移除内置默认值 - 新增配置完整性校验:confs 返回后检查所有必需 section/key 非空 - 区分错误类型:连接拒绝/RpcError/通用异常分别打印明确错误信息到 stderr - confs 连接地址改为引导配置(环境变量优先,默认 127.0.0.1:8010),移除 dnfs.ini 中 [confs] 段 - 启动顺序调整为 confs → db → dnfs → ai_review/...(dnfs 同时依赖 confs 和 db) - 更新 dnfs/README.md 与 confs/README.md 文档
13 KiB
dnfs — 树状网络文件系统
Dendrachy Network File-System,基于父子关系(pid)组织的树状节点服务。
监听 127.0.0.1:8006,提供节点的增删改查,支持以 Base64 编码形式在 content 中存储文件数据。所有写操作需携带由 gateway 签发的 Token。
支持特殊节点(管理员专属):content 为信封 JSON 时,前端按类型渲染为链接卡片 / 可运行网页 / 图片 / 文件下载,详见特殊节点。
架构
客户端 (static/dnfs.html)
│ Bearer Token
▼
dnfs :8006 (Flask + Waitress)
│
├──► db :8005 数据持久化(SQL-over-HTTP,/execute)
└──► ai_review :8007 内容审核(仅 add_note / update_note 时调用)
- 不直连数据库,所有数据操作通过 HTTP 调用
db服务(http://127.0.0.1:8005/execute)。 - 新增/更新节点前调用
ai_review服务(http://127.0.0.1:8007/review)进行内容审核,审核不通过则拒绝写入。 - 通常部署在 gateway 之后(
Proxy.opened白名单中),由 gateway 完成 Token 验签后转发,dnfs 仅做所有权校验。
数据模型
对应 db 服务中的 Notes 表(见 db/create_sqlite3.sql):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INTEGER PK | 节点唯一标识 |
| pid | INTEGER NOT NULL | 父节点 id,0 表示根 |
| owner | VARCHAR(128) | 创建者账号(来自 Token payload 的 user 字段,默认 'Unknow') |
| title | VARCHAR(128) NOT NULL | 节点标题 |
| content | TEXT NOT NULL | 节点内容(可含 Base64 文件数据) |
鉴权与所有权
写操作(add_note / delete_note / update_note)需在请求头携带:
Authorization: Bearer <base64_payload>_<rsa_sign>
- Token 由 gateway 在
/login签发,dnfs 通过 dnfs.py:163decode_token仅解 Base64 取 payload,不验签(验签由 gateway 完成)。 - payload 中取
user字段作为操作者,写入owner。 delete_note与update_note校验所有权:- 节点
owner必须等于当前user,否则403; - 例外:管理员(Token payload
is_admin=true,由 gateway 按[Admin] users白名单签发)可删除/更新任意节点。
- 节点
get_notes不需要鉴权。
特殊节点(管理员专属)
content 为以下信封 JSON 时即为特殊节点,前端 dnfs.js 按 _sp 类型差异化渲染;普通 Markdown 内容不受影响:
_sp |
信封字段 | 前端表现 |
|---|---|---|
link |
url(http/https,≤2048)、note? |
favicon 图标(经 static /api/favicon 代理拉取)+ 可点击链接 |
html |
code(≤512KB)、note? |
"审计源码"(纯文本展示)+ "运行"(新窗口执行)按钮 |
image |
name、data(data:image/ DataURL,≤3.5M 字符)、note? |
图片直接显示 |
file |
name、mime、data(Base64,≤3.5M 字符)、note? |
下载按钮(Base64→Blob→下载) |
规则:
- 仅管理员可发布/更新(
add_note/update_note中按is_admin强制,非管理员返回 403);前端发布面板也仅对管理员显示。 - AI 内容审核剔除 Base64 负载(体量大且无审核意义):
link审title+url+note,html审title+code 前 4000 字符+note,image/file审title+name+note。 {"_sp"开头但 JSON 非法的内容会被拒绝发布(400),防止半截信封流入。
接口文档
所有接口均为 POST,Content-Type: application/json。
POST /get_notes
获取指定父节点下的子节点列表。无需鉴权。
Request
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| pid | int | 否 | 父节点 id,0 表示根,缺省默认 0 |
curl -X POST http://localhost:8006/get_notes \
-H 'Content-Type: application/json' \
-d '{"pid": 0}'
Response 200 — 直接返回节点数组:
[
{"id": 1, "pid": 0, "owner": "admin", "title": "根节点", "content": "hello"},
{"id": 2, "pid": 0, "owner": "alice", "title": "另一个", "content": "world"}
]
空目录返回 []。
POST /add_note
添加新节点。流程:校验参数 → 校验 Token → 校验父节点存在 → AI 内容审核 → 写入数据库(含 owner)。
Request
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| pid | int | 是 | 父节点 id,0 为根,非 0 时校验父节点是否存在 |
| title | str | 是 | 节点标题,不可为空 |
| content | str | 是 | 节点内容,不可为空(可含 Base64 文件数据) |
请求头需带 Authorization: Bearer <token>。
curl -X POST http://localhost:8006/add_note \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{"pid": 0, "title": "新节点", "content": "节点内容"}'
响应
| 状态码 | 场景 | 响应体 |
|---|---|---|
| 200 | 成功 | {"success": true} |
| 400 | 缺少必填字段 | {"error": "Pid, title and content are required"} |
| 400 | 父节点不存在(pid≠0) | {"error": "the new node need a parent"} |
| 400 | AI 内容审核不通过 | {"error": "review failed"} |
| 400 | 数据库写入失败 | {"success": false} |
| 401 | 未携带 Token 或 Token 解析失败 | {"error": "Warning: no Authorization"} |
POST /delete_note
删除指定节点。流程:校验参数 → 校验 Token → 检查子节点 → 校验所有权 → 删除。
存在子节点时拒绝删除(保护树结构完整性)。admin 可删除任意节点,普通用户仅能删除自己拥有的节点。
Request
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int | 是 | 要删除的节点 id |
请求头需带 Authorization: Bearer <token>。
curl -X POST http://localhost:8006/delete_note \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{"id": 1}'
响应
| 状态码 | 场景 | 响应体 |
|---|---|---|
| 200 | 成功 | {"success": true} |
| 400 | 缺少 id | {"error": "Note ID is required"} |
| 400 | 存在子节点 | {"error": "this node has a child "} |
| 400 | 数据库删除失败 | {"success": false} |
| 401 | 未携带 Token | {"error": "Warning: no Authorization"} |
| 403 | 非本人节点且非 admin | {"error": "It's not your note"} |
POST /update_note
更新节点的 pid / title / content(pid 变更可用于迁移节点)。流程:校验参数 → 校验 Token → 校验所有权 → AI 内容审核 → 更新。
仅节点 owner 可更新;管理员(Token payload is_admin)豁免,可更新任意节点。
Request
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int | 是 | 要更新的节点 id |
| pid | int | 是 | 新的父节点 id(迁移时改变此值) |
| title | str | 是 | 新标题,不可为空 |
| content | str | 是 | 新内容,不可为空 |
请求头需带 Authorization: Bearer <token>。
curl -X POST http://localhost:8006/update_note \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{"id": 1, "pid": 2, "title": "新标题", "content": "新内容"}'
响应
| 状态码 | 场景 | 响应体 |
|---|---|---|
| 200 | 成功 | {"success": true} |
| 400 | 缺少必填字段 | {"error": "Invalid param"} |
| 400 | AI 内容审核不通过 | {"error": "review failed"} |
| 400 | 数据库更新失败 | {"success": false} |
| 401 | 未携带 Token | {"error": "Warning: no Authorization"} |
| 403 | 非本人节点 | {"error": "It's not your note"} |
安全特性
- 安全响应头(security_middleware.py):
X-Frame-Options: DENY、X-Content-Type-Options: nosniff、Referrer-Policy、Permissions-Policy、CSP(default-src 'self'、frame-ancestors 'none'),并移除Server头。 - Token 鉴权:写操作强制校验,payload 取
user字段。 - 所有权隔离:普通用户只能改/删自己的节点。
- 内容审核:写入与更新前调用
ai_review,色情/违禁/赌博/诈骗/暴力等内容会被拒绝。
配置
dnfs 已接入 confs 统一配置服务,confs 是 dnfs 启动的强依赖:
- confs 统一配置服务 — 必须成功从 confs 拉取
dnfs.ini,否则 dnfs 启动失败直接退出(exit code 1) - 环境变量覆盖(最高优先级)— Docker/部署环境通过环境变量覆盖 confs 中的值
配置文件
集中托管在 confs/configs/dnfs.ini,由 confs 服务分发:
| Section | Key | 说明 |
|---|---|---|
[server] |
host |
监听地址 |
[server] |
port |
监听端口 |
[database] |
host |
db 服务地址 |
[database] |
port |
db 服务端口 |
[database] |
timeout |
RPC 调用超时(秒) |
[ai_review] |
url |
AI 审核接口地址 |
[ai_review] |
timeout |
AI 审核请求超时(秒) |
引导配置(confs 连接地址)
confs 自身的连接地址不通过 confs 获取(避免循环依赖),通过以下方式确定(优先级从高到低):
- 环境变量
CONFS_HOST/CONFS_PORT - 默认值
127.0.0.1:8010
环境变量(最高优先级,覆盖 confs 配置值)
环境变量覆盖配置文件值,容器化部署时通过 docker-compose.yml 传入服务名:
| 变量 | 容器内值 | 说明 |
|---|---|---|
LISTEN_ADDR |
0.0.0.0 |
监听地址(容器内需绑定所有网卡) |
LISTEN_PORT |
8006 |
监听端口 |
DB_HOST |
db |
db 服务主机名(docker-compose 服务名) |
DB_PORT |
8005 |
db 服务端口 |
REVIEW_URL |
http://ai_review:8007/review |
ai_review 服务地址 |
CONFS_HOST |
confs |
confs 服务主机名(引导配置) |
CONFS_PORT |
8010 |
confs 服务端口(引导配置) |
启动顺序要求
confs 必须先于 dnfs 启动(启动顺序:confs → db → dnfs → ai_review/...)。若 confs 不可用:
- dnfs 打印致命错误到 stderr(包含 confs 地址和具体错误原因),以 exit code 1 终止
- 不会静默使用默认值启动
- 配置完整性校验:若 dnfs.ini 中缺少必需的 section/key 且无对应环境变量覆盖,同样报错退出
- 启动成功时控制台打印:
配置来源: confs (dnfs.ini @ host:port)
前端
三文件架构:
templates/index.html 页面结构(Font Awesome 图标)
static/css/style.css 样式
static/js/script.js 交互逻辑
功能:节点树浏览、新增/编辑/删除节点、文件 Base64 上传与下载、节点迁移(移动到其他父节点下)。
注:首页路由
@app.route('/')当前已注释,前端页面实际由static服务的dnfs.html承载。
依赖
flask
flask-cors
waitress
nuitka # 可选,编译为独立二进制
requirements.txt仍列有flask_sqlalchemy,但代码已不使用(SQLAlchemy 直连代码已注释),属遗留依赖。
运行时需要 db(:8005)和 ai_review(:8007)服务已启动;对外访问需经 gateway 转发并验签。
运行
bash _set.sh # 创建 venv + 安装依赖
bash _run.sh # 启动服务(waitress)
已知问题与风险
decode_token未验签(dnfs.py:167):仅解 Base64 取 payload,存在# FIXME: 验证数字签名标记。当前安全性依赖"必须经 gateway 转发"这一部署假设;若 dnfs 被直接访问(绕过 gateway),任意伪造的 Token 都能通过。建议补 RSA 验签或在部署上确保仅 gateway 可达。review_content返回类型不一致(dnfs.py:73-119):正常路径返回bool(L105),HTTPError 路径返回dict(L113)。调用处用True != review判断,HTTPError 时dict != True恒为真,会一律判为审核失败并吞掉具体错误信息。建议统一返回类型或在调用处 catch 异常。- CORS 全开:
CORS(app)未配置白名单,允许任意 Origin 跨域访问。考虑到写操作需 Token,风险可控,但仍建议按 gateway 的[CORS] allowed_origins方式收紧。 call_db_service异常未捕获:调用处(L208、L261、L318、L347、L440)直接取result['success']/notes['data'],若 db 服务返回非预期结构或网络异常会抛 500。get_notes不鉴权:任何能访问 dnfs 的人都能枚举所有节点内容。若内容含敏感 Base64 文件,需在 gateway 层做路径级鉴权。