Files
stash/README.md
T

12 KiB
Raw Blame History

Stash

Stash 是一个用 Rust 编写的本地静态页面托管工具。编译后的 React 控制台会被内嵌进二进制, 所以发布物只有一个 stash 可执行文件,不需要额外部署前端静态资源。

它能做两件事:托管目录下的多个静态前端项目、给控制台提供同源 JSON / SSE 接口。

快速开始

# 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 管理;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 路径段编码。

目录结构

.
├── 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 <DIR> 可以把数据目录指到别处。 内容库不能放在 --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 一一对应: 应用的 desc / icon / href / cover / hot / banner / badge, 海报的 tags / summary,图文的 blocks / meta。想加字段直接改 JSON,后端会原样透传。

侧栏导航

侧栏的层级、标签和跳转目标都在 nav_entry 表里,不再写死在前端:

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, 下次启动会重新建表并灌入内嵌种子。

rm data/stash.db                        # Linux / macOS
Remove-Item data\stash.db               # Windows PowerShell

前端开发

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 <DIR> ./public 静态页面托管根目录
--data <DIR> 二进制同级 data/ 内容库所在目录(存放 stash.db)
-p, --port <PORT> 8080 监听端口
--host <HOST> 127.0.0.1 监听地址,0.0.0.0 供局域网访问
--no-cors 关闭 关闭全局 CORS 标头注入
-h, --help - 帮助
-V, --version - 版本

--no-listing 参数目前仍能被解析,但由于控制台已改为内嵌前端,它不再影响任何行为。

HTTP API

健康检查

GET /api/health
{ "status": "ok", "service": "stash" }

项目列表

GET /api/projects
{
  "projects": [
    { "name": "admin-console", "route_path": "/admin-console/" },
    { "name": "docs", "route_path": "/docs/" }
  ]
}

订阅项目变化

GET /api/projects/events
Accept: text/event-stream

连接建立后立即推送一次当前快照,之后在项目列表变化时推送:

event: projects
data: {"projects":[{"name":"docs","route_path":"/docs/"}]}

服务端递归监听托管目录,把短时间内的连续事件合并成一次扫描,只在项目名称或访问路径真正变化时广播; 每 15 秒发送一次保活消息,浏览器断开后自动重连。

内容目录

GET /api/catalog

返回控制台需要的全部分类(结构见 types.ts), 数据来自 data/stash.db。一次全量下发,前端搜索在客户端完成:

{
  "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 的 NavModel 一致:

{
  "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" } }
        ]
      }
    ]
  }
}

订阅内容库变化

GET /api/catalog/events
Accept: text/event-stream

当内容库被其它连接(例如用户用 DB Browser 改库)提交后推送一次,事件本身不带数据:

event: catalog
data: {"changed":true}

客户端收到后重新 GET /api/catalog。检测基于 SQLite 的 PRAGMA data_version,每 5 秒轮询一次。

测试

cargo test                    # 目录扫描、隐藏目录过滤、排序、路径编码、文件监听广播、内容库
cd stash-web && npm run build # tsc -b + vite build,同时做类型检查

内容库相关测试全部使用内存库(:memory:),不写磁盘、不污染 data/。 如果运行环境限制了系统临时目录(部分沙箱会这样),前几个测试会报 PermissionDenied, 把 TEMP / TMP 指到工作区内再跑即可:

$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 暴露到局域网前请确认目录内容适合被其他设备访问。