仓库地址 https://github.com/Jacob9102/fnos-file-tool · 问题反馈 https://github.com/Jacob9102/fnos-file-tool/issues
在飞牛 NAS 上创建 / 编辑 / 管理文件的工具,只依赖 Python 标准库,一份源码两种交付:
| 交付形态 | 打开方式 | 鉴权 | 目录范围 |
|---|---|---|---|
| fpk 原生应用 | 飞牛桌面图标,桌面窗口内嵌打开 | 飞牛登录 + 应用账号密码 | 只允许你在「应用设置 → 访问权限」里授权的目录 |
| Docker 服务 | 浏览器访问端口 | 应用账号密码 | 由 compose 的挂载决定 |
能力:23 种文件格式(含空白 Excel / Word / PPT / PDF)、多级建目录并修正权限、批量创建、 上传下载、在线编辑、重命名/复制/移动/删除、chmod/chown,全部在网页里完成。
适用场景:飞牛的文件管理器或其他应用不让你新建文件(共享文件夹根目录权限是
000、目录属主是 root、 应用数据目录只读等),但你确实需要在那里落一个文件(配置文件、.strm、.nfo、说明文档、密钥文件…)。
这不是玄学,实测根因有三条:
| 现象 | 实测结果 | 本工具的解决方式 |
|---|---|---|
| 普通用户写共享目录被拒绝 | touch /vol1/1000/x.txt → Permission denied(共享根目录权限是 d---------) |
容器以 root 运行 + bind mount 挂载 /vol1,直接绕过用户态权限 |
| 目录属主是 root / 其他用户 | 实测:trimafs 按目录本地权限元数据授权,目录属主对其中 root 创建的文件仍有完整读/写/删权限,所以工具建的文件在飞牛里立刻可用,通常不需要 chown | 仍提供 chown 接口,需要归属普通用户时可批量改 |
新建目录会被落成 000 |
同一句 mkdir:容器 /tmp 上得 755,而 /vol1 共享目录上得 000 |
创建时对每一级目录显式 chmod,否则建出来的目录在飞牛里根本进不去 |
第 3 条是踩坑重点:很多"能在飞牛上建文件"的脚本只 mkdir 不 chmod,结果目录是 000,
在飞牛文件管理器里既看不见也进不去,看起来就像"没建成功"。
- ✅ 新建文件(可选格式、可带初始内容、可指定权限),父目录不存在自动创建并修正权限
- ✅ 文件格式下拉框:23 种格式。文本类(txt/json/html/sh…)预填该格式的默认内容; 二进制类(xlsx/docx/pptx/pdf/zip/png/gif/jpg)直接生成空白标准文件,其"初始内容"框会被禁用
- ✅ 扩展名自动补全:选了格式就不用在文件名里手打后缀(
readme+ txt →readme.txt); 名称自带扩展名(含.env这类点文件)时以名称为准,另有无扩展名档位用于Dockerfile/LICENSE - ✅ 新建文件夹,支持
a/b/c多级一次创建,每一级都正确 chmod - ✅ 批量创建:一次粘贴几十个文件名 / 目录名,也可统一指定格式
- ✅ 上传 / 下载:任意大小、任意类型(原始二进制流)
- ✅ 在线编辑:网页里直接改文本内容,
Ctrl+S保存(≤MAX_EDIT_MB) - ✅ 重命名、复制、移动、删除(目录可递归)
- ✅ 改权限 chmod(递归)、改属主 chown(递归,且不会破坏原权限位)
- ✅ 账号密码登录:首次打开设置账号,之后每次打开需登录,登录后可修改用户名/密码
- ✅ 目录授权:只允许操作飞牛「应用设置 → 访问权限」里授权过的目录,未授权的存储空间完全不可见
- ✅ 挂载根目录白名单 + 会话鉴权;未登录时所有文件接口一律拒绝
新建一个 0 字节的 x.xlsx,Excel 打开时会直接报"文件格式无效"。所以二进制格式是真正构造出来的最小合法文件
(只用标准库:zipfile 组装 OOXML、zlib+struct 造 PNG、手写 PDF 交叉引用表):
| 格式 | 生成内容 | 已用真库验证 |
|---|---|---|
| xlsx | 含 1 个空白工作表 Sheet1 的工作簿 | openpyxl.load_workbook 打开成功,max_row=1, max_column=1 |
| docx | 含 1 个空段落的 Word 文档 | python-docx 打开成功,正文为空 |
| pptx | 1 页空白幻灯片(含母版/版式/主题,缺一 PowerPoint 就打不开) | python-pptx 打开成功,1 页 |
| A4 单页空白 PDF(xref 偏移按实际字节计算) | 交叉引用偏移校验通过 | |
| zip | 合法空压缩包(22 字节 EOCD) | zipfile 打开成功,包内为空 |
| png / gif / jpg | 1×1 空白图片 | Pillow 解码成功,尺寸 (1,1) |
在飞牛的 SSH 里拉取代码(或点 GitHub 页面上的 Code → Download ZIP 再传到飞牛):
cd /vol1/1000/docker
git clone https://github.com/Jacob9102/fnos-file-tool.git
# 没装 git 就下载 ZIP: https://codeload.github.com/Jacob9102/fnos-file-tool/zip/refs/heads/main
cd fnos-file-tool
# 先改 docker-compose.yml 里 volumes 的挂载目录(改成你真正要管理的目录)
bash deploy.shdocker compose up -d --build- 先在 SSH 里构建镜像(界面里构建需要能读取构建上下文):
cd /vol1/1000/docker/fnos-file-tool && docker build -t fnos-file-tool:1.1.0 .
- 打开飞牛 Docker → 项目 → 新增,粘贴
docker-compose.yml内容(把build: .那行删掉,保留image: fnos-file-tool:1.1.0),把各卷的宿主路径改成你的实际路径,部署即可。 - 访问
http://<飞牛IP>:8787/,首次打开会让你设置账号密码,设置完即登录。
端口冲突时改
docker-compose.yml里ports的左侧数字,例如"8790:8787"。
fnos-file-tool/
├── app.py # 后端:单文件,仅标准库(HTTP/路由/鉴权/格式生成/生命周期)
├── static/
│ ├── index.html # 前端:单页,无 CDN、无构建步骤,离线可用
│ └── uitest.html # 界面自测页:真实浏览器里自动点击每一个按钮
├── Dockerfile # Docker 版:python:3.12-alpine + 非 root 降权启动(可选)
├── docker-compose.yml # 部署配置(端口 / 挂载 / 开关)
├── deploy.sh # 一键部署(构建 + 启动 + 就绪检查)
├── entrypoint.sh # 容器入口,支持 PUID/PGID 降权
├── build-fpk.sh # 构建飞牛应用包:同步源码 + 生成图标 + 调用官方 fnpack
├── tools/make_icons.py # 图标生成(构建期用 Pillow,运行时不依赖)
├── fpk/ # 飞牛应用包工程
│ ├── manifest # 应用元信息(键值格式,非 JSON)
│ ├── ICON.PNG / ICON_256.PNG
│ ├── app/ui/config # 桌面入口:type=iframe + 统一网关
│ ├── app/ui/images/ # 桌面图标(按尺寸命名,{0} 会被替换)
│ ├── cmd/ # 生命周期:main / install_* / uninstall_* / upgrade_* / config_*
│ ├── config/{privilege,resource}
│ └── wizard/ # 安装向导目录(安装器要求存在)
├── tests/
│ ├── smoke.sh # 接口层 73 项
│ ├── auth.sh # 账号与登录 29 项
│ ├── fpk-sim.sh # 模拟飞牛环境 35 项(生命周期 + 网关 + 授权目录边界)
│ └── validate_formats.py # 空白 Office / 图片文件合法性 39 项
├── LICENSE # MIT
└── README.md
fpk/app/server/app.py、fpk/app/ui/index.html、fpk/LICENSE、fnos-file-tool.fpk都是 构建产物(已被.gitignore忽略),由build-fpk.sh从上面的源码重新同步生成, 仓库里不会出现两份会各自漂移的副本。
| 变量 | 默认 | 说明 |
|---|---|---|
PORT |
8787 |
容器内监听端口 |
ROOTS |
vol1=/mnt/vol1,... |
白名单根目录,标签=容器内路径,逗号分隔;路径必须与 volumes 挂载点一致 |
ALLOW_ANY_ROOT |
false |
true 时不做根目录限制(危险,等于容器内全盘可写) |
ALLOW_DELETE |
true |
设为 0 关闭删除接口 |
ALLOW_CHMOD |
true |
设为 0 关闭改权限接口 |
MAX_EDIT_MB |
4 |
在线编辑的最大文件体积 |
MAX_UPLOAD_MB |
2048 |
单次上传上限 |
ACCESS_LOG |
0 |
设为 1 时逐条打印请求日志(排查问题用) |
PUID / PGID |
0 / 0 |
默认 root(可写任意挂载目录)。设为 1000 等则降权运行,创建的文件直接归属该用户 |
environment:
ROOTS: "share=/mnt/share"
volumes:
- /vol1/1000/share:/mnt/share:rw # 只放行这一个目录比直接挂整个 /vol1~/vol4 安全得多:容器内只有 /mnt/share 可访问,
其他路径会被白名单直接拒绝(返回 403),实测已验证。
要加第二个目录就照抄一行,并在 ROOTS 里补上对应标签:
ROOTS: "share=/mnt/share,文档=/mnt/docs"
volumes:
- /vol1/1000/share:/mnt/share:rw
- /vol1/1000/文档:/mnt/docs:rw所有接口返回 JSON,成功恒为 {"ok": true, ...},失败为 {"ok": false, "error": "..."}。
除下面几个接口外,所有接口都需要登录(会话 Cookie):
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/auth/status |
查询是否已设置账号、是否已登录(免鉴权,前端据此决定显示「首次设置 / 登录 / 主界面」) |
| POST | /api/auth/setup |
首次设置账号 {username, password},仅在尚未设置时可用(用户名 2–32 字符,密码 ≥6 位) |
| POST | /api/auth/login |
登录 {username, password},成功后下发会话 Cookie(失败 5 次锁定 5 分钟) |
| POST | /api/auth/logout |
退出登录 |
| POST | /api/auth/password |
改用户名/密码 {currentPassword, username?, newPassword?} |
| GET | /api/meta |
服务信息、挂载根目录、运行身份(uid/gid)、formats 格式清单与 default_format(免鉴权,前端不写死格式表) |
| GET | /api/settings |
读当前生效的开关、已授权目录、运行模式(网关/TCP)与账号文件位置 |
| POST | /api/settings |
改开关 {allow_delete?, allow_chmod?, require_admin?, max_edit_mb?, max_upload_mb?},写回 settings.json 即时生效(目录授权不在此处改) |
| GET | /api/list?path= |
列目录 |
| GET | /api/read?path= |
读文件(文本返回 UTF-8,二进制返回 base64) |
| GET | /api/download?path= |
下载原始文件(目录会返回 400,请先压缩) |
| POST | /api/mkdir |
{path, name, mode?} 建目录,name 支持 a/b/c 多级 |
| POST | /api/touch |
{path, name, format?, content?, mode?, overwrite?} 建文件(见下方格式说明) |
| POST | /api/batch |
{path, names:[...], format?, content?, overwrite?} 批量建(以 / 结尾建目录,单次 ≤ 500 项) |
| PUT | /api/write |
{path, content, mode?} 覆盖写入文本(≤ MAX_EDIT_MB) |
| POST | /api/upload?path=&name= |
请求体为原始二进制,写入 path/name(≤ MAX_UPLOAD_MB) |
| POST | /api/rename |
{path, newName} |
| POST | /api/copy / /api/move |
{path, target, name?} |
| POST | /api/delete |
{path, recursive?} |
| POST | /api/chmod |
{path, mode, recursive?} |
| POST | /api/chown |
{path, uid, gid?, recursive?, mode?, keepMode?} |
format 取 GET /api/meta 里 formats[].key(如 txt / xlsx / none),默认 txt。规则:
- 名称自带扩展名时以名称为准:
{"name":"自定义.zzz","format":"txt"}→ 就是自定义.zzz。 - 名称没有扩展名时补上所选格式的扩展名:
{"name":"readme","format":"xlsx"}→readme.xlsx。 - 点文件不补:
.env/.gitignore视为用户就是要这个名字。 - 要完全无扩展名:用
format: "none"(Dockerfile、LICENSE等)。 - 二进制格式不接受手填内容:带
content会返回 400;不填内容则生成空白标准文件。 - 文本格式不填内容时写入该格式的默认模板(如 json →
{}、html → 网页骨架、sh → shebang 且权限 755)。 - 名称里可以带子目录:
{"name":"报告/2026/汇总","format":"xlsx"}会连目录一起建好并修正权限。
# 建一个空白 Excel(不用手打 .xlsx 后缀,也不填内容)
curl -s -b $JAR -c $JAR -H 'Content-Type: application/json' \
-X POST "$API/touch" -d '{"path":"/mnt/share","name":"月度汇总","format":"xlsx"}'
# 建一个空白 Word / PPT / PDF / ZIP / 图片同理,把 format 换成 docx / pptx / pdf / zip / png
# 文本格式:不填内容会用该格式的默认模板
curl -s -b $JAR -c $JAR -H 'Content-Type: application/json' \
-X POST "$API/touch" -d '{"path":"/mnt/share","name":"配置","format":"json"}'
# 批量建 3 个空白表格
curl -s -b $JAR -c $JAR -H 'Content-Type: application/json' \
-X POST "$API/batch" \
-d '{"path":"/mnt/share","names":["一月报表","二月报表","三月报表"],"format":"xlsx"}'URL 里的路径必须做百分号编码(浏览器会自动处理)。用
curl手写中文路径时请用--data-urlencode或 python 的quote,否则原始 UTF-8 字节可能撞上 HTTP 解析器保留字符而报 400。
JAR=/tmp/fnos-cookies.txt
# 先登录拿到会话 Cookie;首次使用把 auth/login 换成 auth/setup
curl -s -c $JAR -H 'Content-Type: application/json' \
-X POST "$API/auth/login" -d '{"username":"你的账号","password":"你的密码"}'
API='http://127.0.0.1:8787/api'
# 建一个文件(JSON 请求体里中文没问题)
curl -s -b $JAR -c $JAR -H 'Content-Type: application/json' \
-X POST "$API/touch" \
-d '{"path":"/mnt/vol1/1000","name":"说明.txt","content":"你好 fnOS\n","mode":"664"}'
# 读中文文件名(用 python 做 URL 编码)
P=$(python3 -c "import urllib.parse;print(urllib.parse.quote('/mnt/vol1/1000/说明.txt'))")
curl -s -b $JAR -c $JAR "$API/read?path=$P"
# 建多级目录并指定权限
curl -s -b $JAR -c $JAR -H 'Content-Type: application/json' \
-X POST "$API/mkdir" -d '{"path":"/mnt/vol1/1000","name":"a/b/c","mode":"775"}'
# 把某目录及子项属主改回飞牛的普通用户
curl -s -b $JAR -c $JAR -H 'Content-Type: application/json' \
-X POST "$API/chown" -d '{"path":"/mnt/vol1/1000/文档","uid":1000,"gid":1001,"recursive":true}'- 首次打开务必设置账号密码,并只把端口留在内网。 本服务以 root 运行、能写你挂载的所有目录,把端口暴露到公网等于把 NAS 文件系统开放出去。
- 不要挂载整个存储池,只挂需要操作的目录。
- 不要把端口暴露到公网。需要外网访问时请走飞牛自带的远程访问 / 反向代理,并叠加 HTTPS 与更强的鉴权。
- 容器固定以 root 运行是故意设计(否则无法写入飞牛的受限目录);如果你不需要这个能力,
在 compose 里设
PUID/PGID为普通用户,或者用ALLOW_DELETE=0/ALLOW_CHMOD=0关闭危险接口。 - 所有路径都会做
realpath归一化并校验是否在ROOTS白名单内,防止../与符号链接逃逸(有测试覆盖)。
五套测试,全部可在飞牛本机跑(不需要联网);合计 204 项(格式校验未装可选库时为 196 项):
bash tests/smoke.sh # 73 项:登录 + 接口层(增删改查 / 上传下载字节一致 / 权限 / 安全边界)
bash tests/auth.sh # 29 项:首次设置 / 登录 / 退出 / 改密 / 密码存储 / 会话
bash tests/fpk-sim.sh # 35 项:模拟飞牛应用环境跑生命周期与统一网关
python3 tests/validate_formats.py # 39 项(装可选库后)/ 31 项(仅标准库结构校验)界面层(真实浏览器自动点击,含登录流程):
http://<飞牛IP>:8787/uitest.html?run=1&user=uitest&pass=uitest123456
validate_formats.py 装了 openpyxl / python-docx / python-pptx / Pillow 时做深度校验
(真库打开生成的 xlsx/docx/pptx、Pillow 解码图片、核对 PDF 交叉引用偏移),没装则退化为结构校验。
| 项目 | 值 |
|---|---|
| 系统 | 飞牛 fnOS 1.2.0604(底层 Debian GNU/Linux 12) |
| Docker | 28.5.2 / Compose v5.2.0 |
| 存储池 | /vol1~/vol4(trimafs) |
| 实测结论 1 | 宿主普通用户 touch /vol1/1000/x.txt → Permission denied;应用内同一路径 → 成功 |
| 实测结论 2 | 共享目录的 d--------- 是真实 mode(getfacl 无 ACL 条目),但 trimafs 按目录本地权限元数据授权:uid 1000 能读/改/删 root 在其中创建的文件,非属主 uid 921 被拒 |
| 实测结论 3 | 同一句 mkdir:容器 /tmp 上得 755,/vol4/1000/... 共享目录上得 000 |
| 已修复的坑 | 6 个真实 bug(keep-alive 请求体污染、新建目录落成 000、网关前缀剥除等)的定位与修法见代码注释与提交历史 |
Q:忘记账号密码了怎么办?
删除账号文件后重启应用即回到「首次设置」。Docker 版在 ./data/auth.json,
fpk 版在 …/@appconf/fnos-file-tool/auth.json。
Q:在飞牛文件管理器里看不到刚建的文件?
先点飞牛的刷新。实测中文件是立刻可见且可编辑的;若确实看不到,可能是目录权限问题,
用 /api/chmod 把中间各级目录改成 755。
Q:创建出来的文件属主是 root,飞牛里改不动?
实测结论:不用改。 trimafs 按目录本地权限元数据授权,目录属主(如 uid 1000)对其中 root 创建的文件
仍然可以读、追加、删除(已实测通过)。若你的环境确实不行,两条路:调用 /api/chown 改成 1000:1001;
或把 compose 里 PUID/PGID 设为 1000/1001 重新部署 —— 但注意降权后,容器将失去写入本就对普通用户
不可写目录的能力(那正是本工具存在的理由),需自行权衡。
Q:docker compose up 报端口被占用?
改 docker-compose.yml 里 ports: - "8787:8787" 的左侧数字,或用 ss -ltnp | grep 8787 看谁在用。
Q:构建镜像时卡在 apk add?
容器内的包源需要外网。若 NAS 上网受限,可把 Dockerfile 的 python:3.12-alpine3.20 换成你本地已有的
镜像(例如 python:3.12-alpine),或给 Docker 配置代理后重试。
Q:为什么不用 Nginx + PHP 之类的现成文件管理器? 也可以。本工具的定位是「最小、无依赖、专治飞牛写权限」,代码量小到可以自己审一遍,镜像 60MB,秒级启动。
除了 Docker 版,本项目还能打包成飞牛的 原生应用包 .fpk,安装后在飞牛桌面上是一个图标,
点击后在桌面窗口内嵌打开(不另开浏览器窗口),走飞牛统一网关,不占用任何端口。
飞牛的桌面入口由 app/ui/config 声明,其中 type 决定打开方式:
type |
行为 |
|---|---|
iframe |
在飞牛桌面窗口内打开(本项目使用) |
url |
在浏览器标签页 / 外部 Web 视图中打开 |
本项目的 fpk/app/ui/config:
{
".url": {
"fnos-file-tool.main": {
"title": "文件创建器",
"icon": "images/icon_{0}.png",
"type": "iframe",
"protocol": "",
"gatewayPrefix": "/app/fnos-file-tool",
"gatewaySocket": "app.sock",
"url": "/app/fnos-file-tool",
"allUsers": false,
"control": { "accessPerm": "readonly" }
}
}
}用的是统一网关模式:应用只监听 ${TRIM_APPDEST}/app.sock(Unix Socket,权限 0770),
飞牛校验登录态后把 /app/fnos-file-tool/... 的请求转发进来。这样:
- 不占端口、不把服务暴露到局域网,鉴权交给应用账号 + 飞牛网关;
- 复用飞牛当前域名和 NAS 登录态;
protocol/port在网关入口下不参与路由,因此无需声明端口。
因为请求带前缀,服务端会把前缀剥掉后再路由,并在 HTML 里注入 <base href="/app/fnos-file-tool/">,
前端全部用相对地址调接口 —— 同一套前端在 Docker 直连(<base href="/">)和网关下都能正常工作。
fnos-file-tool/
├── app.py / static/ … ← 开发版源码(Docker 版与 fpk 版共用一份)
├── build-fpk.sh ← 同步源码 + 生成图标 + 调官方 fnpack 打包
├── tools/make_icons.py ← 图标生成(构建期用 Pillow,运行时不依赖)
└── fpk/ ← fpk 工程
├── manifest ← 应用元信息(键值格式,不是 JSON)
├── ICON.PNG / ICON_256.PNG
├── LICENSE
├── app/
│ ├── server/app.py ← 由 build-fpk.sh 从 ../app.py 同步
│ └── ui/
│ ├── config ← 桌面入口(type=iframe + 网关)
│ ├── index.html ← 由 build-fpk.sh 从 ../static/index.html 同步
│ └── images/icon_{0,64,128,256}.png
├── cmd/ ← 生命周期脚本:main / install_* / uninstall_* / upgrade_* / config_*
├── config/privilege ← run-as: root(读写任意目录的前提)
├── config/resource ← data-share 为空(不额外创建共享目录)
└── wizard/ ← 安装向导目录(安装器要求存在)
bash build-fpk.sh # 产出 fnos-file-tool.fpk脚本会用飞牛自带的 /usr/local/bin/fnpack(v1.2.4)打包;普通用户执行时自动通过
nsenter 借宿主命名空间调用它。不要手工拼 tar:fpk 内外层结构(app.tgz 不带 app/ 前缀)
必须与安装器期望一致,fnpack 还会自动写入 checksum。
# 方式一:命令行
appcenter-cli install-fpk /path/to/fnos-file-tool.fpk
appcenter-cli start fnos-file-tool
appcenter-cli status fnos-file-tool
# 方式二:飞牛桌面 → 应用中心 → 手动安装(若被禁用先执行 appcenter-cli manual-install enable)装好后飞牛桌面出现「文件创建器」图标,点击即在桌面窗口内打开。
| Docker 版 | fpk 原生版 | |
|---|---|---|
| 监听 | TCP 端口(默认 8787) | Unix Socket ${TRIM_APPDEST}/app.sock |
| 入口 | 浏览器直接访问 | 飞牛桌面内嵌窗口(iframe) |
| 鉴权 | 应用账号密码 | 应用账号密码(可叠加飞牛管理员校验) |
| 可访问目录 | 由 volumes 挂载决定 | 自动发现 /vol1…/volN(+ 系统授权目录) |
| 运行身份 | 容器内 root | 应用进程 root(config/privilege) |
| 运行依赖 | Docker | 系统 Python 3.8+(飞牛自带 python3,或商店的 python312) |
模式由环境变量决定:GATEWAY_SOCKET 存在 → 网关模式(cmd/main 会显式导出);
否则走 TCP。LISTEN_MODE=tcp|socket 可强制指定。
装好后在飞牛桌面点开「文件创建器」,会先看到首次使用:设置账号,设置用户名与密码(≥6 位)即完成注册并自动登录。
然后必须授权目录,否则界面里什么都看不到(这是刻意的安全边界):
飞牛桌面 → 应用中心 → 已安装 → 文件创建器 → 设置(齿轮) → 访问权限 / 授权目录 → 添加目录
保存后 config_callback 会自动重启应用加载新的授权目录(授权目录是通过环境变量
TRIM_DATA_ACCESSIBLE_PATHS 注入的,只在进程启动时读取一次)。之后刷新页面即可看到目录。
不确定授权有没有生效?登录后点右上角 ⚙ 设置,「已授权目录」里会列出应用实际收到的路径, 还能看到原始的
TRIM_DATA_ACCESSIBLE_PATHS值,方便核对。
登录后可在应用内 ⚙ 设置 里直接修改开关(会写回这个文件):
{
"require_admin": false, // 是否额外要求飞牛管理员身份(默认 false,以应用账号为准)
"allow_delete": true, // 是否允许删除
"allow_chmod": true, // 是否允许改权限/属主
"max_edit_mb": 4, // 在线编辑上限
"max_upload_mb": 2048 // 上传上限
}目录授权不在这里配置,必须走飞牛的「应用设置 → 访问权限」,确保授权边界只由系统设置决定。
账号文件为同一个目录下的 auth.json(PBKDF2-SHA256 加盐,权限 600);
忘记密码时可删除该文件后重启应用,重新走首次设置。
- 登录是唯一入口:未登录时所有文件接口返回 401(未设置账号时返回 409)。
密码用
hashlib.pbkdf2_hmac加盐存储(20 万次迭代),会话是 HMAC 签名的 Cookie(HttpOnly,7 天)。 登录失败 5 次锁定 5 分钟。 - 目录授权是硬边界:只放行飞牛授权目录,
/vol1整个存储空间、/etc、/tmp等一律 403, 连浏览都不允许。所有路径仍做realpath归一化,..与符号链接逃逸均被拒绝。 - 网关=边界:Socket 权限 0770,只有飞牛网关能连;服务不对外暴露任何端口。
- 可选叠加飞牛管理员校验:设置里打开
require_admin后,非管理员的飞牛账号即使知道应用密码也会被 403。
bash tests/fpk-sim.sh # 35 项:模拟飞牛环境(生命周期 + 网关 + 登录 + 授权目录边界)
bash tests/auth.sh # 29 项:账号与登录(首次设置/登录/退出/改密/限流/持久化)
bash tests/smoke.sh # 73 项:登录 + 接口层(Docker 模式)
python3 tests/validate_formats.py # 39 项:空白 Office / 图片文件的合法性界面自测(真实浏览器自动点击,含登录流程):
http://<飞牛IP>:8787/uitest.html?run=1&user=uitest&pass=uitest123456
fpk-sim.sh 会造一套 target/var/etc/cmd,用 TRIM_* 环境变量跑
cmd/main start|status|restart|stop,通过 Unix Socket 带网关前缀访问接口,验证:
socket 权限 0770、重复 start 不产生双进程、<base> 注入、未设置账号时全部拒绝、
登录后可读写、未授权路径(/vol1、/etc、/tmp)一律 403、退出后 401、
改密与登录限流、设置持久化、新增授权目录后 config_callback 重启加载生效。
| 项 | 当前值 | 说明 |
|---|---|---|
maintainer / distributor |
Jacob9102 |
已按 GitHub 用户名填写;改署名时同步改 LICENSE 版权行 |
os_min_version |
未设置 | 建议在真机验证后按实际兼容版本填写 |
LICENSE |
MIT(版权行 Jacob9102) |
已填写 |
| 图标 | 脚本生成的蓝色文稿+加号 | 换设计图后重跑 tools/make_icons.py |
platform |
all |
纯 Python 标准库,无架构相关二进制 |
| 项 | 值 |
|---|---|
| 仓库 | https://github.com/Jacob9102/fnos-file-tool |
| 问题反馈 / 功能建议 | https://github.com/Jacob9102/fnos-file-tool/issues |
| 许可证 | MIT(见 LICENSE) |
| 维护者 | Jacob9102 |
| CI | 每次 push / PR 自动跑 tests/ 里的测试,见 .github/workflows/tests.yml |
欢迎提 Issue 与 PR(修 bug、加文件格式、补测试都行)。提交前建议先在本机跑一遍:
bash tests/smoke.sh && bash tests/auth.sh && bash tests/fpk-sim.sh && python3 tests/validate_formats.py