// 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 } // - (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 已删除: 诊断接口会暴露服务器路径与签名, 且无调用方 export default router