# fn-dir-mapper
**Repository Path**: chenshaogui/fn-dir-mapper
## Basic Information
- **Project Name**: fn-dir-mapper
- **Description**: 函数目录映射可视化工具,自动解析代码仓库中的函数定义与目录结构,生成模块依赖关系图谱。支持多种编程语言,输出可视化图表帮助开发者快速理解项目架构、梳理调用链路、识别循环依赖,适用于代码重构、新成员熟悉项目等场景。
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-11-02
- **Last Updated**: 2026-11-06
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# FnDirMapper(飞牛目录映射器)
专为飞牛 fnOS 打造的轻量级目录软链接(Symlink)映射管理工具。跨存储空间 / 应用目录
建软链(如 `/vol1/1000/docker/xxx → /vol2/1000/xxx`)不再需要 SSH 敲 `ln -s`,
浏览器里点几下即可完成,并对历史映射做持久记录与有效性校验。
## 功能
- 🔍 创建前实时预检:输入路径即给出结论,不靠提交后报错——目标是否存在 / 是否为空目录 /
子项数量与体积、链接位置是空闲、文件、非空目录还是已有链接,均逐项标注
- 📁 可视化目录浏览器:面包屑 + 存储空间快捷入口 + 上一级,可切换「只看目录 / 显示文件」,
显示每个目录的项数与空目录提示、链接指向与失效标记,并可在当前目录**新建文件夹**
- ➕ 一键创建软链接:`ln -s 目标 源` 的图形化等价;「一键同名映射到其它存储空间」自动推导源路径;
链接位置的上级目录不存在时可勾选**自动创建缺失的上级目录**(新目录继承上级属主与权限,
避免 root 建完目录后应用写不了);同名冲突保护,真实文件与非空目录永不覆盖
- ✂️ 解除软链接:单条解除、勾选后批量解除;只 `unlink` 链接本身,永不递归删除真实数据
- 📋 映射记录管理:状态徽标(有效 / 目标失效 / 已被真实目录替代 / 目标已变更 / 链接不存在)、
关键字搜索、状态筛选、异常优先排序、单条重新校验、备注编辑
- 🗄️ 存储空间概览:顶栏展示各 `/volN` 可用容量与使用率进度条
- 🧭 「收编」SSH 手工创建的链接:浏览时直接登记进映射记录统一管理与校验
- 📱 手机端适配:≤ 860px 表格转卡片,≤ 620px 重排为拇指友好布局(顶栏存储空间横向滑动、
按钮铺满、弹窗全屏 / 确认框底部抽屉、长路径换行不截断、避开刘海与 Home 条)
- 🌙 自适应界面:跟随系统的浅色 / 深色主题
## 架构(对齐官方 Native 案例)
- 后端:零依赖 Node(`server/`),fnOS 下直接监听统一网关 unix socket `app.sock`,
本地开发退回 TCP 5612
- 前端:原生 HTML/JS(`web/`),由同一服务托管,相对路径调 API,网关前缀无关
- 运行时:manifest 声明 `install_dep_apps=nodejs_v22`,不内置 node,fpk 体积 < 1MB
- 权限:`config/privilege` 使用 `run-as: root`——跨卷跨属主创建 symlink 必须特权;
安全兜底:统一网关先校验 fnOS 登录态,后端强制「源必须位于存储空间(`/volN`)、目标必须存在、
拒绝 `..`、双向递归防护、只 unlink 链接 / 只 rmdir 空目录」;页脚展示服务运行身份(uid)
- 记录文件:`TRIM_PKGETC/links.json`(fnOS 上为 `/var/apps/fn-dir-mapper/etc`,卸载随应用清除)
## 目录结构
```text
fn-dir-mapper/
├── package/ # fnOS 应用包(fnpack build --directory package)
│ ├── manifest
│ ├── config/ # privilege / resource
│ ├── cmd/ # 生命周期脚本(main 负责 start|stop|status)
│ ├── app/ui/ # 桌面入口配置 + 图标
│ └── app/{server,web}# 由 scripts/sync-package.mjs 生成(已 gitignore)
├── server/ # 后端源码(零依赖)
├── web/ # 前端源码(原生 JS,含生成的图标)
├── scripts/ # 图标生成 / sync / fnpack 打包 / 冒烟测试 / 界面静态校验
└── package.json # npm scripts 入口
```
## 后端接口
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/api/health` | 服务信息(含运行身份 uid / gid / 用户名) |
| GET | `/api/volumes` | 存储空间列表与容量(`/volN` + statfs) |
| GET | `/api/browse` | 目录浏览(`path`、`onlyDirs`),返回项数 / 链接数 / 断链数统计 |
| GET | `/api/stat` | 单个路径详情:是否存在 / 类型 / **是否为空** / 子项统计与体积 |
| GET | `/api/preflight` | 创建前预检:`errors` / `warnings` / `infos` / `needForce` / `needMkdir` / `parentPath` / `suggestedSource` |
| GET | `/api/links` | 映射记录列表(`check=1` 同时校验) |
| GET | `/api/scan` | 扫描目录下已存在的软链接(含未登记的) |
| POST | `/api/link` | 创建软链接(`source`、`target`、`note`、`force`、`makeParent`) |
| POST | `/api/mkdir` | 新建目录(逐级创建,属主与权限继承最近的已有目录) |
| DELETE | `/api/link` | 解除链接(`recordOnly=true` 时仅删记录) |
| POST | `/api/link/record` | 收编手工链接 |
| POST | `/api/link/note` | 更新备注 |
| POST | `/api/check` | 有效性校验(带 `sources` 则只查指定记录) |
预检(`/api/preflight`)与创建(`/api/link`)共用同一套判定函数,界面结论不会与实际行为不一致。
存储空间路径按 `/^\/vol\d*(\/|$)/` 判定:飞牛的存储空间是**根目录下的 `/vol1`、`/vol2` …**,
并不是 `/vol` 的子目录,所以不能按 `/vol/` 前缀判断(否则 `/vol1/…` 会被误拒)。
入口地址不带尾斜杠时(网关给的就是 `/app/{appname}`)服务会 302 补上斜杠:否则页面里的
相对资源 `styles.css` / `app.js` 会被解析到上一级而 404,表现为“无样式 + 点击无响应”;
前端另有 `` 与 `apiBase()` 双重兜底。
## 界面与移动端适配
断点分两层:`≤ 860px` 把两列输入合并为一列、映射表格转成卡片(勾选框 / 映射 / 状态一行,
操作按钮独占下一行);`≤ 620px` 针对手机:顶栏存储空间改成横向滑动单行、主操作按钮铺满、
目录选择器全屏、确认框改底部抽屉、长路径换行完整展示。另有几条触屏专属规则:
- 输入控件 `font-size: 16px`——iOS Safari 对小于 16px 的输入框会在聚焦时放大整个页面;
- `@media (hover: none)` 收敛 hover 态——触屏点完样式会“粘”在悬停色上不还原;
- `?` 提示用 `data-tip` 气泡而不是 `title`——触屏不会触发 title,否则说明在手机上永远看不到;
- 弹窗容器需 `min-height: 0`——grid item 默认 `min-height:auto` 会被内容撑破,
目录选择器底部那一排按钮会被顶出屏幕外(已用 390px 视口实测)。
列宽写在 `table.links .c-*` 类上而不是内联 `style`:内联样式盖不住,窄屏的卡片重排会失效。
## 已知限制(真机实测)
**飞牛文件管理器把软链接显示成「文件」,手机 App 里还点不开。**实测
`/vol1/1000/测试 → /vol2/1000/ssd980/docker/compose/kasm_telegram` 在 PC 端「文件管理」里
类型列显示的是链接名、大小 46 B,恰好等于目标路径字符串的长度;加尾斜杠重建后是 47 B、
表现完全一致。说明它是按 `lstat` 判类型、不做跟随,**不是链接建错了**(尾斜杠方案已被实测证伪)。
- 影响范围:只影响在飞牛文件管理器 / 手机 App 里把它当文件夹浏览;
应用(docker 等)读写穿透软链接由内核完成,不受影响。
- 本应用的「浏览…」入口按 `stat`(跟随)判类型,所以能正常进入映射出来的目录。
- `namei -l` 里 `/vol1`、`/vol2/1000/ssd980` 等级目录显示为 `d---------`,但 PC 端能正常列出
其内容,说明飞牛的实际访问控制不靠这 9 位(走 ACL),它不是“打不开”的原因,别再往这个方向查。
- 想在文件管理器里当真实文件夹用,只有 `mount --bind` 一条路(需 root、重启不保留、
误操作风险与删链接不同类),与本应用「只建/删软链接、不碰挂载点」的安全边界冲突,故未实现。
- 手机 App 里要看这些数据,直接切到目标所在的存储空间浏览即可。
## 本地开发(Windows)
```powershell
npm start # 打开 http://127.0.0.1:5612/app/fn-dir-mapper/(不带尾斜杠会 302 自动补上)
# Windows 本机没有 /vol 目录,调试时可放开源路径限制(此时也支持 D:/xxx 盘符绝对路径):
# $env:FNM_ALLOW_ANY_SOURCE="1"; npm start
npm run check:ui # 界面静态校验(8 项):HTML id / el 控件声明 / 接口路由 / CSS 类名 / 移动端适配规则
npm run icons # 重新生成应用图标(仅在调整图标后需要)
```
## 冒烟测试
服务端跑起来后执行(76 项用例:创建 / 断链校验 / 收编 / 预检 / 目录是否为空 / 自动建目录 / 安全拒绝)。
测试目录 `D:/fnm-test` 里的 fixture 由脚本自建并自清,可反复执行,不需要人工预置:
```powershell
$env:FNM_ALLOW_ANY_SOURCE="1"; npm start # 一个终端
$env:SMOKE_ROOT="D:/fnm-test"; npm run smoke # 另一个终端
```
关键安全用例:真实文件与非空目录**永远不会**被覆盖或删除(即使勾选了替换),
只有软链接和空目录可被接管;解除链接只 `unlink` 链接本身;
递归防护双向生效(源在目标内部、目标在源内部都会被拒)。
## 打包
```powershell
npm run sync # server/ web/ → package/app/(server.js 重命名为 server.cjs)
npm run build # 调用官方 fnpack 生成 dist/fn-dir-mapper_.fpk
```
fnpack 二进制默认自动下载;若本地已有可直接复用(避免联网):
```powershell
$env:FNPACK="D:\myProject\scrcpy-web-rtc\fnos-app-native\.tools\fnpack.exe"; npm run build
```
图标由脚本生成(fnpack 硬性要求这些文件存在):
```powershell
npm run icons # scripts/gen-icons.mjs:自绘位图 + 自写 PNG 编码,产出下列文件
```
- `package/ICON.PNG`(64)、`package/ICON_256.PNG`(256)
- `package/app/ui/images/icon_{64,128,256}.png`(桌面入口)、`web/images/`(页面内 logo / favicon)
> 图标语义:左侧描边文件夹(软链接别名)→ 箭头 → 右侧实心文件夹(真实目录),
> 右上角绿点表示映射有效。改图形请改 `scripts/gen-icons.mjs` 里的形状参数与配色,
> 不要手改 PNG。
## 安装测试清单(对齐官方文档)
- [ ] 应用中心本地安装 fpk,能正常安装 / 启动 / 停止,状态显示正确
- [ ] 桌面卡片打开 `/app/fn-dir-mapper` 页面正常
- [ ] 创建 `/vol1/… → /vol2/…` 链接,SSH `ls -l` 验证
- [ ] 删除目标目录后点「校验有效性」,应显示「目标失效」
- [ ] 解除链接后目标数据完好
- [ ] 卸载后重新安装,服务正常拉起
- [ ] 应用中心「停用 / 启用」(走 config_callback 重启)生效
- [ ] 预检提示:选空目录作目标时提示“目标为空”;选非空目录作链接位置时被拦下
- [ ] 只填「目标目录」时,目标框下不再出现“路径必须是以 / 开头”(缺失参数不等于非法路径,错误按字段归属)
- [ ] 链接位置的上层目录还不存在时,勾选可自动创建,且 `ls -l` 确认新目录属主不是 root
- [ ] 目录浏览器里「新建文件夹」可用;报错信息中的路径与实际访问的一致
- [ ] 手机上(≤ 620px):顶栏可横滑、按钮好点、目录选择器底部四个按钮都在屏内、长路径完整可见
- [ ] 手机上批量勾选解除、搜索与状态筛选可正常操作
## 参考
- https://developer.fnnas.com/docs/examples/native/
- https://developer.fnnas.com/docs/core-concepts/privilege/
- https://developer.fnnas.com/docs/core-concepts/resource/
- https://developer.fnnas.com/docs/core-concepts/framework/