Implementing API - WIP frontend
This commit is contained in:
+136
-42
@@ -7,18 +7,17 @@
|
||||
* @requires node:http
|
||||
* @requires node:fs/promises
|
||||
* @requires node:path
|
||||
* @requires ./vmdParser.js
|
||||
* @requires ./gpxParser.js
|
||||
* @requires ./files.js
|
||||
*
|
||||
* @see {@link https://nodejs.org/api/http.html}
|
||||
* @see {@link https://nodejs.org/api/fs.html}
|
||||
*/
|
||||
|
||||
import * as http from "node:http";
|
||||
import {URL} from "node:url";
|
||||
import fs from "node:fs/promises";
|
||||
import {join, resolve, extname} from "node:path";
|
||||
import {vmdParser, VMDReference} from "./vmdParser.js";
|
||||
import {gpxParser} from "./gpxParser.js";
|
||||
import {DataFile, GpxFile, VmdFile, VMDReference} from "./fileHandler.js";
|
||||
|
||||
/**
|
||||
* Configuration options for the web server.
|
||||
@@ -26,7 +25,7 @@ import {gpxParser} from "./gpxParser.js";
|
||||
* @typedef {Object} WebserverOptions
|
||||
* @property {number} [port=4399] - Port number to listen on
|
||||
* @property {string} [publicDir='app/public'] - Directory for static assets
|
||||
* @property {string} [dataDir='data/routes'] - Directory for route data files
|
||||
* @property {string} [dataDir='data'] - Path to data directory
|
||||
* @property {number} [maxBodySize=1048576] - Maximum request body size in bytes (1MB)
|
||||
* @property {number} [timeout=30000] - Request timeout in milliseconds (30s)
|
||||
*/
|
||||
@@ -154,7 +153,7 @@ export default class Webserver {
|
||||
* maxBodySize: 2097152
|
||||
* });
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
constructor(options = {dataDir: null}) {
|
||||
this.#validateOptions(options);
|
||||
|
||||
this.port = options.port ?? 4399;
|
||||
@@ -195,6 +194,9 @@ export default class Webserver {
|
||||
if (options.publicDir !== undefined && typeof options.publicDir !== "string")
|
||||
throw new TypeError("publicDir must be a string");
|
||||
|
||||
if (options.dataDir !== undefined && typeof options.dataDir !== "string")
|
||||
throw new TypeError("dataDir must be a string");
|
||||
|
||||
if (
|
||||
options.maxBodySize !== undefined &&
|
||||
(typeof options.maxBodySize !== "number" || options.maxBodySize <= 0)
|
||||
@@ -342,8 +344,7 @@ export default class Webserver {
|
||||
* // Returns "text/html; charset=utf-8"
|
||||
*/
|
||||
#getMimeType(filePath) {
|
||||
const ext = extname(filePath).toLowerCase();
|
||||
return MIME_TYPES[ext] || "application/text";
|
||||
return MIME_TYPES[extname(filePath).toLowerCase()] || "application/text";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -398,20 +399,28 @@ export default class Webserver {
|
||||
*/
|
||||
async #handleGpxRequest(req, res) {
|
||||
const relativePath = req.url.slice(1); // Remove leading slash
|
||||
const safePath = this.#resolveSafePath(this.#dataDir, relativePath);
|
||||
|
||||
if (!safePath) {
|
||||
this.#sendErrorPage(res, STATUS_CODES.FORBIDDEN, "Invalid path");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await gpxParser(safePath, "auto-generated-ref");
|
||||
// Use DataFile.open() which returns GpxFile for .gpx extensions
|
||||
const file = DataFile.open(relativePath, this.#dataDir);
|
||||
|
||||
// Parse the GPX file
|
||||
await file.parse(null);
|
||||
|
||||
// Return parsed data
|
||||
res.writeHead(STATUS_CODES.OK, {"Content-Type": "application/json; charset=utf-8"});
|
||||
res.end(JSON.stringify(data, null, 2));
|
||||
res.end(
|
||||
JSON.stringify(
|
||||
{Reference: file.reference, Title: file.title, points: file.points},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[GPX ERROR]", error.message);
|
||||
this.#sendErrorPage(res, STATUS_CODES.INTERNAL_ERROR, "Failed to parse GPX file");
|
||||
if (error.message.includes("not found"))
|
||||
this.#sendErrorPage(res, STATUS_CODES.NOT_FOUND, error.message);
|
||||
else this.#sendErrorPage(res, STATUS_CODES.INTERNAL_ERROR, "Failed to parse GPX file");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,6 +511,82 @@ export default class Webserver {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle API calls
|
||||
*
|
||||
* @private
|
||||
* @method handleAPIcall
|
||||
* @param {http.IncomingMessage} req - Request object
|
||||
* @param {http.ServerResponse} res - Response object
|
||||
* @param {string} body - Parsed request body content
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async #handleAPIcall(req, res, body) {
|
||||
const apiurl = new URL(req.url, `api://localhost:${this.port}/`);
|
||||
const [contentType, action] = apiurl.pathname.replace("/api/", "").split("/");
|
||||
|
||||
switch (contentType) {
|
||||
case "vmd": {
|
||||
switch (action) {
|
||||
case "countries": {
|
||||
const list = await DataFile.list(
|
||||
apiurl.searchParams.get("type"),
|
||||
this.#dataDir,
|
||||
{onlyfiles: false}
|
||||
);
|
||||
res.writeHead(STATUS_CODES.OK, {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
});
|
||||
res.end(JSON.stringify(list));
|
||||
break;
|
||||
}
|
||||
case "entries": {
|
||||
const relativePath =
|
||||
apiurl.searchParams.get("type") +
|
||||
"/" +
|
||||
apiurl.searchParams.get("country");
|
||||
console.log(relativePath);
|
||||
|
||||
const list = await VmdFile.list(relativePath, this.#dataDir, {depth: 1});
|
||||
res.writeHead(STATUS_CODES.OK, {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
});
|
||||
res.end(JSON.stringify(list));
|
||||
break;
|
||||
}
|
||||
case "entry": {
|
||||
const vmdobj = new VmdFile(body, this.#dataDir);
|
||||
const t = await vmdobj.parse();
|
||||
res.writeHead(STATUS_CODES.OK, {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
});
|
||||
res.end(JSON.stringify(vmdobj));
|
||||
break;
|
||||
}
|
||||
case "reference": {
|
||||
res.writeHead(STATUS_CODES.OK, {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
});
|
||||
res.end(JSON.stringify(new VMDReference(body, this.#dataDir)));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
res.writeHead(STATUS_CODES.NOT_FOUND, {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
});
|
||||
res.end(
|
||||
JSON.stringify({error: "endpoint does not exist or is not implemented"})
|
||||
);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle VMD reference requests.
|
||||
* Processes POST requests with VMD reference data.
|
||||
@@ -517,7 +602,7 @@ export default class Webserver {
|
||||
*/
|
||||
async #handleVmdRequest(req, res, body) {
|
||||
try {
|
||||
const vmd = new VMDReference(body);
|
||||
const vmd = new VMDReference(body, this.#dataDir);
|
||||
|
||||
if (!vmd.path) {
|
||||
const defaultResponse = {
|
||||
@@ -532,26 +617,35 @@ export default class Webserver {
|
||||
return;
|
||||
}
|
||||
|
||||
const safePath = this.#resolveSafePath(this.#dataDir, vmd.path);
|
||||
// Use DataFile.open() which returns VmdFile or GpxFile based on extension
|
||||
const file = DataFile.open(vmd.path, this.#dataDir);
|
||||
|
||||
if (!safePath)
|
||||
return this.#sendErrorPage(res, STATUS_CODES.FORBIDDEN, "Invalid VMD path");
|
||||
// Parse the file (handles both VMD and GPX)
|
||||
await file.parse(vmd);
|
||||
|
||||
if (vmd.path.endsWith(".vmd")) {
|
||||
const data = await fs.readFile(safePath, "utf-8");
|
||||
const vmdObject = vmdParser(data, vmd);
|
||||
// Return parsed data
|
||||
let responseData;
|
||||
if (file instanceof VmdFile) {
|
||||
responseData = file.toJSON();
|
||||
} else if (file instanceof GpxFile) {
|
||||
responseData = {Reference: file.reference, Title: file.title, points: file.points};
|
||||
} else {
|
||||
// Generic file
|
||||
responseData = {content: file.content};
|
||||
}
|
||||
|
||||
res.writeHead(STATUS_CODES.OK, {"Content-Type": "application/json; charset=utf-8"});
|
||||
res.end(JSON.stringify(vmdObject, null, 2));
|
||||
} else if (vmd.path.endsWith(".gpx")) {
|
||||
const data = await gpxParser(safePath, vmd);
|
||||
|
||||
res.writeHead(STATUS_CODES.OK, {"Content-Type": "application/json; charset=utf-8"});
|
||||
res.end(JSON.stringify(data, null, 2));
|
||||
} else this.#sendErrorPage(res, STATUS_CODES.BAD_REQUEST, "Unsupported file type");
|
||||
res.writeHead(STATUS_CODES.OK, {"Content-Type": "application/json; charset=utf-8"});
|
||||
res.end(JSON.stringify(responseData, null, 2));
|
||||
} catch (error) {
|
||||
console.error("[VMD ERROR]", error.message);
|
||||
this.#sendErrorPage(res, STATUS_CODES.INTERNAL_ERROR, "Failed to process VMD request");
|
||||
if (error.message.includes("not found"))
|
||||
this.#sendErrorPage(res, STATUS_CODES.NOT_FOUND, error.message);
|
||||
else
|
||||
this.#sendErrorPage(
|
||||
res,
|
||||
STATUS_CODES.INTERNAL_ERROR,
|
||||
"Failed to process VMD request"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -632,6 +726,11 @@ export default class Webserver {
|
||||
return;
|
||||
}
|
||||
|
||||
if (decodedUrl.startsWith("/api/")) {
|
||||
const body = await this.#collectBody(req);
|
||||
return await this.#handleAPIcall(req, res, body);
|
||||
}
|
||||
|
||||
// 2. POST requests with VMD reference
|
||||
if (req.method === "POST" && req.headers["content-type"] === "vmd/reference") {
|
||||
const body = await this.#collectBody(req);
|
||||
@@ -646,24 +745,19 @@ export default class Webserver {
|
||||
}
|
||||
|
||||
// 4. Static public files
|
||||
const relativePath = decodedUrl.slice(1) || "index.html";
|
||||
let filePath = join(this.#publicDir, relativePath);
|
||||
let relativePath = decodedUrl.slice(1) || "index.html";
|
||||
|
||||
// Append .html extension if no extension present
|
||||
if (!extname(relativePath)) {
|
||||
filePath = join(this.#publicDir, relativePath + ".html");
|
||||
}
|
||||
if (!extname(relativePath)) relativePath += ".html";
|
||||
|
||||
const safePath = this.#resolveSafePath(this.#publicDir, relativePath);
|
||||
|
||||
if (!safePath) {
|
||||
this.#sendErrorPage(res, STATUS_CODES.FORBIDDEN, "Invalid path");
|
||||
return;
|
||||
}
|
||||
if (!safePath) return this.#sendErrorPage(res, STATUS_CODES.FORBIDDEN, "Invalid path");
|
||||
|
||||
await this.#serveStaticFile(res, safePath, true);
|
||||
} catch (error) {
|
||||
console.error("[REQUEST ERROR]", error.message);
|
||||
throw error;
|
||||
this.#sendErrorPage(res, STATUS_CODES.INTERNAL_ERROR, "An unexpected error occurred");
|
||||
} finally {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
Reference in New Issue
Block a user