-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathserver.js
More file actions
189 lines (160 loc) · 6.13 KB
/
server.js
File metadata and controls
189 lines (160 loc) · 6.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
// boneset-api/server.js
const express = require("express");
const axios = require("axios");
const cors = require("cors");
const path = require("path");
const fs = require("fs/promises");
const rateLimit = require("express-rate-limit");
const app = express();
const PORT = process.env.PORT || 8000;
app.use(cors());
app.use("/images", express.static(path.join(__dirname, "public/images"))); // local static images (useful in dev)
// ---- GitHub sources (Pelvis + Skull) ----
const GITHUB_REPO = "https://raw.githubusercontent.com/oss-slu/DigitalBonesBox/data/DataPelvis/";
const BONESET_JSON_URL = `${GITHUB_REPO}boneset/bony_pelvis.json`;
const BONES_DIR_URL = `${GITHUB_REPO}bones/`;
// Skull is branch-aware so a single PR works now; flip SKULL_BRANCH to "data" later
const SKULL_BRANCH = process.env.SKULL_BRANCH || "issue127-skull-boneset";
const GITHUB_REPO_SKULL = `https://raw.githubusercontent.com/oss-slu/DigitalBonesBox/${SKULL_BRANCH}/DataSkull/`;
const SKULL_JSON_URL = `${GITHUB_REPO_SKULL}boneset/skull.json`;
// ---- Local data dir (used for pelvis descriptions in dev) ----
const DATA_DIR = path.join(__dirname, "data");
// ---- Rate limiter for FS-backed endpoints ----
const bonesetLimiter = rateLimit({
windowMs: 60 * 1000,
max: 60,
standardHeaders: true,
legacyHeaders: false,
});
// ---- Allowlist for /api/boneset/:bonesetId ----
const ALLOWED_BONESETS = new Set(["bony_pelvis", "skull"]);
// ---- Helpers ----
async function fetchJSON(url) {
try {
const response = await axios.get(url, { timeout: 10_000 });
return response.data;
} catch (error) {
console.error(`Failed to fetch ${url}:`, error.message);
return null;
}
}
function safeDataPath(fileName) {
const base = path.resolve(DATA_DIR);
const candidate = path.resolve(DATA_DIR, fileName);
if (!candidate.startsWith(base + path.sep)) {
const err = new Error("Invalid path");
err.code = "EINVAL";
throw err;
}
return candidate;
}
function escapeHtml(str = "") {
return String(str).replace(/[&<>"']/g, (c) => ({
"&": "&",
"<": "<",
">": ">",
"\"": """,
"'": "'",
})[c]);
}
async function loadLocalBoneset(id) {
const file = safeDataPath(`final_${id}.json`);
const raw = await fs.readFile(file, "utf8");
return JSON.parse(raw);
}
function findNodeById(boneset, id) {
if (!boneset) return null;
for (const bone of boneset.bones || []) {
if (bone.id === id) return bone;
for (const sub of bone.subbones || []) {
if (sub.id === id) return sub;
}
}
return null;
}
// ---- Routes ----
app.get("/", (_req, res) => {
res.json({ message: "Welcome to the Boneset API (GitHub-Integrated)" });
});
// Merged list for dropdowns (Pelvis from DataPelvis raw; Skull from branch-aware DataSkull raw)
app.get("/combined-data", async (_req, res) => {
try {
const [pelvis, skull] = await Promise.all([
fetchJSON(BONESET_JSON_URL), // DataPelvis/boneset/bony_pelvis.json
fetchJSON(SKULL_JSON_URL), // DataSkull/boneset/skull.json (branch-aware)
]);
if (!pelvis || !skull) return res.status(500).json({ error: "Failed to load data" });
const bonesets = [
{ id: pelvis.id, name: pelvis.name },
{ id: skull.id, name: skull.name },
];
const bones = [];
const subbones = [];
// Pelvis: expand each bone file from DataPelvis/bones/
for (const boneId of pelvis.bones) {
const boneJsonUrl = `${BONES_DIR_URL}${boneId}.json`;
const boneData = await fetchJSON(boneJsonUrl);
if (boneData) {
bones.push({ id: boneData.id, name: boneData.name, boneset: pelvis.id });
(boneData.subBones || []).forEach(subId => {
subbones.push({ id: subId, name: subId.replace(/_/g, " "), bone: boneData.id });
});
}
}
// Skull: bones & subbones already included in master skull.json
for (const b of skull.bones || []) {
bones.push({ id: b.id, name: b.name, boneset: skull.id });
for (const sb of b.subbones || []) {
subbones.push({ id: sb.id, name: sb.name, bone: b.id });
}
}
res.json({ bonesets, bones, subbones });
} catch (error) {
console.error("Error fetching combined data:", error.message);
res.status(500).json({ error: "Internal Server Error" });
}
});
// Return description HTML (Skull from GitHub; Pelvis from local dev file)
app.get("/api/description", bonesetLimiter, async (req, res) => {
const boneId = String(req.query.boneId || "");
const bonesetId = String(req.query.bonesetId || "bony_pelvis");
if (!/^[a-z0-9_]+$/.test(boneId) || !ALLOWED_BONESETS.has(bonesetId)) {
return res.type("text/html").send("");
}
try {
const set = bonesetId === "skull"
? await fetchJSON(SKULL_JSON_URL) // GitHub (no local dependency)
: await loadLocalBoneset(bonesetId); // local file for pelvis in dev
const node = findNodeById(set, boneId);
if (!node) return res.type("text/html").send("");
const name = node.name || boneId.replace(/_/g, " ");
const lines = Array.isArray(node.description) ? node.description : [];
let html = `<li><strong>${escapeHtml(name)}</strong></li>`;
for (const line of lines) html += `<li>${escapeHtml(line)}</li>`;
res.type("text/html").send(html);
} catch (err) {
console.error("description error:", err);
res.type("text/html").send("<li>Description not available.</li>");
}
});
// Dev helper: serve local merged JSONs (if present)
app.get("/api/boneset/:bonesetId", bonesetLimiter, async (req, res) => {
const { bonesetId } = req.params;
if (!ALLOWED_BONESETS.has(bonesetId)) {
return res.status(404).json({ error: `Boneset '${bonesetId}' not found` });
}
try {
const filePath = safeDataPath(`final_${bonesetId}.json`);
const raw = await fs.readFile(filePath, "utf8");
res.type("application/json").send(raw);
} catch (err) {
if (err.code === "ENOENT") {
return res.status(404).json({ error: `Boneset '${bonesetId}' not found` });
}
console.error("Error reading boneset file:", err);
res.status(500).json({ error: "Internal Server Error" });
}
});
app.listen(PORT, () => {
console.log(`🚀 Server running on http://127.0.0.1:${PORT}`);
});