主题
13 — 固件资源服务器
部署时间:2026-11-08 05:26~05:30 CST 名称:TenoDX Firmware Registry | 版本 1.0.0 纯 API 服务,没有任何浏览器界面。
1. 选型:为什么自己写
| 候选 | 否决理由 |
|---|---|
| MinIO | 自带 Web Console,与"API-only"要求冲突;常驻内存 ≥200 MB,本机仅剩 ~1 GB |
| Gitea Releases | 同样自带 Web UI;为"发固件"引入整套 issue/PR 体系,过重 |
| Nexus / Artifactory | Java 栈,内存 500 MB 起步,2C1.7G 跑不动 |
| nginx/Caddy 静态目录 | 没有版本元数据、没有更新说明、没有鉴权上传 |
| 自建 FastAPI ✅ | 只做发固件这一件事:版本 + 时间 + 更新说明 + SHA256 + 鉴权上传 |
关键取舍:元数据直接存进已经存在的 PostgreSQL(新增库 tenodx_fw), 这样"数据库每日备份""数据库监控"自动覆盖新服务,不用再引一套有状态组件。 文件本体落在 Docker 卷 tenodx-fwdata。
2. 部署信息
| 项 | 值 |
|---|---|
| 镜像 | 本地构建 tenodx/fw-registry:1.0.0(python:3.12-slim + FastAPI) |
| 容器 | tenodx-fw-registry |
| 监听 | 127.0.0.1:8000(仅 loopback) |
| 内存 | ~51 MB |
| 数据库 | PostgreSQL 库 tenodx_fw(复用 tenodx-postgres) |
| 文件存储 | Docker 卷 tenodx-fwdata → /data/firmware |
| 部署目录 | /opt/stacks/fw-registry/ |
| 源码 | server/fw-registry/(Dockerfile + app/main.py + publish.ps1) |
| Caddy 路由 | fw.localhost → tenodx-fw-registry:8000(见 12-Web入口.md) |
| 监控 | Uptime Kuma「Firmware Registry」项,探针 /api/v1/health |
⚠️ 重要:当前对外不可达
阿里云安全组只放行 22/tcp,所以这台服务器上的固件服务公网访问不到。 现在的使用方式是走 SSH 隧道:
powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\server-info\scripts\tunnel.ps1 -Service fw
# 本地 http://127.0.0.1:18000 -> 服务器 127.0.0.1:8000要真正"向用户分发",必须先放行安全组(步骤见 02-防火墙与端口.md 第 7 节), 然后在 12-Web入口.md 第 7 节把 Caddy 换成真域名 + 自动 HTTPS。
3. API 参考
所有接口返回 JSON;下载接口返回二进制流。没有 /docs、/redoc、/openapi.json(均已关闭)。
鉴权
请求头:X-API-Token: <token>
| Token | 权限 | 存放位置 |
|---|---|---|
FW_ADMIN_TOKEN | 上传 / 修改 / 删除(隐含读权限) | 服务器 /opt/stacks/fw-registry/.env |
FW_READ_TOKEN | 只读(仅在开启下载鉴权后使用) | 同上 |
- 缺 token 或 token 错误 → 401
- 比较用
hmac.compare_digest(常数时间,防时序攻击)
公开接口(无需 token)
GET /api/v1/health
json
{ "status": "ok", "database": "ok" }数据库不可用时返回 503 且 status: "degraded"。(监控用的就是这个)
GET /api/v1/firmware
查询参数:model(默认全部)、channel、include_yanked、limit(默认 100)
json
{
"count": 2,
"items": [
{
"id": 2,
"model": "h503",
"version": "1.2.3",
"channel": "stable",
"filename": "h503-1.2.3.bin",
"size": 131072,
"sha256": "22250e12fcdf...",
"content_type": "application/octet-stream",
"changelog": "- 修复触摸基线漂移\n- 提升采样率至 240Hz",
"uploaded_at": "2026-11-07T21:31:13.259688+00:00",
"uploaded_by": "build-agent",
"yanked": false,
"downloads": 3,
"download_path": "/api/v1/firmware/2/download"
}
]
}排序是按版本号智能比较(
1.10 > 1.9,v1.2.3也认,正式版优于预发布版), 不是按上传时间。所以重新上传旧版本不会让它变成"最新"。
GET /api/v1/firmware/latest?model=h503&channel=stable
返回单个最新版本对象;没有任何固件时 404。
GET /api/v1/firmware/{id}
返回单个版本对象。
GET /api/v1/firmware/{id}/download
返回文件流,附带头:
X-Firmware-Version: 1.2.3
X-Firmware-SHA256: 22250e12...
Cache-Control: public, max-age=31536000, immutable并自动累加下载计数。文件在磁盘上缺失时返回 410 Gone。
当前下载公开。要改成需要 token:服务器
.env里设FW_REQUIRE_DOWNLOAD_TOKEN=1然后docker compose up -d。 这就是你说的"之后可能要鉴权"的开关,不用改代码。
管理接口(需要 admin token)
POST /api/v1/firmware — 上传
multipart/form-data:
| 字段 | 必填 | 说明 |
|---|---|---|
file | ✅ | 固件,「,.bin .hex .elf .srec .s19 .zip .dfu」 |
version | ✅ | 如 1.2.3 |
model | 默认 h503 | |
channel | stable / beta / dev,默认 stable | |
changelog | 更新说明,纯文本(换行保留) | |
uploaded_by | 上传者标识 |
返回 201 + 版本对象。
错误码:
| 码 | 含义 |
|---|---|
| 400 | 缺 version / 扩展名不支持 / 空文件 |
| 401 | token 无效 |
| 409 | 同 model+version+channel 已存在(版本号必须唯一,不能覆盖) |
| 413 | 超过 FW_MAX_UPLOAD_BYTES(默认 64 MB) |
服务端会边写边算 SHA256,返回的 sha256 可与本地 Get-FileHash 对照。
PATCH /api/v1/firmware/{id}
可改 changelog、channel、yanked(下架但不删文件)。
DELETE /api/v1/firmware/{id}?purge_file=true
删除记录并(默认)删除磁盘文件。
4. 上传固件(你手动上传的流程)
一次性准备:拿 admin token
bash
ssh tenodx "grep '^FW_ADMIN_TOKEN=' /opt/stacks/fw-registry/.env | cut -d= -f2-"方式 A:用 publish.ps1(推荐)
powershell
# 1) 开隧道
powershell -NoProfile -ExecutionPolicy Bypass -File .\server-info\scripts\tunnel.ps1 -Service fw
# 2) 上传
$env:FW_ADMIN_TOKEN = "<上一步拿到的 token>"
powershell -NoProfile -ExecutionPolicy Bypass `
-File .\server\fw-registry\publish.ps1 `
-File .\TenoDXController-H503\build\h503.bin `
-Version 1.2.3 `
-Changelog @"
- 修复触摸基线漂移
- 采样率提升至 240Hz
- 优化 USB 重连时序
"@脚本会打印本地/服务端 SHA256 并在不一致时告警。
方式 B:curl
bash
curl -X POST http://127.0.0.1:18000/api/v1/firmware \
-H "X-API-Token: $FW_ADMIN_TOKEN" \
-F "file=@h503.bin" \
-F "version=1.2.3" \
-F "model=h503" \
-F "channel=stable" \
-F "changelog=修复触摸基线漂移" \
-F "uploaded_by=me"方式 C:curl 走 Caddy(域名可用后)
bash
curl -X POST https://fw.your.domain.example/api/v1/firmware \
-H "X-API-Token: $FW_ADMIN_TOKEN" -F file=@h503.bin -F version=1.2.35. 目录与文件布局
/data/firmware/h503/1.2.3/h503-1.2.3.bin # 卷 tenodx-fwdata 内DB 表结构:
sql
firmware(
id, model, version, channel, filename, size, sha256, content_type,
changelog, uploaded_at, uploaded_by, yanked, downloads
UNIQUE (model, version, channel)
)6. 备份
| 内容 | 方式 | 覆盖 |
|---|---|---|
| 元数据 | 已在 pg_dump 里(库 tenodx_fw) | 每日 03:30 ✅ |
| 固件文件 | 尚未备份 ⚠️ | — |
建议补充(未自动做,因为固件体积可能很大):
bash
docker run --rm -v tenodx-fwdata:/data -v /var/backups:/backup alpine \
tar czf /backup/fwdata-$(date +%F).tar.gz -C /data .7. 运维命令
bash
cd /opt/stacks/fw-registry
docker compose ps
docker compose logs --tail 50
docker compose up -d # 改 .env 后应用
docker compose build && docker compose up -d # 改代码后重建
# 看磁盘用量
docker system df -v | grep fwdata
# 直接查库
docker exec -it tenodx-postgres psql -U tenodx -d tenodx_fw \
-c "select id, version, channel, size, uploaded_at from firmware order by id;"改上传上限
bash
sed -i 's/^FW_MAX_UPLOAD_BYTES=.*/FW_MAX_UPLOAD_BYTES=134217728/' /opt/stacks/fw-registry/.env
docker compose up -d下架某个版本(保留文件)
bash
curl -X PATCH http://127.0.0.1:8000/api/v1/firmware/3 \
-H "X-API-Token: $ADMIN" -F yanked=true8. 安全说明
| 项 | 状态 |
|---|---|
| 只监听 looback | ✅ 127.0.0.1:8000 |
| 无 Web UI | ✅ /、/docs、/openapi.json 全部 404 |
| 写操作鉴权 | ✅ 常数时间比较,401 已验证 |
| 文件名清洗 | ✅ 去除路径与非法字符,防目录穿越 |
| 扩展名白名单 | ✅ 只允许固件后缀 |
| 上传大小限制 | ✅ 默认 64 MB,流式校验 |
| SHA256 完整性 | ✅ 上传即计算,端到端验证过 |
| 容器权限 | ✅ 普通非特权容器,mem_limit: 256m |
| 日志轮转 | ✅ 20 MB × 3 |
待办:下载鉴权默认关闭。若将来要限制"只有授权用户能下载", 把 FW_REQUIRE_DOWNLOAD_TOKEN=1 打开并给用户分发 FW_READ_TOKEN。
9. 验证记录(2026-11-08)
| 检查 | 结果 |
|---|---|
| 构建 | ✅ 镜像 tenodx/fw-registry:1.0.0 |
| 健康检查 | ✅ healthy,/api/v1/health → {"status":"ok"} |
| 无 token 上传 | ✅ 401 |
| 错误 token 上传 | ✅ 401 |
| 无 token 删除 | ✅ 401 |
| 带 token 上传 | ✅ 201,返回 SHA256 |
| 下载 + 校验 | ✅ 上传/下载 SHA256 完全一致 |
| 删除 + 清文件 | ✅ {"deleted":1,"file_purged":true} |
/docs /openapi.json / | ✅ 均 404(确认 API-only) |
| Caddy 路由 | ✅ Host: fw.localhost → health 200、list 正常 |
| 监控 | ✅ Uptime Kuma「Firmware Registry」UP |
| 端到端(Hub) | ✅ 上传 → Hub 拉取到版本/时间/更新说明 → 删除 |