(board: Board, req: Request)
| 422 | } |
| 423 | |
| 424 | async function handleBoardReload(board: Board, req: Request): Promise<Response> { |
| 425 | let body: any; |
| 426 | try { |
| 427 | body = await req.json(); |
| 428 | } catch { |
| 429 | return Response.json({ error: "Invalid JSON" }, { status: 400 }); |
| 430 | } |
| 431 | const newHtmlPath = typeof body?.html === "string" ? body.html : ""; |
| 432 | if (!newHtmlPath || !fs.existsSync(newHtmlPath)) { |
| 433 | return Response.json({ error: `HTML file not found: ${newHtmlPath}` }, { status: 400 }); |
| 434 | } |
| 435 | const resolvedReload = fs.realpathSync(path.resolve(newHtmlPath)); |
| 436 | if (!resolvedReload.startsWith(board.allowedDir + path.sep)) { |
| 437 | return Response.json( |
| 438 | { error: `Path must be within: ${board.allowedDir}` }, |
| 439 | { status: 403 }, |
| 440 | ); |
| 441 | } |
| 442 | if (!fs.statSync(resolvedReload).isFile()) { |
| 443 | return Response.json( |
| 444 | { error: `Path must be a file, not a directory: ${newHtmlPath}` }, |
| 445 | { status: 400 }, |
| 446 | ); |
| 447 | } |
| 448 | board.htmlContent = fs.readFileSync(resolvedReload, "utf-8"); |
| 449 | board.state = "serving"; |
| 450 | board.lastTouched = Date.now(); |
| 451 | markMeaningfulActivity(); |
| 452 | dlog(`board ${board.id} reloaded from ${resolvedReload}`); |
| 453 | return Response.json({ reloaded: true }); |
| 454 | } |
| 455 | |
| 456 | function boardExpiredHtml(id: string): string { |
| 457 | return `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Board expired — gstack</title> |
no test coverage detected