mirror of
https://github.com/BobbyWibowo/lolisafe.git
synced 2024-12-14 16:36:21 +00:00
277 lines
7.1 KiB
JavaScript
277 lines
7.1 KiB
JavaScript
|
/*
|
||
|
* 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
|