110 lines
3.3 KiB
TypeScript
110 lines
3.3 KiB
TypeScript
import {
|
|
existsSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
realpathSync,
|
|
statSync,
|
|
} from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const COMPLETE_RUN_FILES = [
|
|
"manifest.json",
|
|
"status.json",
|
|
"capture-jobs.jsonl",
|
|
"scan-results.json",
|
|
"processing-report.json",
|
|
] as const;
|
|
|
|
export interface NativeScannerRunDirectoryOptions {
|
|
outputRoot: string;
|
|
requestedRunDir?: string;
|
|
activeRunDir?: string;
|
|
}
|
|
|
|
export function resolveNativeScannerRunDirectory({
|
|
outputRoot,
|
|
requestedRunDir,
|
|
activeRunDir,
|
|
}: NativeScannerRunDirectoryOptions) {
|
|
const root = path.resolve(outputRoot);
|
|
const requested = requestedRunDir?.trim();
|
|
if (requested) return resolveContainedDirectory(root, requested);
|
|
|
|
const active = activeRunDir?.trim();
|
|
if (active) return resolveContainedDirectory(root, active);
|
|
|
|
return newestCompleteNativeScannerRun(root);
|
|
}
|
|
|
|
export function newestCompleteNativeScannerRun(outputRoot: string) {
|
|
const root = path.resolve(outputRoot);
|
|
if (!isDirectory(root)) return "";
|
|
|
|
try {
|
|
const candidates = readdirSync(root, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => completeRunCandidate(root, path.join(root, entry.name)))
|
|
.filter((entry): entry is CompleteRunCandidate => Boolean(entry))
|
|
.sort((left, right) => right.createdAt - left.createdAt || right.name.localeCompare(left.name));
|
|
return candidates[0]?.runDir ?? "";
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
interface CompleteRunCandidate {
|
|
runDir: string;
|
|
name: string;
|
|
createdAt: number;
|
|
}
|
|
|
|
function completeRunCandidate(root: string, candidate: string): CompleteRunCandidate | null {
|
|
const runDir = resolveContainedDirectory(root, candidate);
|
|
if (!runDir || COMPLETE_RUN_FILES.some((file) => !existsSync(path.join(runDir, file)))) return null;
|
|
|
|
try {
|
|
const statusPayload = JSON.parse(readFileSync(path.join(runDir, "status.json"), "utf8"));
|
|
const status = statusPayload?.scanner ?? statusPayload;
|
|
const target = Number(status?.target ?? 0);
|
|
const captured = Number(status?.captured ?? 0);
|
|
if (status?.running !== false || status?.status !== "done" || target < 1 || captured < target) return null;
|
|
|
|
const results = JSON.parse(readFileSync(path.join(runDir, "scan-results.json"), "utf8"));
|
|
if (!Array.isArray(results) || results.length < captured) return null;
|
|
|
|
const manifest = JSON.parse(readFileSync(path.join(runDir, "manifest.json"), "utf8"));
|
|
const manifestCreatedAt = Date.parse(String(manifest?.createdAt ?? ""));
|
|
return {
|
|
runDir,
|
|
name: path.basename(runDir),
|
|
createdAt: Number.isFinite(manifestCreatedAt) ? manifestCreatedAt : statSync(runDir).mtimeMs,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function resolveContainedDirectory(root: string, candidate: string) {
|
|
try {
|
|
const resolvedRoot = realpathSync(root);
|
|
const resolvedCandidate = realpathSync(path.resolve(candidate));
|
|
if (!isPathInside(resolvedRoot, resolvedCandidate) || !isDirectory(resolvedCandidate)) return "";
|
|
return resolvedCandidate;
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function isDirectory(candidate: string) {
|
|
try {
|
|
return statSync(candidate).isDirectory();
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function isPathInside(root: string, candidate: string) {
|
|
const relative = path.relative(root, candidate);
|
|
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
}
|