93 lines
2.6 KiB
TypeScript
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,
|
|
};
|
|
}
|