111 lines
3.8 KiB
JavaScript
111 lines
3.8 KiB
JavaScript
// 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
|