astronomical-theater/handlers/static.js

77 lines
2.9 KiB
JavaScript

import * as path from "path"
import * as fs from "fs";
import DefaultHandler from "./default.js";
let fileTypeFromFile;
import("file-type").then(ft => fileTypeFromFile = ft.default.fromFile).catch(console.error);
export default class StaticHandler extends DefaultHandler {
constructor(basePath, directoryList = false) {
super();
this.basePath = basePath;
this.directoryList = directoryList;
//This will throw if the file doesn't exist
this.isSingleFile = !fs.statSync(basePath).isDirectory();
this.matchesSubpaths = !this.isSingleFile;
}
// This is the handler
// url is the URL object, with url.pathname being the path. p is the 'client-side' basepath
async handle (url, p, socket) {
const relativePath = path.relative(p, url.pathname);
//Concat and normalize the passed URL as being relative to the base path
const toServe = path.join(this.basePath, relativePath);
//If the resulting path is a parent, relative to basePath, disallow that
if (path.relative(this.basePath, toServe).startsWith("..")) {
return "50"
}
console.log(`Attempting to serve ${toServe}.`);
try {
if (fs.statSync(toServe).isDirectory()) {
console.log(`File is a directory and directoryList is ${this.directoryList}.`);
if (this.directoryList) {
//TODO: Static content directory listing
} else {
return "51 Not Found\r\n";
}
}
}catch (e) {
//If the file doesn't exist
console.log(`File doesn't exist or can't be opened.`)
return "51 Not Found\r\n";
}
let mimeType = "text/gemini";
if (fileTypeFromFile) {
const type = await fileTypeFromFile(toServe);
// `type` will be undefined if the file is plaintext
// In which case we want to fall down to serving plaintext
if (type) {
mimeType = type.mime;
}
}
//TODO: some file-ending based logic, please
//So we can serve md and txt
//TODO: convert line endings
// I read somewhere that `readFileSync` on every request is a bad idea, but I don't care
socket.write(`20 ${mimeType}\r\n`);
const fileReader = fs.createReadStream(toServe);
//This function (handle) needs to return after we're done writing the file to the stream
//Because the place that called this is going to handle .end() the socket
//So we don't end the socket here, and we make a promise to wait for the reader to end before returning
//It would be great if we could just not return from this function but I don't think that's how this works
fileReader.pipe(socket, {end: false});
await new Promise((res, rej) => fileReader.on("end", res));
return "";
}
}