71 lines
3.0 KiB
JavaScript
71 lines
3.0 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { execFileSync } from "node:child_process";
|
|
import { fileURLToPath } from "node:url";
|
|
import {
|
|
createDeploymentHttpServer,
|
|
listenDeploymentHttpServer,
|
|
loadDeploymentContract,
|
|
} from "./deployment-http-server.mjs";
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
|
|
function option(name, fallback) {
|
|
const index = process.argv.indexOf(name);
|
|
return index >= 0 ? process.argv[index + 1] : fallback;
|
|
}
|
|
|
|
function assertSafeArchiveEntries(archive) {
|
|
const entries = execFileSync("tar", ["-tzf", archive], { encoding: "utf8" })
|
|
.split("\n")
|
|
.filter(Boolean);
|
|
assert.ok(entries.length > 0, "binary archive is empty");
|
|
for (const entry of entries) {
|
|
assert.ok(!entry.startsWith("/"), `binary archive contains an absolute path: ${entry}`);
|
|
assert.ok(!entry.split("/").includes(".."), `binary archive contains path traversal: ${entry}`);
|
|
assert.ok(entry === "blender-web-offline/" || entry.startsWith("blender-web-offline/"), `unexpected binary archive root: ${entry}`);
|
|
}
|
|
}
|
|
|
|
const archive = path.resolve(option("--archive", path.join(repoRoot, "release/blender-web-offline.tar.gz")));
|
|
const port = Number.parseInt(option("--port", process.env.WEB_TEST_PORT ?? "5189"), 10);
|
|
assert.ok(fs.statSync(archive).isFile(), `binary archive is missing: ${archive}`);
|
|
assert.ok(Number.isSafeInteger(port) && port > 0 && port < 65536, `invalid deployment port: ${port}`);
|
|
assertSafeArchiveEntries(archive);
|
|
|
|
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "blender-archive-server-"));
|
|
execFileSync("tar", ["--no-same-owner", "--no-same-permissions", "-xzf", archive, "-C", workspace]);
|
|
const bundleRoot = path.join(workspace, "blender-web-offline");
|
|
const staticRoot = path.join(bundleRoot, "app");
|
|
assert.ok(fs.statSync(path.join(staticRoot, "index.html")).isFile(), "extracted binary archive has no app/index.html");
|
|
assert.ok(!staticRoot.startsWith(`${repoRoot}${path.sep}`), "archive server must not serve a workspace directory");
|
|
|
|
const contract = loadDeploymentContract(path.join(bundleRoot, "deployment-contract.json"));
|
|
const server = createDeploymentHttpServer({ root: staticRoot, contract });
|
|
let closing = false;
|
|
|
|
async function close(exitCode = 0) {
|
|
if (closing) return;
|
|
closing = true;
|
|
await new Promise((resolve) => server.close(() => resolve()));
|
|
fs.rmSync(workspace, { recursive: true, force: true });
|
|
process.exit(exitCode);
|
|
}
|
|
|
|
process.once("SIGINT", () => void close(0));
|
|
process.once("SIGTERM", () => void close(0));
|
|
process.once("uncaughtException", (error) => {
|
|
process.stderr.write(`${error.stack ?? error}\n`);
|
|
void close(1);
|
|
});
|
|
process.once("unhandledRejection", (error) => {
|
|
process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`);
|
|
void close(1);
|
|
});
|
|
|
|
const origin = await listenDeploymentHttpServer(server, { port });
|
|
process.stdout.write(`archive-deployment-server ${origin} root=${staticRoot} archive=${archive}\n`);
|
|
|