Files
gd-list/server/server.js
T

151 lines
4.2 KiB
JavaScript

const http = require('http');
const fs = require('fs');
const path = require('path');
const { URL } = require('url');
const appRoot = path.resolve(__dirname, '..');
const dataPath = path.join(__dirname, 'data.txt');
const port = Number(process.env.PORT) || 3000;
const host = process.env.HOST || '127.0.0.1';
const clients = new Set();
const contentTypes = {
'.html': 'text/html; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.txt': 'text/plain; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon'
};
function parseData(text) {
return text
.split(/\r?\n/)
.map(line => line.trim())
.filter(Boolean)
.map(line => {
const parts = line.split('|').map(part => part.trim());
return {
level: parts[0] || 'Unknown',
position: parts[1] || '',
title: parts[2] || 'Untitled',
url: parts[3] || ''
};
});
}
function readDataText() {
return fs.readFileSync(dataPath, 'utf8');
}
function sendJson(res, statusCode, payload) {
res.writeHead(statusCode, {
'Content-Type': 'application/json; charset=utf-8',
'Cache-Control': 'no-store'
});
res.end(JSON.stringify(payload));
}
function sendEvent(eventName, data) {
const message = `event: ${eventName}\ndata: ${JSON.stringify(data)}\n\n`;
for (const client of clients) {
client.write(message);
}
}
function serveFile(reqPath, res) {
let filePath = path.join(appRoot, reqPath === '/' ? 'index.html' : reqPath.slice(1));
filePath = path.normalize(filePath);
if (!filePath.startsWith(appRoot)) {
res.writeHead(403, {'Content-Type': 'text/plain; charset=utf-8'});
res.end('Forbidden');
return;
}
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(err.code === 'ENOENT' ? 404 : 500, {'Content-Type': 'text/plain; charset=utf-8'});
res.end(err.code === 'ENOENT' ? 'Not found' : 'Server error');
return;
}
const ext = path.extname(filePath).toLowerCase();
res.writeHead(200, {
'Content-Type': contentTypes[ext] || 'application/octet-stream',
'Cache-Control': ext === '.html' ? 'no-store' : 'no-cache'
});
res.end(data);
});
}
const server = http.createServer((req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
if (req.method === 'GET' && url.pathname === '/api/list') {
try {
const text = readDataText();
sendJson(res, 200, { items: parseData(text), text });
} catch (error) {
sendJson(res, 500, { error: 'Could not read server/data.txt' });
}
return;
}
if (req.method === 'PUT' && url.pathname === '/api/list') {
let body = '';
req.on('data', chunk => {
body += chunk;
if (body.length > 5 * 1024 * 1024) {
req.destroy();
}
});
req.on('end', () => {
try {
const payload = JSON.parse(body || '{}');
const text = String(payload.text || '').trim();
fs.writeFileSync(dataPath, `${text}\n`, 'utf8');
sendEvent('list-update', { updatedAt: new Date().toISOString() });
sendJson(res, 200, { ok: true });
} catch (error) {
sendJson(res, 400, { error: 'Invalid list payload' });
}
});
return;
}
if (req.method === 'GET' && url.pathname === '/events') {
res.writeHead(200, {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive'
});
res.write('retry: 3000\n\n');
clients.add(res);
req.on('close', () => {
clients.delete(res);
});
return;
}
if (req.method !== 'GET' && req.method !== 'HEAD') {
res.writeHead(405, {'Content-Type': 'text/plain; charset=utf-8'});
res.end('Method not allowed');
return;
}
serveFile(url.pathname, res);
});
fs.watch(dataPath, { persistent: true }, () => {
sendEvent('list-update', { updatedAt: new Date().toISOString() });
});
server.listen(port, host, () => {
console.log(`fedl server running at http://${host}:${port}`);
console.log(`Using live list file: ${dataPath}`);
});