mirror of
https://github.com/BobbyWibowo/lolisafe.git
synced 2025-01-19 01:31:34 +00:00
feat: ServeStaticQuick
chokidar is now a production dependency please read the comments in ServeStaticQuick.js for a description of what the class does public and dist directories are now served with that class by default before starting hyper-express on the listen port, await for all ServeLiveDirectory and ServeStaticQuick instances
This commit is contained in:
parent
d40d1e396f
commit
2389974c7d
@ -95,6 +95,11 @@ class ServeLiveDirectory {
|
|||||||
return res.send(file.buffer)
|
return res.send(file.buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ready () {
|
||||||
|
// Returns a promise which resolves to true once LiveDirectory instance is ready
|
||||||
|
return this.instance.ready()
|
||||||
|
}
|
||||||
|
|
||||||
#middleware (req, res, next) {
|
#middleware (req, res, next) {
|
||||||
// Only process GET and HEAD requests
|
// Only process GET and HEAD requests
|
||||||
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
||||||
|
276
controllers/middlewares/ServeStaticQuick.js
Normal file
276
controllers/middlewares/ServeStaticQuick.js
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
/*
|
||||||
|
* ServeStaticQuick.
|
||||||
|
*
|
||||||
|
* This is intended as a compromise between ServeStatic handler and
|
||||||
|
* ServeLiveDirectory middleware.
|
||||||
|
*
|
||||||
|
* This monitors and caches the configured directory's file tree for quick lookups,
|
||||||
|
* thus allowing multiple instances of this middleware to be used together, if needed.
|
||||||
|
*
|
||||||
|
* When matches are found, it will then simply spawn ReadStream to the physical files.
|
||||||
|
* Due to the fact that it does not have to pre-cache the whole files into memory,
|
||||||
|
* this is likely the better choice to serve generic assets
|
||||||
|
* in an environment where memory space is a premium.
|
||||||
|
*
|
||||||
|
* This class also has Conditional GETs support,
|
||||||
|
* which involves handling cache-related headers such as
|
||||||
|
* If-Match, If-Unmodified-Since, ETag, etc.
|
||||||
|
* And partial bytes fetch by handling Content-Range header,
|
||||||
|
* which is useful for streaming, among other things.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const chokidar = require('chokidar')
|
||||||
|
const etag = require('etag')
|
||||||
|
const fs = require('fs')
|
||||||
|
const parseRange = require('range-parser')
|
||||||
|
const serveUtils = require('./../utils/serveUtils')
|
||||||
|
const logger = require('./../../logger')
|
||||||
|
|
||||||
|
class ServeStaticQuick {
|
||||||
|
directory
|
||||||
|
files
|
||||||
|
watcher
|
||||||
|
|
||||||
|
#options
|
||||||
|
#readyPromise
|
||||||
|
#readyResolve
|
||||||
|
|
||||||
|
constructor (directory, options = {}) {
|
||||||
|
if (!directory || typeof directory !== 'string') {
|
||||||
|
throw new TypeError('Root directory must be set')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.directory = serveUtils.forwardSlashes(directory)
|
||||||
|
|
||||||
|
// Ensure does not end with a forward slash
|
||||||
|
if (this.directory.endsWith('/')) {
|
||||||
|
this.directory = this.directory.slice(0, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.acceptRanges === undefined) {
|
||||||
|
options.acceptRanges = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.etag === undefined) {
|
||||||
|
options.etag = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.lastModified === undefined) {
|
||||||
|
options.lastModified = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.setHeaders && typeof options.setHeaders !== 'function') {
|
||||||
|
throw new TypeError('Middleware option setHeaders must be a function')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.files = new Map()
|
||||||
|
|
||||||
|
this.watcher = chokidar.watch(this.directory, {
|
||||||
|
alwaysStat: true,
|
||||||
|
awaitWriteFinish: {
|
||||||
|
pollInterval: 100,
|
||||||
|
stabilityThreshold: 500
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.#bindWatchHandlers()
|
||||||
|
|
||||||
|
this.#options = options
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Based on https://github.com/pillarjs/send/blob/0.18.0/index.js
|
||||||
|
* Copyright(c) 2012 TJ Holowaychuk
|
||||||
|
* Copyright(c) 2014-2022 Douglas Christopher Wilson
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
handler (req, res, stat) {
|
||||||
|
// ReadStream options
|
||||||
|
let len = stat.size
|
||||||
|
const opts = {}
|
||||||
|
let ranges = req.headers.range
|
||||||
|
let offset = 0
|
||||||
|
|
||||||
|
// set content-type
|
||||||
|
res.type(req.path)
|
||||||
|
|
||||||
|
// set header fields
|
||||||
|
this.#setHeaders(req, res, stat)
|
||||||
|
|
||||||
|
// conditional GET support
|
||||||
|
if (serveUtils.isConditionalGET(req)) {
|
||||||
|
if (serveUtils.isPreconditionFailure(req, res)) {
|
||||||
|
return res.status(412).end()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serveUtils.isFresh(req, res)) {
|
||||||
|
return res.status(304).end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// adjust len to start/end options
|
||||||
|
len = Math.max(0, len - offset)
|
||||||
|
if (opts.end !== undefined) {
|
||||||
|
const bytes = opts.end - offset + 1
|
||||||
|
if (len > bytes) len = bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range support
|
||||||
|
if (this.#options.acceptRanges && serveUtils.BYTES_RANGE_REGEXP.test(ranges)) {
|
||||||
|
// parse
|
||||||
|
ranges = parseRange(len, ranges, {
|
||||||
|
combine: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// If-Range support
|
||||||
|
if (!serveUtils.isRangeFresh(req, res)) {
|
||||||
|
// range stale
|
||||||
|
ranges = -2
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsatisfiable
|
||||||
|
if (ranges === -1) {
|
||||||
|
// Content-Range
|
||||||
|
res.header('Content-Range', serveUtils.contentRange('bytes', len))
|
||||||
|
|
||||||
|
// 416 Requested Range Not Satisfiable
|
||||||
|
return res.status(416).end()
|
||||||
|
}
|
||||||
|
|
||||||
|
// valid (syntactically invalid/multiple ranges are treated as a regular response)
|
||||||
|
if (ranges !== -2 && ranges.length === 1) {
|
||||||
|
// Content-Range
|
||||||
|
res.status(206)
|
||||||
|
res.header('Content-Range', serveUtils.contentRange('bytes', len, ranges[0]))
|
||||||
|
|
||||||
|
// adjust for requested range
|
||||||
|
offset += ranges[0].start
|
||||||
|
len = ranges[0].end - ranges[0].start + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set read options
|
||||||
|
opts.start = offset
|
||||||
|
opts.end = Math.max(offset, offset + len - 1)
|
||||||
|
|
||||||
|
// HEAD support
|
||||||
|
if (req.method === 'HEAD') {
|
||||||
|
// If HEAD, also set Content-Length (must be string)
|
||||||
|
res.header('Content-Length', String(len))
|
||||||
|
return res.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len === 0) {
|
||||||
|
res.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.#stream(req, res, stat, opts, len)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise which resolves to true once ServeStaticQuick is ready
|
||||||
|
ready () {
|
||||||
|
// Resolve with true if ready is not a promise
|
||||||
|
if (this.#readyPromise === true) return Promise.resolve(true)
|
||||||
|
|
||||||
|
// Create a promise if one does not exist for ready event
|
||||||
|
if (this.#readyPromise === undefined) { this.#readyPromise = new Promise((resolve) => (this.#readyResolve = resolve)) }
|
||||||
|
|
||||||
|
return this.#readyPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
#bindWatchHandlers () {
|
||||||
|
this.watcher.on('all', (event, path, stat) => {
|
||||||
|
const relPath = serveUtils.relativePath(this.directory, path)
|
||||||
|
|
||||||
|
if (!relPath) return // skips root directory
|
||||||
|
|
||||||
|
switch (event) {
|
||||||
|
case 'add':
|
||||||
|
case 'addDir':
|
||||||
|
case 'change':
|
||||||
|
this.files.set(relPath, stat)
|
||||||
|
break
|
||||||
|
case 'unlink':
|
||||||
|
case 'unlinkDir':
|
||||||
|
this.files.delete(relPath)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Bind 'ready' for when all files have been loaded
|
||||||
|
this.watcher.once('ready', () => {
|
||||||
|
// Resolve pending promise if one exists
|
||||||
|
if (typeof this.#readyResolve === 'function') {
|
||||||
|
this.#readyResolve()
|
||||||
|
this.#readyResolve = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark instance as ready
|
||||||
|
this.#readyPromise = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#get (path) {
|
||||||
|
const stat = this.files.get(path)
|
||||||
|
|
||||||
|
if (!stat || stat.isDirectory()) return
|
||||||
|
|
||||||
|
return stat
|
||||||
|
}
|
||||||
|
|
||||||
|
#middleware (req, res, next) {
|
||||||
|
// Only process GET and HEAD requests
|
||||||
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
const stat = this.#get(req.path)
|
||||||
|
if (stat === undefined) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.handler(req, res, stat)
|
||||||
|
}
|
||||||
|
|
||||||
|
#setHeaders (req, res, stat) {
|
||||||
|
// Always do external setHeaders function first,
|
||||||
|
// in case it will overwrite the following default headers anyways
|
||||||
|
if (this.#options.setHeaders) {
|
||||||
|
this.#options.setHeaders(req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#options.acceptRanges && !res.get('Accept-Ranges')) {
|
||||||
|
res.header('Accept-Ranges', 'bytes')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#options.lastModified && !res.get('Last-Modified')) {
|
||||||
|
const modified = stat.mtime.toUTCString()
|
||||||
|
res.header('Last-Modified', modified)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#options.etag && !res.get('ETag')) {
|
||||||
|
const val = etag(stat)
|
||||||
|
res.header('ETag', val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#stream (req, res, stat, opts, len) {
|
||||||
|
const fullPath = this.directory + req.path
|
||||||
|
const readStream = fs.createReadStream(fullPath, opts)
|
||||||
|
|
||||||
|
readStream.on('error', error => {
|
||||||
|
readStream.destroy()
|
||||||
|
logger.error(error)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2nd param will be set as Content-Length header (must be number)
|
||||||
|
return res.stream(readStream, len)
|
||||||
|
}
|
||||||
|
|
||||||
|
get middleware () {
|
||||||
|
return this.#middleware.bind(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ServeStaticQuick
|
18
lolisafe.js
18
lolisafe.js
@ -47,6 +47,7 @@ const ExpressCompat = require('./controllers/middlewares/ExpressCompat')
|
|||||||
const NunjucksRenderer = require('./controllers/middlewares/NunjucksRenderer')
|
const NunjucksRenderer = require('./controllers/middlewares/NunjucksRenderer')
|
||||||
const RateLimiter = require('./controllers/middlewares/RateLimiter')
|
const RateLimiter = require('./controllers/middlewares/RateLimiter')
|
||||||
const ServeLiveDirectory = require('./controllers/middlewares/ServeLiveDirectory')
|
const ServeLiveDirectory = require('./controllers/middlewares/ServeLiveDirectory')
|
||||||
|
const ServeStaticQuick = require('./controllers/middlewares/ServeStaticQuick')
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
const ServeStatic = require('./controllers/handlers/ServeStatic')
|
const ServeStatic = require('./controllers/handlers/ServeStatic')
|
||||||
@ -190,17 +191,17 @@ if (config.cacheControl) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init LiveDirectory middlewares for static assets
|
// Init ServeStaticQuick middlewares for static assets
|
||||||
// Static assets in /public directory
|
// Static assets in /public directory
|
||||||
const serveLiveDirectoryPublicInstance = new ServeLiveDirectory(paths.public, {
|
const serveStaticQuickPublicInstance = new ServeStaticQuick(paths.public, {
|
||||||
setHeaders: setHeadersForStaticAssets
|
setHeaders: setHeadersForStaticAssets
|
||||||
})
|
})
|
||||||
safe.use(serveLiveDirectoryPublicInstance.middleware)
|
safe.use(serveStaticQuickPublicInstance.middleware)
|
||||||
// Static assets in /dist directory
|
// Static assets in /dist directory
|
||||||
const serveLiveDirectoryDistInstance = new ServeLiveDirectory(paths.dist, {
|
const serveStaticQuickDistInstance = new ServeStaticQuick(paths.dist, {
|
||||||
setHeaders: setHeadersForStaticAssets
|
setHeaders: setHeadersForStaticAssets
|
||||||
})
|
})
|
||||||
safe.use(serveLiveDirectoryDistInstance.middleware)
|
safe.use(serveStaticQuickDistInstance.middleware)
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
safe.use(album)
|
safe.use(album)
|
||||||
@ -310,6 +311,13 @@ safe.use('/api', api)
|
|||||||
logger.log(`Connection established with ${utils.scan.version}`)
|
logger.log(`Connection established with ${utils.scan.version}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Await all ServeLiveDirectory and ServeStaticQuick instances
|
||||||
|
await Promise.all([
|
||||||
|
serveStaticQuickDistInstance.ready(),
|
||||||
|
serveStaticQuickPublicInstance.ready(),
|
||||||
|
serveLiveDirectoryCustomPagesInstance.ready()
|
||||||
|
])
|
||||||
|
|
||||||
// Binds Express to port
|
// Binds Express to port
|
||||||
await safe.listen(utils.conf.port)
|
await safe.listen(utils.conf.port)
|
||||||
logger.log(`lolisafe started on port ${utils.conf.port}`)
|
logger.log(`lolisafe started on port ${utils.conf.port}`)
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
"bcrypt": "~5.0.1",
|
"bcrypt": "~5.0.1",
|
||||||
"better-sqlite3": "~7.6.2",
|
"better-sqlite3": "~7.6.2",
|
||||||
"blake3": "~2.1.7",
|
"blake3": "~2.1.7",
|
||||||
|
"chokidar": "~3.5.3",
|
||||||
"clamscan": "~2.1.2",
|
"clamscan": "~2.1.2",
|
||||||
"content-disposition": "~0.5.4",
|
"content-disposition": "~0.5.4",
|
||||||
"etag": "~1.8.1",
|
"etag": "~1.8.1",
|
||||||
@ -62,7 +63,6 @@
|
|||||||
"@ronilaukkarinen/gulp-stylelint": "~14.0.6",
|
"@ronilaukkarinen/gulp-stylelint": "~14.0.6",
|
||||||
"browserslist": "~4.21.3",
|
"browserslist": "~4.21.3",
|
||||||
"bulma": "~0.9.4",
|
"bulma": "~0.9.4",
|
||||||
"chokidar": "~3.5.3",
|
|
||||||
"cssnano": "~5.1.12",
|
"cssnano": "~5.1.12",
|
||||||
"del": "~6.1.1",
|
"del": "~6.1.1",
|
||||||
"eslint": "~8.20.0",
|
"eslint": "~8.20.0",
|
||||||
|
Loading…
Reference in New Issue
Block a user