Enrich program cards and serve local library
This commit is contained in:
parent
8ed968c61b
commit
4fdbae2c8a
9 changed files with 155 additions and 51 deletions
2
.env
2
.env
|
|
@ -1,4 +1,2 @@
|
||||||
HOST_PORT=8134
|
HOST_PORT=8134
|
||||||
PROGRAM_REPOSITORY=https://git.p20.dev/christian/c64-basic
|
|
||||||
PROGRAM_BRANCH=main
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ FROM node:22-alpine
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY server.mjs ./server.mjs
|
COPY server.mjs ./server.mjs
|
||||||
COPY public ./public
|
COPY public ./public
|
||||||
|
COPY . /programs
|
||||||
|
|
||||||
ENV PORT=8080
|
ENV PORT=8080
|
||||||
|
ENV PROGRAM_DIRECTORY=/programs
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
USER node
|
USER node
|
||||||
|
|
|
||||||
20
README.md
20
README.md
|
|
@ -10,15 +10,15 @@ machine-language payload is required.
|
||||||
| Program | Type | Controls | Description | Run locally |
|
| Program | Type | Controls | Description | Run locally |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| [Meteor Dodge](meteor-dodge/meteor-dodge.bas) | Arcade | `A`, `D`, `Q` | Dodge falling meteors and build your score. | [Run program](http://localhost:8134/?program=meteor-dodge%2Fmeteor-dodge.prg) |
|
| [Meteor Dodge](meteor-dodge/meteor-dodge.bas) | Arcade | `A`, `D`, `Q` | Dodge falling meteors and build your score. | [Run program](http://localhost:8134/?program=meteor-dodge%2Fmeteor-dodge.prg) |
|
||||||
| [Code Breaker](code-breaker/code-breaker.bas) | Logic | Digits, `Q` | Deduce a four-digit secret in ten guesses. | [Run program](http://localhost:8134/?program=code-breaker%2Fcode-breaker.prg) |
|
| [Code Breaker](code-breaker/code-breaker.bas) | Logic | `0`-`9`, `Q` | Deduce a four-digit secret in ten guesses. | [Run program](http://localhost:8134/?program=code-breaker%2Fcode-breaker.prg) |
|
||||||
| [Submarine Hunt](submarine-hunt/submarine-hunt.bas) | Strategy | Coordinates, `Q` | Find three hidden submarines with limited sonar shots. | [Run program](http://localhost:8134/?program=submarine-hunt%2Fsubmarine-hunt.prg) |
|
| [Submarine Hunt](submarine-hunt/submarine-hunt.bas) | Strategy | Row, Column, `Q` | Find three hidden submarines with limited sonar shots. | [Run program](http://localhost:8134/?program=submarine-hunt%2Fsubmarine-hunt.prg) |
|
||||||
| [Lunar Lander](lunar-lander/lunar-lander.bas) | Simulation | `0`-`9`, `Q` | Choose engine burns and touch down gently. | [Run program](http://localhost:8134/?program=lunar-lander%2Flunar-lander.prg) |
|
| [Lunar Lander](lunar-lander/lunar-lander.bas) | Simulation | `0`-`9`, `Q` | Choose engine burns and touch down gently. | [Run program](http://localhost:8134/?program=lunar-lander%2Flunar-lander.prg) |
|
||||||
| [Memory Matrix](memory-matrix/memory-matrix.bas) | Memory | `1`-`4`, `Q` | Repeat a growing sequence of flashing quadrants. | [Run program](http://localhost:8134/?program=memory-matrix%2Fmemory-matrix.prg) |
|
| [Memory Matrix](memory-matrix/memory-matrix.bas) | Memory | `1`, `2`, `3`, `4`, `Q` | Repeat a growing sequence of flashing quadrants. | [Run program](http://localhost:8134/?program=memory-matrix%2Fmemory-matrix.prg) |
|
||||||
| [Robot Maze](robot-maze/robot-maze.bas) | Puzzle | `W`, `A`, `S`, `D`, `Q` | Navigate a freshly generated maze to its exit. | [Run program](http://localhost:8134/?program=robot-maze%2Frobot-maze.prg) |
|
| [Robot Maze](robot-maze/robot-maze.bas) | Puzzle | `W`, `A`, `S`, `D`, `Q` | Navigate a freshly generated maze to its exit. | [Run program](http://localhost:8134/?program=robot-maze%2Frobot-maze.prg) |
|
||||||
| [Music Box](music-box/music-box.bas) | Music | `A`-`K`, `1`-`3`, `Q` | Play the SID chip as a small computer keyboard. | [Run program](http://localhost:8134/?program=music-box%2Fmusic-box.prg) |
|
| [Music Box](music-box/music-box.bas) | Music | `A`-`K`, `1`, `2`, `3`, `Q` | Play the SID chip as a small computer keyboard. | [Run program](http://localhost:8134/?program=music-box%2Fmusic-box.prg) |
|
||||||
| [Pixel Painter](pixel-painter/pixel-painter.bas) | Creative | Cursor keys, Space, `C`, `Q` | Draw character-cell pictures on the screen. | [Run program](http://localhost:8134/?program=pixel-painter%2Fpixel-painter.prg) |
|
| [Pixel Painter](pixel-painter/pixel-painter.bas) | Creative | Cursor keys, Space, `C`, `Q` | Draw character-cell pictures on the screen. | [Run program](http://localhost:8134/?program=pixel-painter%2Fpixel-painter.prg) |
|
||||||
| [Stock Trader](stock-trader/stock-trader.bas) | Simulation | `B`, `S`, `W`, `Q` | Trade a volatile stock over twenty market days. | [Run program](http://localhost:8134/?program=stock-trader%2Fstock-trader.prg) |
|
| [Stock Trader](stock-trader/stock-trader.bas) | Simulation | `B`, `S`, `W`, `Q` | Trade a volatile stock over twenty market days. | [Run program](http://localhost:8134/?program=stock-trader%2Fstock-trader.prg) |
|
||||||
| [Starfield](starfield/starfield.bas) | Demo | `1`-`3`, `Q` | Watch and control a layered animated starfield. | [Run program](http://localhost:8134/?program=starfield%2Fstarfield.prg) |
|
| [Starfield](starfield/starfield.bas) | Demo | `1`, `2`, `3`, `Q` | Watch and control a layered animated starfield. | [Run program](http://localhost:8134/?program=starfield%2Fstarfield.prg) |
|
||||||
|
|
||||||
## Running the programs
|
## Running the programs
|
||||||
|
|
||||||
|
|
@ -35,10 +35,12 @@ restrictions. Click inside the emulator to enable keyboard input and sound.
|
||||||
Direct keyboard input is enabled by default, mouse locking is disabled, and
|
Direct keyboard input is enabled by default, mouse locking is disabled, and
|
||||||
moving the pointer anywhere over the emulator reveals its control bar.
|
moving the pointer anywhere over the emulator reveals its control bar.
|
||||||
|
|
||||||
The app reads the public repository tree at runtime. New `.prg` files pushed to
|
Docker Compose mounts this repository read-only at `/programs`, and the app
|
||||||
the configured branch appear after clicking **Refresh list**; rebuilding the
|
serves PRG files from that local mount. There is no runtime dependency on the
|
||||||
Docker image is not required. Repository, branch, and host port settings live in
|
Git server and no browser CORS boundary. Add a program to `programs.json` and
|
||||||
`.env`, where the default host port is `8134`.
|
place its PRG in the referenced path; it appears after clicking **Refresh list**
|
||||||
|
without rebuilding the image. The default host port is configured as `8134` in
|
||||||
|
`.env`.
|
||||||
|
|
||||||
Each directory contains an editable, line-numbered `.bas` listing and a matching
|
Each directory contains an editable, line-numbered `.bas` listing and a matching
|
||||||
tokenized `.prg` file. The PRGs use the normal C64 BASIC load address, `$0801`.
|
tokenized `.prg` file. The PRGs use the normal C64 BASIC load address, `$0801`.
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- "${HOST_PORT:-8134}:8080"
|
- "${HOST_PORT:-8134}:8080"
|
||||||
environment:
|
environment:
|
||||||
PROGRAM_REPOSITORY: "${PROGRAM_REPOSITORY:-https://git.p20.dev/christian/c64-basic}"
|
PROGRAM_DIRECTORY: /programs
|
||||||
PROGRAM_BRANCH: "${PROGRAM_BRANCH:-main}"
|
volumes:
|
||||||
|
- .:/programs:ro
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|
|
||||||
73
programs.json
Normal file
73
programs.json
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "Meteor Dodge",
|
||||||
|
"type": "Arcade",
|
||||||
|
"description": "Dodge falling meteors and build your score.",
|
||||||
|
"controls": ["A", "D", "Q"],
|
||||||
|
"path": "meteor-dodge/meteor-dodge.prg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Code Breaker",
|
||||||
|
"type": "Logic",
|
||||||
|
"description": "Deduce a four-digit secret in ten guesses.",
|
||||||
|
"controls": ["0-9", "Q"],
|
||||||
|
"path": "code-breaker/code-breaker.prg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Submarine Hunt",
|
||||||
|
"type": "Strategy",
|
||||||
|
"description": "Find three hidden submarines with limited sonar shots.",
|
||||||
|
"controls": ["ROW", "COLUMN", "Q"],
|
||||||
|
"path": "submarine-hunt/submarine-hunt.prg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Lunar Lander",
|
||||||
|
"type": "Simulation",
|
||||||
|
"description": "Choose engine burns and touch down gently.",
|
||||||
|
"controls": ["0-9", "Q"],
|
||||||
|
"path": "lunar-lander/lunar-lander.prg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Memory Matrix",
|
||||||
|
"type": "Memory",
|
||||||
|
"description": "Repeat a growing sequence of flashing quadrants.",
|
||||||
|
"controls": ["1", "2", "3", "4", "Q"],
|
||||||
|
"path": "memory-matrix/memory-matrix.prg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Robot Maze",
|
||||||
|
"type": "Puzzle",
|
||||||
|
"description": "Navigate a freshly generated maze to its exit.",
|
||||||
|
"controls": ["W", "A", "S", "D", "Q"],
|
||||||
|
"path": "robot-maze/robot-maze.prg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Music Box",
|
||||||
|
"type": "Music",
|
||||||
|
"description": "Play the SID chip as a small computer keyboard.",
|
||||||
|
"controls": ["A-K", "1", "2", "3", "Q"],
|
||||||
|
"path": "music-box/music-box.prg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Pixel Painter",
|
||||||
|
"type": "Creative",
|
||||||
|
"description": "Draw character-cell pictures on the screen.",
|
||||||
|
"controls": ["CURSOR KEYS", "SPACE", "C", "Q"],
|
||||||
|
"path": "pixel-painter/pixel-painter.prg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Stock Trader",
|
||||||
|
"type": "Simulation",
|
||||||
|
"description": "Trade a volatile stock over twenty market days.",
|
||||||
|
"controls": ["B", "S", "W", "Q"],
|
||||||
|
"path": "stock-trader/stock-trader.prg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Starfield",
|
||||||
|
"type": "Demo",
|
||||||
|
"description": "Watch and control a layered animated starfield.",
|
||||||
|
"controls": ["1", "2", "3", "Q"],
|
||||||
|
"path": "starfield/starfield.prg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
@ -9,6 +9,18 @@ function titleFromPath(path) {
|
||||||
return filename.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
|
return filename.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const typeIcons = {
|
||||||
|
Arcade: '🎮',
|
||||||
|
Logic: '◆',
|
||||||
|
Strategy: '♟',
|
||||||
|
Simulation: '⚙',
|
||||||
|
Memory: '◫',
|
||||||
|
Puzzle: '⌘',
|
||||||
|
Music: '♪',
|
||||||
|
Creative: '✎',
|
||||||
|
Demo: '✦'
|
||||||
|
};
|
||||||
|
|
||||||
async function loadPrograms() {
|
async function loadPrograms() {
|
||||||
status.hidden = false;
|
status.hidden = false;
|
||||||
status.className = 'status';
|
status.className = 'status';
|
||||||
|
|
@ -23,11 +35,32 @@ async function loadPrograms() {
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.className = 'program-card';
|
link.className = 'program-card';
|
||||||
link.href = `/?program=${encodeURIComponent(program.path)}`;
|
link.href = `/?program=${encodeURIComponent(program.path)}`;
|
||||||
|
|
||||||
|
const pill = document.createElement('span');
|
||||||
|
pill.className = 'type-pill';
|
||||||
|
pill.textContent = `${typeIcons[program.type] || '●'} ${program.type}`;
|
||||||
|
|
||||||
const title = document.createElement('strong');
|
const title = document.createElement('strong');
|
||||||
title.textContent = titleFromPath(program.path);
|
title.className = 'program-title';
|
||||||
|
title.textContent = program.title;
|
||||||
|
|
||||||
|
const description = document.createElement('span');
|
||||||
|
description.className = 'program-description';
|
||||||
|
description.textContent = program.description;
|
||||||
|
|
||||||
|
const controls = document.createElement('span');
|
||||||
|
controls.className = 'controls';
|
||||||
|
controls.setAttribute('aria-label', `Controls: ${program.controls.join(', ')}`);
|
||||||
|
for (const control of program.controls) {
|
||||||
|
const key = document.createElement('kbd');
|
||||||
|
key.textContent = control;
|
||||||
|
controls.append(key);
|
||||||
|
}
|
||||||
|
|
||||||
const action = document.createElement('span');
|
const action = document.createElement('span');
|
||||||
|
action.className = 'program-action';
|
||||||
action.textContent = 'Run program →';
|
action.textContent = 'Run program →';
|
||||||
link.append(title, action);
|
link.append(pill, title, description, controls, action);
|
||||||
programsElement.append(link);
|
programsElement.append(link);
|
||||||
}
|
}
|
||||||
status.textContent = programs.length ? `${programs.length} programs available` : 'No PRG files found.';
|
status.textContent = programs.length ? `${programs.length} programs available` : 'No PRG files found.';
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
<section id="library" class="panel">
|
<section id="library" class="panel">
|
||||||
<p class="eyebrow">Program library</p>
|
<p class="eyebrow">Program library</p>
|
||||||
<h1>Choose a program</h1>
|
<h1>Choose a program</h1>
|
||||||
<p class="lede">Programs are read directly from the public repository each time this page loads.</p>
|
<p class="lede">Programs are read directly from the locally mounted repository each time this page loads.</p>
|
||||||
<p id="status" class="status">Loading programs…</p>
|
<p id="status" class="status">Loading programs…</p>
|
||||||
<div id="programs" class="program-grid" aria-live="polite"></div>
|
<div id="programs" class="program-grid" aria-live="polite"></div>
|
||||||
<button id="refresh" class="secondary" type="button">Refresh list</button>
|
<button id="refresh" class="secondary" type="button">Refresh list</button>
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,14 @@ h1 { margin: 0; font-size: clamp(2rem, 7vw, 4rem); line-height: 1; }
|
||||||
.lede, .hint { color: #c8c1df; }
|
.lede, .hint { color: #c8c1df; }
|
||||||
.status { margin: 2rem 0 1rem; color: #9ce6c2; }
|
.status { margin: 2rem 0 1rem; color: #9ce6c2; }
|
||||||
.program-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 1rem; }
|
.program-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 1rem; }
|
||||||
.program-card { display: flex; min-height: 125px; flex-direction: column; justify-content: space-between; padding: 1.25rem; border: 1px solid #ffffff24; border-radius: 14px; background: #211c3bcc; color: #fff; text-decoration: none; transition: transform .15s, border-color .15s; }
|
.program-card { display: flex; min-height: 250px; flex-direction: column; align-items: flex-start; padding: 1.25rem; border: 1px solid #ffffff24; border-radius: 14px; background: #211c3bcc; color: #fff; text-decoration: none; transition: transform .15s, border-color .15s; }
|
||||||
.program-card:hover { transform: translateY(-3px); border-color: #b58cff; }
|
.program-card:hover { transform: translateY(-3px); border-color: #b58cff; }
|
||||||
.program-card span { color: #bca8f1; font-size: .9rem; }
|
.type-pill { display: inline-flex; gap: .4rem; align-items: center; padding: .3rem .65rem; border: 1px solid #b58cff55; border-radius: 999px; background: #8d5cff24; color: #d9c7ff; font-size: .75rem; font-weight: 750; letter-spacing: .04em; text-transform: uppercase; }
|
||||||
|
.program-title { margin-top: 1rem; font-size: 1.2rem; }
|
||||||
|
.program-description { margin-top: .55rem; color: #c8c1df; font-size: .92rem; line-height: 1.45; }
|
||||||
|
.controls { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: 1rem; }
|
||||||
|
kbd { min-width: 1.8rem; padding: .25rem .45rem; border: 1px solid #aaa1c9; border-bottom-width: 3px; border-radius: 5px; background: #f2eff8; color: #241f38; font: 700 .72rem ui-monospace, monospace; text-align: center; }
|
||||||
|
.program-action { margin-top: auto; padding-top: 1.25rem; color: #bca8f1; font-size: .9rem; }
|
||||||
.button, button { display: inline-block; margin-top: 1.5rem; padding: .7rem 1rem; border: 1px solid #ffffff2b; border-radius: 8px; background: #302653; color: #fff; font: inherit; text-decoration: none; cursor: pointer; }
|
.button, button { display: inline-block; margin-top: 1.5rem; padding: .7rem 1rem; border: 1px solid #ffffff2b; border-radius: 8px; background: #302653; color: #fff; font: inherit; text-decoration: none; cursor: pointer; }
|
||||||
button:disabled { opacity: .5; cursor: wait; }
|
button:disabled { opacity: .5; cursor: wait; }
|
||||||
.player-heading { display: flex; align-items: end; justify-content: space-between; gap: 1rem; }
|
.player-heading { display: flex; align-items: end; justify-content: space-between; gap: 1rem; }
|
||||||
|
|
|
||||||
56
server.mjs
56
server.mjs
|
|
@ -1,20 +1,12 @@
|
||||||
import { createReadStream, existsSync, statSync } from 'node:fs';
|
import { createReadStream, existsSync, readFileSync, statSync } from 'node:fs';
|
||||||
import { createServer } from 'node:http';
|
import { createServer } from 'node:http';
|
||||||
import { extname, join, normalize } from 'node:path';
|
import { extname, join, normalize, resolve, sep } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
const port = Number(process.env.PORT || 8080);
|
const port = Number(process.env.PORT || 8080);
|
||||||
const repository = (process.env.PROGRAM_REPOSITORY || 'https://git.p20.dev/christian/c64-basic').replace(/\/$/, '');
|
const programDirectory = resolve(process.env.PROGRAM_DIRECTORY || fileURLToPath(new URL('./', import.meta.url)));
|
||||||
const branch = process.env.PROGRAM_BRANCH || 'main';
|
const manifestPath = join(programDirectory, 'programs.json');
|
||||||
const publicDirectory = fileURLToPath(new URL('./public/', import.meta.url));
|
const publicDirectory = fileURLToPath(new URL('./public/', import.meta.url));
|
||||||
const repositoryMatch = repository.match(/^https:\/\/([^/]+)\/([^/]+)\/([^/]+)$/);
|
|
||||||
|
|
||||||
if (!repositoryMatch) {
|
|
||||||
throw new Error('PROGRAM_REPOSITORY must look like https://host/owner/repository');
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, repositoryHost, repositoryOwner, repositoryName] = repositoryMatch;
|
|
||||||
const apiBase = `https://${repositoryHost}/api/v1/repos/${encodeURIComponent(repositoryOwner)}/${encodeURIComponent(repositoryName)}`;
|
|
||||||
|
|
||||||
function sendJson(response, status, value) {
|
function sendJson(response, status, value) {
|
||||||
const body = JSON.stringify(value);
|
const body = JSON.stringify(value);
|
||||||
|
|
@ -26,36 +18,34 @@ function sendJson(response, status, value) {
|
||||||
response.end(body);
|
response.end(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchPrograms() {
|
function fetchPrograms() {
|
||||||
const treeUrl = `${apiBase}/git/trees/${encodeURIComponent(branch)}?recursive=true`;
|
const programs = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
||||||
const upstream = await fetch(treeUrl, { signal: AbortSignal.timeout(10000) });
|
if (!Array.isArray(programs)) throw new Error('programs.json must contain an array');
|
||||||
if (!upstream.ok) throw new Error(`Repository returned HTTP ${upstream.status}`);
|
return programs.map(program => {
|
||||||
const tree = await upstream.json();
|
if (!program.path?.toLowerCase().endsWith('.prg')) throw new Error('Every program must reference a PRG file');
|
||||||
return tree.tree
|
const filePath = resolve(programDirectory, program.path);
|
||||||
.filter(entry => entry.type === 'blob' && entry.path.toLowerCase().endsWith('.prg'))
|
if (!filePath.startsWith(programDirectory + sep) || !existsSync(filePath) || !statSync(filePath).isFile()) {
|
||||||
.map(entry => ({ path: entry.path, size: entry.size }))
|
throw new Error(`Program file is missing: ${program.path}`);
|
||||||
.sort((left, right) => left.path.localeCompare(right.path));
|
}
|
||||||
|
return { ...program, size: statSync(filePath).size };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function proxyProgram(response, programPath) {
|
function sendProgram(response, programPath) {
|
||||||
const programs = await fetchPrograms();
|
const programs = fetchPrograms();
|
||||||
if (!programs.some(program => program.path === programPath)) {
|
if (!programs.some(program => program.path === programPath)) {
|
||||||
sendJson(response, 404, { error: 'Program not found' });
|
sendJson(response, 404, { error: 'Program not found' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const encodedPath = programPath.split('/').map(encodeURIComponent).join('/');
|
const filePath = resolve(programDirectory, programPath);
|
||||||
const rawUrl = `${repository}/raw/branch/${encodeURIComponent(branch)}/${encodedPath}`;
|
|
||||||
const upstream = await fetch(rawUrl, { signal: AbortSignal.timeout(10000) });
|
|
||||||
if (!upstream.ok) throw new Error(`Program download returned HTTP ${upstream.status}`);
|
|
||||||
const bytes = Buffer.from(await upstream.arrayBuffer());
|
|
||||||
response.writeHead(200, {
|
response.writeHead(200, {
|
||||||
'Content-Type': 'application/octet-stream',
|
'Content-Type': 'application/octet-stream',
|
||||||
'Content-Length': bytes.length,
|
'Content-Length': statSync(filePath).size,
|
||||||
'Content-Disposition': `inline; filename="${programPath.split('/').at(-1)}"`,
|
'Content-Disposition': `inline; filename="${programPath.split('/').at(-1)}"`,
|
||||||
'Cache-Control': 'no-store'
|
'Cache-Control': 'no-store'
|
||||||
});
|
});
|
||||||
response.end(bytes);
|
createReadStream(filePath).pipe(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
function serveStatic(response, pathname) {
|
function serveStatic(response, pathname) {
|
||||||
|
|
@ -80,15 +70,15 @@ createServer(async (request, response) => {
|
||||||
if (request.method !== 'GET') {
|
if (request.method !== 'GET') {
|
||||||
sendJson(response, 405, { error: 'Method not allowed' });
|
sendJson(response, 405, { error: 'Method not allowed' });
|
||||||
} else if (url.pathname === '/api/programs') {
|
} else if (url.pathname === '/api/programs') {
|
||||||
sendJson(response, 200, { repository, branch, programs: await fetchPrograms() });
|
sendJson(response, 200, { programs: fetchPrograms() });
|
||||||
} else if (url.pathname === '/api/program') {
|
} else if (url.pathname === '/api/program') {
|
||||||
await proxyProgram(response, url.searchParams.get('path') || '');
|
sendProgram(response, url.searchParams.get('path') || '');
|
||||||
} else if (!serveStatic(response, url.pathname)) {
|
} else if (!serveStatic(response, url.pathname)) {
|
||||||
sendJson(response, 404, { error: 'Not found' });
|
sendJson(response, 404, { error: 'Not found' });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (!response.headersSent) sendJson(response, 502, { error: 'Unable to reach the program repository' });
|
if (!response.headersSent) sendJson(response, 500, { error: 'Unable to read the program library' });
|
||||||
else response.end();
|
else response.end();
|
||||||
}
|
}
|
||||||
}).listen(port, '0.0.0.0', () => {
|
}).listen(port, '0.0.0.0', () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue