rexuws
Version:
An express-like framework built on top of uWebsocket.js aims at simple codebase and high performance
267 lines (266 loc) • 11.3 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.StaticServer = void 0;
/* eslint-disable consistent-return */
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable no-await-in-loop */
const promises_1 = __importDefault(require("fs/promises"));
const mime_types_1 = require("mime-types");
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
// eslint-disable-next-line import/no-cycle
const router_1 = require("../../../router");
const utils_1 = require("../../../utils/utils");
const fileWatcher_1 = __importDefault(require("../../../utils/fileWatcher"));
class StaticServer {
constructor(path) {
if (!path) {
throw new TypeError('Missing path for serving static contents');
}
if (!path) {
throw new TypeError('Path must be a string');
}
this.path = path;
this.pathMappers = new Map();
}
static Config(opts, logger) {
const defaultOpts = {
maxSize: 1024 * 1000,
useCache: true,
watcher: true,
maxAge: 604800,
};
if (!opts || typeof opts !== 'object' || Object.keys(opts).length <= 0)
this.Options = defaultOpts;
this.Options = {
...opts,
...defaultOpts,
};
if (logger) {
this.Logger = logger;
}
this.Watcher = opts.watcher || false;
}
async readFilesOrDir(dirContent, parentPath) {
StaticServer.Logger.info(`Reading files in ${parentPath || dirContent}...`);
for (let i = 0; i < dirContent.length; i++) {
const name = parentPath
? `${parentPath}/${dirContent[i]}`
: dirContent[i];
const fullPath = path_1.default.join(this.path, name);
StaticServer.Logger.info(`Reading ${name} (${fullPath}) `);
const stat = await promises_1.default.stat(fullPath);
if (stat.isFile()) {
const { name: key, data } = await StaticServer.readFileFromPathAndStat(fullPath, stat, name);
this.pathMappers.set(key, data);
}
else if (stat.isDirectory()) {
this.pathMappers.set(name, { isDirectory: true });
const childDirContent = await promises_1.default.readdir(fullPath);
await this.readFilesOrDir(childDirContent, name);
}
}
}
static async readFileFromPathAndStat(fullPath, stat, name) {
let completeMime = '';
const { mtime } = stat;
// Need to set milliseconds to zero to avoid mis-compared UTCString to the original one
mtime.setMilliseconds(0);
if (this.Options.useCache) {
let mime = await utils_1.getMime(fullPath);
// eslint-disable-next-line prefer-destructuring
if (Array.isArray(mime))
mime = mime[0];
if (mime === 'text/plain') {
const namePart = name ? name.split('/') : fullPath.split('/');
completeMime = mime_types_1.contentType(namePart[namePart.length - 1]) || mime;
}
else
completeMime = mime === 'text/html' ? 'text/html; charset=utf-8' : mime;
}
if (this.Options.maxSize === -1 || stat.size > this.Options.maxSize) {
return {
data: {
fileSize: stat.size,
mime: completeMime,
lastModified: stat.mtime.toUTCString(),
mtime: stat.mtime,
},
name,
};
}
return {
name,
data: {
fileSize: stat.size,
mime: completeMime,
data: await promises_1.default.readFile(fullPath),
lastModified: stat.mtime.toUTCString(),
mtime: stat.mtime,
},
};
}
async init() {
try {
const dir = await promises_1.default.readdir(this.path);
await this.readFilesOrDir(dir);
StaticServer.Store[this.path] = this.pathMappers;
}
catch (error) {
StaticServer.Logger.trace(error);
}
}
static GetRouter() {
const router = new router_1.DefaultRouter();
const setupPaths = Object.keys(this.Store);
if (this.Watcher === true) {
this.Logger.info('[Watcher] Watching', ...setupPaths);
if (!this.FileWatcher) {
this.FileWatcher = new fileWatcher_1.default(setupPaths, {
add: (baseUrl, path, stats) => {
this.Logger.info('[Watcher] Add file to', baseUrl, path);
this.readFileFromPathAndStat(`${baseUrl}/${path}`, stats, path)
.then(({ data }) => {
this.Store[baseUrl].set(path, {
data: data.data,
fileSize: data.fileSize,
isDirectory: data.isDirectory,
mime: data.mime,
});
})
.catch((err) => {
this.Logger.error('Something went wrong while reading file added file', `${baseUrl}/${path}`);
this.Logger.trace(err);
});
},
change: (baseUrl, path, stats) => {
this.Logger.info('[Watcher] File gets changed in', baseUrl, path);
this.readFileFromPathAndStat(`${baseUrl}/${path}`, stats, path)
.then(({ data }) => {
this.Store[baseUrl].set(path, {
data: data.data,
fileSize: data.fileSize,
isDirectory: data.isDirectory,
mime: data.mime,
});
})
.catch((err) => {
this.Logger.error('Something went wrong while reading changed file', `${baseUrl}/${path}`);
this.Logger.trace(err);
});
},
unlink: (baseUrl, path) => {
this.Logger.info('[Watcher] Remove file in', baseUrl, path);
this.Store[baseUrl].delete(path);
},
});
}
}
return router.get('/*', (req, res, next) => {
const file = req.originalUrl.startsWith('/')
? req.originalUrl.substring(1)
: req.originalUrl;
// look up for file in the store
for (let i = 0; i < setupPaths.length; i++) {
const found = this.Store[setupPaths[i]].get(file);
if (found) {
if (found.isDirectory) {
return next(`Cannot GET ${req.url}`);
}
const { data, mime, fileSize, lastModified, mtime } = found;
const headerIfModified = req.header('if-modified-since');
if (headerIfModified) {
if (new Date(headerIfModified) >= mtime) {
return res.status(304).end();
}
}
if (data)
return res.sendFile(data, {
mime,
fileSize,
lastModified,
maxAge: this.Options.maxAge,
});
// if there is no data which means fs.readFile hasn't been call and this
// file is not elegible for caching
// use res.sendfile to handle the action
// the mime type has already been retrieve so pass it directly to res.sendFile to skip calling fs.getStat()
return res.sendFile(path_1.default.join(setupPaths[i], file), {
mime,
fileSize,
lastModified,
maxAge: this.Options.maxAge,
});
}
}
// look up for file changes setting
// eslint-disable-next-line consistent-return
if (this.Watcher !== 'onNotFound')
return next(`Cannot GET ${req.url}`);
// Get relative path in folder
let fileStat;
let fullPath;
let foundDir;
for (let i = 0; i < setupPaths.length; i++) {
if (!fileStat)
try {
const joinedPath = path_1.default.join(setupPaths[i], file);
fileStat = fs_1.default.statSync(joinedPath);
fullPath = joinedPath;
foundDir = setupPaths[i];
return;
}
catch (error) {
// do nothing
}
}
try {
if (!fileStat)
return next(`Cannot GET ${req.url}`);
if (fileStat.isDirectory())
return next(`Cannot GET ${req.url}`);
const { name, data: result } = this.readFileFromPathAndStat(fullPath, fileStat, file);
// update in relative store
this.Store[foundDir].set(name, result);
const { data, mime, fileSize, lastModified, mtime } = result;
const headerIfModified = req.header('if-modified-since');
if (headerIfModified) {
if (new Date(headerIfModified) >= mtime) {
return res.status(304).end();
}
}
// if there is no data which means fs.readFile hasn't been call and this
// file is not elegible for caching
// use res.sendfile to handle
// the mime type has already been retrieve so pass it directly to res.sendFile
if (!data)
return res.sendFile(fullPath, {
mime,
fileSize,
lastModified,
maxAge: this.Options.maxAge,
});
return res.sendFile(data, {
mime,
lastModified,
maxAge: this.Options.maxAge,
});
}
catch {
return next(`Cannot GET ${req.url}`);
}
});
}
}
exports.StaticServer = StaticServer;
StaticServer.Store = {};
StaticServer.Options = {
maxSize: 1024 * 100,
useCache: true,
watcher: true,
};
StaticServer.Logger = console;
StaticServer.Watcher = false;