Files
boss/src/lib/boss-ota.ts
2026-03-26 23:16:56 +08:00

93 lines
2.6 KiB
TypeScript

import { createHash } from "node:crypto";
import { existsSync } from "node:fs";
import { promises as fs } from "node:fs";
import path from "node:path";
export interface PublishedOtaAsset {
absolutePath: string;
fileName: string;
sizeBytes: number;
sha256: string;
updatedAt: string;
downloadUrl: string;
}
const OTA_PACKAGE_FILE_NAME = "boss-android-latest.apk";
const OTA_META_FILE_NAME = "boss-android-latest.json";
const OTA_DOWNLOAD_URL = "/api/v1/user/ota/package";
function detectRuntimeRoot(startDir: string) {
let current = startDir;
while (true) {
if (existsSync(path.join(current, "package.json")) && existsSync(path.join(current, "src", "app"))) {
return current;
}
const parent = path.dirname(current);
if (parent === current) {
return startDir;
}
current = parent;
}
}
function resolveRuntimeRoot() {
if (process.env.BOSS_RUNTIME_ROOT?.trim()) {
return path.resolve(process.env.BOSS_RUNTIME_ROOT);
}
if (process.env.BOSS_STATE_FILE?.trim()) {
return path.dirname(path.dirname(path.resolve(process.env.BOSS_STATE_FILE)));
}
return detectRuntimeRoot(/* turbopackIgnore: true */ process.cwd());
}
const runtimeRoot = resolveRuntimeRoot();
function otaPublicDir() {
return path.join(runtimeRoot, "public", "downloads");
}
function otaPackagePath() {
return path.join(otaPublicDir(), OTA_PACKAGE_FILE_NAME);
}
function otaMetaPath() {
return path.join(otaPublicDir(), OTA_META_FILE_NAME);
}
export async function getPublishedOtaAsset(): Promise<PublishedOtaAsset | null> {
const apkPath = otaPackagePath();
if (!existsSync(apkPath)) {
return null;
}
const metaPath = otaMetaPath();
if (existsSync(metaPath)) {
try {
const meta = JSON.parse(await fs.readFile(metaPath, "utf8")) as Partial<PublishedOtaAsset> & {
urlPath?: string;
};
return {
absolutePath: apkPath,
fileName: meta.fileName ?? OTA_PACKAGE_FILE_NAME,
sizeBytes: meta.sizeBytes ?? (await fs.stat(apkPath)).size,
sha256: meta.sha256 ?? createHash("sha256").update(await fs.readFile(apkPath)).digest("hex"),
updatedAt: meta.updatedAt ?? (await fs.stat(apkPath)).mtime.toISOString(),
downloadUrl: meta.downloadUrl ?? meta.urlPath ?? OTA_DOWNLOAD_URL,
};
} catch {
// Fall through to live stat/hash.
}
}
const stat = await fs.stat(apkPath);
const content = await fs.readFile(apkPath);
return {
absolutePath: apkPath,
fileName: OTA_PACKAGE_FILE_NAME,
sizeBytes: stat.size,
sha256: createHash("sha256").update(content).digest("hex"),
updatedAt: stat.mtime.toISOString(),
downloadUrl: OTA_DOWNLOAD_URL,
};
}