ota
This commit is contained in:
+3
-1
@@ -8,6 +8,7 @@ import dataRouter from './routes/data.js'
|
||||
import newsRouter from './routes/news.js'
|
||||
import alertsRouter from './routes/alerts.js'
|
||||
import articlesRouter from './routes/articles.js'
|
||||
import otaRouter from './routes/ota.js'
|
||||
import monitorsRouter from './routes/monitors.js'
|
||||
|
||||
const PORT = Number(process.env.PORT) || 3001
|
||||
@@ -27,6 +28,7 @@ app.use('/api', dataRouter)
|
||||
app.use('/api', newsRouter)
|
||||
app.use('/api', alertsRouter)
|
||||
app.use('/api', articlesRouter)
|
||||
app.use('/api', otaRouter)
|
||||
app.use('/api', monitorsRouter)
|
||||
|
||||
// 静态资源 (Vite build 产物)
|
||||
@@ -53,4 +55,4 @@ import('./cron.js').catch((e) => console.error('[cron] 加载失败:', e.message
|
||||
app.listen(PORT, () => {
|
||||
console.log(`[iboard-api] listening on http://127.0.0.1:${PORT}`)
|
||||
console.log(`[iboard-api] static dist: ${HAS_DIST ? DIST_DIR : '(not built, API only)'}`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// OTA 升级接口 (兼容 tauri-plugin-hotswap 服务端约定)
|
||||
// GET /api/ota/check/:current_sequence
|
||||
// - 客户端传当前 sequence, 服务端判断是否有更新
|
||||
// - 有: 200 + manifest JSON
|
||||
// - 无: 204 No Content
|
||||
// GET /api/ota/bundle/:filename
|
||||
// - 下载前端 bundle (tar.gz), 供 manifest.url 引用
|
||||
// GET /api/ota/manifest-web
|
||||
// - 给 Web 浏览器用的简化版 manifest (无签名, 仅展示用)
|
||||
//
|
||||
// 数据源: 环境变量 OTA_PACKAGE_DIR 指向的目录, 里面要放:
|
||||
// - 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'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
const router = Router()
|
||||
|
||||
function resolveOtaDir() {
|
||||
const dir = process.env.OTA_PACKAGE_DIR
|
||||
? path.resolve(process.env.OTA_PACKAGE_DIR)
|
||||
: path.resolve(process.cwd(), 'ota-package')
|
||||
return dir
|
||||
}
|
||||
|
||||
function readLatest() {
|
||||
const p = path.join(resolveOtaDir(), 'latest.json')
|
||||
if (!fs.existsSync(p)) return null
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(p, 'utf8'))
|
||||
} catch (e) {
|
||||
console.error('[ota] latest.json 解析失败:', e.message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/ota/check/:current_sequence
|
||||
// 客户端实际请求 URL 是 endpoint 中 {{current_sequence}} 替换后的形式
|
||||
router.get('/ota/check/:current_sequence', (req, res) => {
|
||||
const currentSeq = Number(req.params.current_sequence)
|
||||
if (!Number.isFinite(currentSeq) || currentSeq < 0) {
|
||||
return res.status(400).json({ error: '无效的 sequence' })
|
||||
}
|
||||
const latest = readLatest()
|
||||
if (!latest) {
|
||||
// 无可用包, 按约定返回 204
|
||||
return res.status(204).end()
|
||||
}
|
||||
// 不新于当前则 204
|
||||
if (typeof latest.sequence === 'number' && latest.sequence <= currentSeq) {
|
||||
return res.status(204).end()
|
||||
}
|
||||
// 检查 binary_version 兼容性 (客户端会再校验一次, 这里只做参考过滤)
|
||||
// 客户端传 binary_version query: 暂不强制, 让客户端兜底
|
||||
const dir = resolveOtaDir()
|
||||
const base = process.env.OTA_PUBLIC_BASE || ''
|
||||
res.json({
|
||||
version: latest.version,
|
||||
sequence: latest.sequence,
|
||||
min_binary_version: latest.min_binary_version || '0.0.0',
|
||||
url: (base ? base : '') + '/api/ota/bundle/' + encodeURIComponent(latest.bundle),
|
||||
signature: latest.signature,
|
||||
notes: latest.notes || '',
|
||||
pub_date: latest.pub_date || new Date().toISOString(),
|
||||
mandatory: !!latest.mandatory,
|
||||
bundle_size: latest.bundle_size || 0,
|
||||
})
|
||||
})
|
||||
|
||||
// GET /api/ota/bundle/:filename - 流式下载 bundle
|
||||
router.get('/ota/bundle/:filename', (req, res) => {
|
||||
const dir = resolveOtaDir()
|
||||
// 防越界: 只取 basename
|
||||
const name = path.basename(req.params.filename)
|
||||
const p = path.join(dir, name)
|
||||
if (!fs.existsSync(p)) return res.status(404).json({ error: 'bundle 不存在' })
|
||||
const stat = fs.statSync(p)
|
||||
res.setHeader('Content-Type', 'application/gzip')
|
||||
res.setHeader('Content-Length', stat.size)
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="' + name + '"')
|
||||
fs.createReadStream(p).pipe(res)
|
||||
})
|
||||
|
||||
// GET /api/ota/manifest-web - Web 浏览器用的简化版
|
||||
// 客户端会拿这个跟当前 build_id 比较, 有新版本就提示用户刷新
|
||||
router.get('/ota/manifest-web', (_req, res) => {
|
||||
const latest = readLatest()
|
||||
if (!latest) return res.json({ available: false })
|
||||
res.json({
|
||||
available: true,
|
||||
version: latest.version,
|
||||
build_id: latest.build_id || latest.version,
|
||||
note: latest.notes || '',
|
||||
size: latest.bundle_size || 0,
|
||||
})
|
||||
})
|
||||
|
||||
// GET /api/ota/info - 诊断/自检
|
||||
router.get('/ota/info', (_req, res) => {
|
||||
const dir = resolveOtaDir()
|
||||
const latest = readLatest()
|
||||
res.json({
|
||||
ota_dir: dir,
|
||||
latest,
|
||||
})
|
||||
})
|
||||
|
||||
export default router
|
||||
Reference in New Issue
Block a user