79 lines
2.7 KiB
JavaScript
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);
|
|
}
|