const http = require("http"); const fs = require("fs"); const path = require("path"); const url = require("url"); const { spawnSync } = require("child_process"); const ROOT = path.resolve(__dirname, ".."); const WEBROOT = path.resolve(__dirname); function sendJson(res, status, payload) { const body = JSON.stringify(payload, null, 2); res.writeHead(status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body), }); res.end(body); } function sendFile(res, filePath) { fs.readFile(filePath, (err, data) => { if (err) { res.writeHead(404); res.end("Not found"); return; } const ext = path.extname(filePath).toLowerCase(); const mime = ext === ".html" ? "text/html" : ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : "application/octet-stream"; res.writeHead(200, { "Content-Type": mime }); res.end(data); }); } function listDir(requestedPath) { const cleaned = (requestedPath || "").replace(/^\/+/, ""); const safePath = path.resolve(ROOT, cleaned || "."); if (!safePath.startsWith(ROOT)) { return { error: "Path outside repo root." }; } if (!fs.existsSync(safePath)) { return { error: "Path not found." }; } const stat = fs.statSync(safePath); if (!stat.isDirectory()) { return { error: "Path is not a directory." }; } const entries = fs.readdirSync(safePath, { withFileTypes: true }); const formatted = entries.map((entry) => { const entryPath = path.join(safePath, entry.name); const relPath = path.relative(ROOT, entryPath); const isDir = entry.isDirectory(); const size = isDir ? 0 : fs.statSync(entryPath).size; return { name: entry.name, type: isDir ? "dir" : "file", relPath: relPath.replace(/\\/g, "/"), size, }; }); formatted.sort((a, b) => { if (a.type !== b.type) return a.type === "dir" ? -1 : 1; return a.name.localeCompare(b.name); }); return { path: path.relative(ROOT, safePath).replace(/\\/g, "/"), entries: formatted }; } function readTabularFile(requestedPath, limit = 50) { const cleaned = (requestedPath || "").replace(/^\/+/, ""); const safePath = path.resolve(ROOT, cleaned || "."); if (!safePath.startsWith(ROOT)) { return { error: "Path outside repo root." }; } if (!fs.existsSync(safePath)) { return { error: "Path not found." }; } const stat = fs.statSync(safePath); if (!stat.isFile()) { return { error: "Path is not a file." }; } const ext = path.extname(safePath).toLowerCase(); if ([".xlsx", ".xls"].includes(ext)) { const script = ` import json import pandas as pd import sys path = sys.argv[1] limit = int(sys.argv[2]) if len(sys.argv) > 2 else 0 df = pd.read_excel(path) df = df.fillna("") header = [str(c) for c in df.columns.tolist()] total_rows = int(df.shape[0]) if limit > 0: df = df.head(limit) rows = df.astype(str).values.tolist() print(json.dumps({"header": header, "rows": rows, "rows_total": total_rows})) `; const proc = spawnSync("python3", ["-c", script, safePath, String(limit || 0)], { encoding: "utf8", }); if (proc.status !== 0) { const err = proc.stderr || proc.stdout || "python3 failed to read excel"; return { error: err.trim() }; } try { const parsed = JSON.parse(proc.stdout); return { path: path.relative(ROOT, safePath).replace(/\\/g, "/"), delimiter: "excel", header: parsed.header || [], rows: parsed.rows || [], rows_total: parsed.rows_total, }; } catch (err) { return { error: "Failed to parse Excel preview output." }; } } if (![".csv", ".tsv", ".txt"].includes(ext)) { return { error: "Only .csv, .tsv, .txt, .xlsx, or .xls are supported for preview." }; } const content = fs.readFileSync(safePath, "utf8"); const lines = content.split(/\r?\n/).filter((line) => line.trim().length > 0); if (!lines.length) { return { error: "File is empty." }; } const delimiter = ext === ".tsv" ? "\t" : ","; const header = lines[0].split(delimiter).map((v) => v.trim()); const totalRows = lines.length - 1; const rowLimit = limit && limit > 0 ? limit : totalRows; const rows = lines .slice(1, rowLimit + 1) .map((line) => line.split(delimiter).map((v) => v.trim())); return { path: path.relative(ROOT, safePath).replace(/\\/g, "/"), delimiter, header, rows, rows_total: totalRows, }; } function runPython(script, args = []) { const proc = spawnSync("python3", ["-c", script, ...args], { cwd: ROOT, encoding: "utf8", env: { ...process.env, PYTHONWARNINGS: "ignore" }, }); if (proc.status !== 0) { const err = (proc.stderr || proc.stdout || "python3 failed").trim(); return { error: err || "python3 failed" }; } try { return JSON.parse(proc.stdout); } catch (err) { return { error: "Failed to parse python output." }; } } function importPapila() { const script = ` import json from classes.v2 import PapilaData pd = PapilaData.from_dirs( image_dir="Papila/FundusImages", clinical_dir="Papila/ClinicalData", label_col="Diagnosis", cat_cols=["Gender", "Phakic/Pseudophakic"], ) out = { "image_dir": str(pd.clinical.image_dir), "clinical_dir": str(pd.clinical.clinical_dir) if pd.clinical.clinical_dir else None, "label_col": pd.label_col, "df_rows": int(pd.df.shape[0]), "df_cols": int(pd.df.shape[1]), "df_columns": list(pd.df.columns), } print(json.dumps(out)) `; return runPython(script); } function papilaDfPreview(limit = 50) { const script = ` import json from classes.v2 import PapilaData pd = PapilaData.from_dirs( image_dir="Papila/FundusImages", clinical_dir="Papila/ClinicalData", label_col="Diagnosis", cat_cols=["Gender", "Phakic/Pseudophakic"], ) df = pd.df.fillna("") header = [str(c) for c in df.columns.tolist()] limit = int(${Number(limit)}) if ${Number(limit)} else 0 if limit > 0: df = df.head(limit) rows = df.astype(str).values.tolist() print(json.dumps({"header": header, "rows": rows, "rows_total": int(pd.df.shape[0])})) `; return runPython(script); } const server = http.createServer((req, res) => { const parsed = url.parse(req.url, true); if (parsed.pathname === "/api/fs" || parsed.pathname === "/api/fs/") { const requestedPath = parsed.query.path || ""; const payload = listDir(requestedPath); if (payload.error) { sendJson(res, 400, payload); return; } sendJson(res, 200, payload); return; } if (parsed.pathname === "/api/import/papila" || parsed.pathname === "/api/import/papila/") { const payload = importPapila(); if (payload.error) { sendJson(res, 400, payload); return; } sendJson(res, 200, payload); return; } if (parsed.pathname === "/api/papila/df" || parsed.pathname === "/api/papila/df/") { const rawLimit = parsed.query.limit; const limit = rawLimit ? Number.parseInt(rawLimit, 10) : 50; const payload = papilaDfPreview(Number.isFinite(limit) ? limit : 50); if (payload.error) { sendJson(res, 400, payload); return; } sendJson(res, 200, payload); return; } if (parsed.pathname === "/api/file" || parsed.pathname === "/api/file/") { const requestedPath = parsed.query.path || ""; const rawLimit = parsed.query.limit; const limit = rawLimit ? Number.parseInt(rawLimit, 10) : 50; const payload = readTabularFile(requestedPath, Number.isFinite(limit) ? limit : 50); if (payload.error) { sendJson(res, 400, payload); return; } sendJson(res, 200, payload); return; } let filePath = parsed.pathname === "/" ? "index.html" : parsed.pathname.slice(1); filePath = path.join(WEBROOT, filePath); if (!filePath.startsWith(WEBROOT)) { res.writeHead(403); res.end("Forbidden"); return; } sendFile(res, filePath); }); const port = process.env.PORT || 5173; server.listen(port, () => { console.log(`Hypertower UI running at http://localhost:${port}`); console.log(`Repo root: ${ROOT}`); });