# Stash Stash 是一个用 Rust 编写的本地静态页面托管工具。编译后的 React 控制台会被内嵌进二进制, 所以发布物只有一个 `stash` 可执行文件,不需要额外部署前端静态资源。 它能做两件事:托管目录下的多个静态前端项目、给控制台提供同源 JSON / SSE 接口。 ## 快速开始 ```bash # 1. 先构建前端(dist 会被内嵌,缺了它 cargo build 会直接失败) cd stash-web npm install npm run build cd .. # 2. 编译并运行 cargo run # 托管 ./public,监听 127.0.0.1:8080 # 自定义目录 / 端口 / 对外暴露 cargo run -- --root ./sites --port 3000 cargo run -- --root ./sites --host 0.0.0.0 --port 3000 ``` 启动后打开终端里打印的地址即可,例如 `http://127.0.0.1:8080/`。 默认托管目录 `./public` 不存在时会自动创建;首次启动还会与二进制同级创建 `data/`, 并在其中生成内容库 `stash.db`(已内置示例内容,可以直接用 SQLite 工具修改)。 环境要求: - Rust 工具链(2024 Edition) - Node.js 24,推荐用 [Volta](https://volta.sh/) 管理;`stash-web/package.json` 里已用 `volta` 字段固定版本 ## 工作方式 请求按下面的顺序命中: | 路径 | 处理方式 | | --- | --- | | `/api/*` | JSON / SSE 接口 | | `/`、`/index.html` | 内嵌前端的 `index.html`(`no-cache`) | | `/assets/**` | 内嵌前端的静态资源(文件名带 hash,长缓存) | | `/favicon.svg` | 内嵌前端根目录文件 | | 其他路径 | `ServeDir` 托管 `--root` 目录,目录自动补 `index.html` | | 仍未命中 | 内置 404 页面(显示请求路径) | 托管目录下的每个一级子目录,只要包含 `index.html` 或 `index.htm`,就会被识别为一个项目, 通过 `/<目录名>/` 访问。目录名会做 URL 路径段编码。 ## 目录结构 ```text . ├── Cargo.toml # 单 crate(lib + bin),依赖清单 ├── Cargo.lock ├── build.rs # 把 stash-web/dist 同步到 $OUT_DIR/web ├── src/ # Rust 后端 │ ├── main.rs # CLI 参数、启动横幅、停机信号 │ ├── lib.rs # 库导出 │ ├── config.rs # 服务配置、数据目录与路径校验 │ ├── error.rs # 错误类型与 JSON 错误响应 │ ├── server.rs # 路由、内嵌静态资源、SSE、生命周期 │ ├── listing.rs # 托管目录扫描 │ ├── watcher.rs # 文件监听、防抖、快照广播 │ └── db/ # SQLite 内容库 │ ├── mod.rs # 打开、PRAGMA、种子导入时机 │ ├── schema.rs # 建表语句与种子导入 │ ├── catalog.rs # 查询结果组装成前端 Category[] │ └── seed.json # 内嵌种子数据(编译进二进制) ├── stash-web/ # React + Vite + TypeScript 控制台 │ ├── package.json # 含 volta 版本固定 │ ├── vite.config.ts # 开发期 /api 代理到 127.0.0.1:8080 │ └── src/ ├── data/ # 内容库目录(与二进制同级,运行时生成) │ └── stash.db # SQLite 内容库 └── public/ # 默认托管目录(可放多个前端项目) ``` `templates/` 与 `assets/` 是上一版 Askama 控制台留下的文件,当前已无任何代码引用, 可以直接删除。 ## 前端怎么被内嵌 1. `npm run build` 产出 `stash-web/dist`; 2. `build.rs` 在编译 Rust 之前把 `dist` 复制到 `$OUT_DIR/web`(先清空,避免旧 hash 文件残留); 3. `src/server.rs` 用 `include_dir!("$OUT_DIR/web")` 在编译期把整棵目录树嵌进二进制; 4. 二进制运行时不再读取任何前端文件。 构建顺序不能颠倒:`dist` 不存在时 `build.rs` 会带着提示信息直接失败,而不是产出一个没有界面的二进制。 `build.rs` 声明了 `rerun-if-changed`,改动 `dist` 后重新 `cargo build` 会自动重新嵌入。 ## 内容库(data/stash.db) 控制台的分类 / 分组 / 条目不写死在前端代码里,而是存在一个 SQLite 文件中, 默认位置是**二进制同级**的 `data/stash.db`。首次启动时若文件不存在, 会用内嵌的 `src/db/seed.json` 建库并灌入示例内容;之后一直沿用该文件(不覆盖已有数据)。 用 `--data ` 可以把数据目录指到别处。 **内容库不能放在 `--root` 托管目录内部**——那里的文件会被 `ServeDir` 当静态资源对外提供, 启动时会直接报错拒绝。 ### 表结构 | 表 | 说明 | | --- | --- | | `category` | 分类;`kind` 取值 `apps` / `posters` / `articles`,`sort` 决定首页分区顺序 | | `app_group` | 应用类分类下的分组,`sort` 决定组内顺序 | | `entry` | 条目;`name` 是展示名,`views` / `likes` 单独成列,其余字段在 `data` JSON 列里 | | `nav_entry` | 侧栏导航;自引用树,`parent_id` 为空的是顶层项,带 `category_id` 为直达链接,否则是可折叠分组 | `entry.data` 的键名与前端 [`types.ts`](stash-web/src/data/types.ts) 一一对应: 应用的 `desc` / `icon` / `href` / `cover` / `hot` / `banner` / `badge`, 海报的 `tags` / `summary`,图文的 `blocks` / `meta`。想加字段直接改 JSON,后端会原样透传。 ### 侧栏导航 侧栏的层级、标签和跳转目标都在 `nav_entry` 表里,不再写死在前端: ```text nav_entry ├─ id / parent_id / label / category_id / group_id / sort ├─ 顶层项 parent_id IS NULL │ ├─ category_id 非空 → 直达链接(如「常用推荐」→ recommend) │ └─ category_id 为空 → 可折叠分组(如「素材资源」) └─ 叶子 parent_id 指向分组,category_id 必填,group_id 可选 ``` 两个细节是刻意设计的: - 分组名可以与分类名不同(「素材资源」并不对应任何分类); - 多个叶子可以指向同一个目标(「前端调试工具」和「在线编辑器」都打开 `recommend` 的 `dev` 分组)。 删除分类时,指向它的导航叶子会被外键级联删掉,不会留下点了没反应的死链接。 ### 自己改内容 1. 用任何 SQLite 工具(DB Browser for SQLite、`sqlite3` CLI、DBeaver…)打开 `data/stash.db`; 2. 分类内容改 `category` / `app_group` / `entry`,侧栏改 `nav_entry`; 3. 提交修改后,页面会在 **5 秒内**自动刷新——服务端轮询 `PRAGMA data_version` 检测外部写入; 4. 也可以点侧栏的「重新扫描」或直接刷新浏览器。 改坏 `data` 列(比如写成非法 JSON)时,接口会返回带「分类/条目 id」的错误信息, 页面顶部会显示出来,改回去即可。 ### 重置 开发阶段表结构还会调整,**没有实现迁移**。要重置内容:关掉服务后删掉 `data/stash.db`, 下次启动会重新建表并灌入内嵌种子。 ```bash rm data/stash.db # Linux / macOS Remove-Item data\stash.db # Windows PowerShell ``` ## 前端开发 ```bash cd stash-web npm install npm run dev # http://127.0.0.1:5173,/api 自动代理到后端 8080 npm run build # 产物输出到 stash-web/dist npm run lint # oxlint ``` 开发时前后端分开跑:一个终端 `cargo run`,另一个终端 `npm run dev`。 控制台的用户资料目前是占位数据(见 `stash-web/src/profile.ts`),后端尚未提供鉴权接口。 ## 命令行参数 | 参数 | 默认值 | 说明 | | --- | --- | --- | | `-r, --root ` | `./public` | 静态页面托管根目录 | | `--data ` | 二进制同级 `data/` | 内容库所在目录(存放 `stash.db`) | | `-p, --port ` | `8080` | 监听端口 | | `--host ` | `127.0.0.1` | 监听地址,`0.0.0.0` 供局域网访问 | | `--no-cors` | 关闭 | 关闭全局 CORS 标头注入 | | `-h, --help` | - | 帮助 | | `-V, --version` | - | 版本 | `--no-listing` 参数目前仍能被解析,但由于控制台已改为内嵌前端,它不再影响任何行为。 ## HTTP API ### 健康检查 ```http GET /api/health ``` ```json { "status": "ok", "service": "stash" } ``` ### 项目列表 ```http GET /api/projects ``` ```json { "projects": [ { "name": "admin-console", "route_path": "/admin-console/" }, { "name": "docs", "route_path": "/docs/" } ] } ``` ### 订阅项目变化 ```http GET /api/projects/events Accept: text/event-stream ``` 连接建立后立即推送一次当前快照,之后在项目列表变化时推送: ```text event: projects data: {"projects":[{"name":"docs","route_path":"/docs/"}]} ``` 服务端递归监听托管目录,把短时间内的连续事件合并成一次扫描,只在项目名称或访问路径真正变化时广播; 每 15 秒发送一次保活消息,浏览器断开后自动重连。 ### 内容目录 ```http GET /api/catalog ``` 返回控制台需要的全部分类(结构见 [`types.ts`](stash-web/src/data/types.ts)), 数据来自 `data/stash.db`。一次全量下发,前端搜索在客户端完成: ```json { "categories": [ { "id": "recommend", "kind": "apps", "title": "常用推荐", "barColor": "#3858f6", "icon": "🔖", "groups": [ { "id": "common", "name": "常用站点", "items": [ { "id": "github", "name": "GitHub", "desc": "代码托管与开源社区", "views": 429 } ] } ] } ] } ``` 「本地服务」分类**不在**这个接口里,它由 `/api/projects` 扫描托管目录后在前端拼接。 返回体里还有一个 `nav` 字段,即侧栏导航,结构与 [`nav.ts`](stash-web/src/data/nav.ts) 的 `NavModel` 一致: ```json { "nav": { "topLevel": [{ "id": "nav-recommend", "label": "常用推荐", "target": { "categoryId": "recommend" } }], "groups": [ { "id": "nav-assets", "label": "素材资源", "leaves": [ { "id": "nav-ui", "label": "UI 设计资源", "target": { "categoryId": "recommend", "groupId": "ui" } } ] } ] } } ``` ### 订阅内容库变化 ```http GET /api/catalog/events Accept: text/event-stream ``` 当内容库被**其它连接**(例如用户用 DB Browser 改库)提交后推送一次,事件本身不带数据: ```text event: catalog data: {"changed":true} ``` 客户端收到后重新 `GET /api/catalog`。检测基于 SQLite 的 `PRAGMA data_version`,每 5 秒轮询一次。 ## 测试 ```bash cargo test # 目录扫描、隐藏目录过滤、排序、路径编码、文件监听广播、内容库 cd stash-web && npm run build # tsc -b + vite build,同时做类型检查 ``` 内容库相关测试全部使用**内存库**(`:memory:`),不写磁盘、不污染 `data/`。 如果运行环境限制了系统临时目录(部分沙箱会这样),前几个测试会报 `PermissionDenied`, 把 `TEMP` / `TMP` 指到工作区内再跑即可: ```powershell $env:TEMP = "$PWD\target\test-tmp"; $env:TMP = $env:TEMP; cargo test ``` ## 已知限制 - 控制台是单页应用但没有客户端路由,因此没有 SPA history 回退;刷新不存在的物理路径会返回 404。 - 内嵌资源占用了 `/assets/` 前缀,托管目录下如果存在同名目录会被遮蔽。 - `/` 固定由控制台占用,上一版「根目录有 `index.html` 就直接当单站点展示」的行为已取消; 托管目录里的项目统一走 `/<目录名>/`。 - 项目扫描只检查一级子目录,不递归查找更深层项目。 - 文件事件经过约 150ms 防抖,新项目出现在控制台可能有短暂延迟。 - 内容库没有迁移机制:表结构变化时需要删掉 `data/stash.db` 重建(当前属于开发阶段)。 - 内容库被外部工具修改后,页面最长有 5 秒延迟才自动刷新。 - 内容库按单实例使用设计:多个进程同时写同一个库会在 `busy_timeout`(5 秒)后报错。 - 没有身份认证,默认只建议绑定 `127.0.0.1`;使用 `0.0.0.0` 暴露到局域网前请确认目录内容适合被其他设备访问。