UNPKG

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
"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;