Files
roast_command_center/server/alog-library.js
T
snowspeeder 824a964f9d
Test and deploy / test-and-deploy (push) Successful in 2m28s
Rebuild printable roast plans and remove machine-based adjustments
2026-09-05 14:56:11 -04:00

79 lines
2.7 KiB
JavaScript

// Optional convenience: list/read .alog files from a local directory the roastetta skill
// (or the operator) already populated, without this webapp owning the Cloudflare/login
// problem itself. Path-traversal guarded: only a bare basename ending in .alog, resolved
// and asserted to stay inside ALOG_DIR.
import * as fs from "node:fs/promises";
import { constants } from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { parseAlog } from "./alog.js";
function resolveAlogDir() {
if (process.env.ALOG_DIR) return process.env.ALOG_DIR;
return path.join(os.homedir(), "Roastetta");
}
async function firstExistingDir(candidates) {
for (const dir of candidates) {
try {
const stat = await fs.stat(dir);
if (stat.isDirectory()) return dir;
} catch {
/* try next */
}
}
return null;
}
export async function listAlogLibrary() {
const dir = await firstExistingDir([resolveAlogDir(), "/Users/shane/dev/hope_roaster/ref/roasts"]);
if (!dir) return [];
const entries = await fs.readdir(dir, { withFileTypes: true });
const files = [];
for (const entry of entries) {
if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".alog")) continue;
const full = path.join(dir, entry.name);
const stat = await fs.stat(full);
files.push({ filename: entry.name, sizeBytes: stat.size, mtime: stat.mtime.toISOString() });
}
return files.sort((a, b) => b.mtime.localeCompare(a.mtime));
}
export async function readAlogFromLibrary(filenameParam) {
const base = path.basename(String(filenameParam ?? ""));
if (!base || base !== String(filenameParam) || !base.toLowerCase().endsWith(".alog")) {
const err = new Error("Filename must end in .alog");
err.code = "bad_filename";
throw err;
}
const dir = await firstExistingDir([resolveAlogDir(), "/Users/shane/dev/hope_roaster/ref/roasts"]);
if (!dir) {
const err = new Error("No .alog library directory found.");
err.code = "not_found";
throw err;
}
const full = path.resolve(dir, base);
if (!full.startsWith(path.resolve(dir) + path.sep)) {
const err = new Error("Invalid filename.");
err.code = "bad_filename";
throw err;
}
let content;
let handle;
try {
// Do not follow a symlink outside the operator's configured library.
handle = await fs.open(full, constants.O_RDONLY | constants.O_NOFOLLOW);
const stat = await handle.stat();
if (!stat.isFile() || stat.size > 8 * 1024 * 1024) throw new Error("Invalid library file");
content = await handle.readFile("utf8");
} catch {
const err = new Error(`${base} not found in the .alog library.`);
err.code = "not_found";
throw err;
} finally {
await handle?.close();
}
return parseAlog(content, base);
}