feat(ota): 新增 OTA-only 热更通道; 修底部导航椭圆 FAB/点击高亮, 顶部内缩减半
This commit is contained in:
+65
-1
@@ -12,9 +12,19 @@
|
||||
// - latest.json (由 scripts/build-ota.mjs 生成)
|
||||
// { sequence, version, min_binary_version, signature, notes, bundle, mandatory, pub_date, bundle_size }
|
||||
// - <filename> (tar.gz 升级包)
|
||||
import { Router } from 'express'
|
||||
//
|
||||
// 只更新 OTA 升级包 (不重建镜像/APK) 的通道:
|
||||
// POST /api/ota/admin/bundle (mounted at /api)
|
||||
// - Header x-ota-token 必须等于环境变量 OTA_ADMIN_TOKEN (CI 从配置中心注入)
|
||||
// - body = tar.gz 原始字节 (Content-Type: application/gzip)
|
||||
// - 元数据走 header: x-ota-seq(>当前), x-ota-sha256, x-ota-signature(非空),
|
||||
// x-ota-version, x-ota-min-bin, x-ota-notes
|
||||
// - 校验通过后覆盖写 OTA_PACKAGE_DIR/latest.json + wardrobe-ota.tar.gz
|
||||
// - 进程内每次请求现读文件, 无需重启; 配合 -v wardrobe-ota:/app/ota-package 卷可持久化
|
||||
import { Router, raw } from 'express'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -106,4 +116,58 @@ router.get('/ota/manifest-web', (_req, res) => {
|
||||
|
||||
// GET /api/ota/info 已删除: 诊断接口会暴露服务器路径与签名, 且无调用方
|
||||
|
||||
// POST /api/ota/admin/bundle - 只更新 OTA 升级包, 不重建镜像/APK
|
||||
// .gitea/workflows/ota-only.yml 在构建+签名后调用此接口, 直推运行中的容器
|
||||
router.post(
|
||||
'/ota/admin/bundle',
|
||||
raw({ type: 'application/gzip', limit: '32mb' }),
|
||||
(req, res) => {
|
||||
const expected = process.env.OTA_ADMIN_TOKEN || ''
|
||||
const token = String(req.headers['x-ota-token'] || '')
|
||||
if (!expected || token !== expected) {
|
||||
return res.status(401).json({ error: 'unauthorized' })
|
||||
}
|
||||
const seq = Number(req.headers['x-ota-seq'])
|
||||
if (!Number.isFinite(seq) || seq <= 0) {
|
||||
return res.status(400).json({ error: '无效 sequence' })
|
||||
}
|
||||
const body = req.body
|
||||
if (!Buffer.isBuffer(body) || body.length === 0) {
|
||||
return res.status(400).json({ error: '空 bundle' })
|
||||
}
|
||||
const sha = String(req.headers['x-ota-sha256'] || '').trim().toLowerCase()
|
||||
const h = crypto.createHash('sha256').update(body).digest('hex')
|
||||
if (!sha || sha !== h) {
|
||||
return res.status(400).json({ error: sha ? 'sha256 不匹配' : '缺少 sha256' })
|
||||
}
|
||||
const signature = String(req.headers['x-ota-signature'] || '').trim()
|
||||
if (!signature) {
|
||||
return res.status(400).json({ error: '缺少签名' })
|
||||
}
|
||||
const dir = resolveOtaDir()
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
const latest = readLatest()
|
||||
if (latest && typeof latest.sequence === 'number' && seq <= latest.sequence) {
|
||||
return res.status(409).json({ error: `sequence 不高于当前`, current: latest.sequence })
|
||||
}
|
||||
const manifest = {
|
||||
version: String(req.headers['x-ota-version'] || '').trim() || '0.1.0-ota',
|
||||
sequence: seq,
|
||||
min_binary_version: String(req.headers['x-ota-min-bin'] || '').trim() || '0.0.0',
|
||||
bundle: 'wardrobe-ota.tar.gz',
|
||||
bundle_size: body.length,
|
||||
bundle_sha256: h,
|
||||
signature,
|
||||
notes: String(req.headers['x-ota-notes'] || '').trim(),
|
||||
pub_date: new Date().toISOString(),
|
||||
mandatory: false,
|
||||
build_id: 'seq-' + seq,
|
||||
}
|
||||
fs.writeFileSync(path.join(dir, 'wardrobe-ota.tar.gz'), body)
|
||||
fs.writeFileSync(path.join(dir, 'latest.json'), JSON.stringify(manifest, null, 2))
|
||||
console.log(`[ota] admin 上传 bundle: seq=${seq} size=${body.length} sha256=${h}`)
|
||||
res.json({ ok: true, sequence: seq, size: body.length })
|
||||
}
|
||||
)
|
||||
|
||||
export default router
|
||||
Reference in New Issue
Block a user