You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

213 lines
5.8 KiB

4 years ago
'use strict'
const path = require('path')
const url = require('url')
const statSync = require('fs').statSync
const { PassThrough } = require('readable-stream')
const glob = require('glob')
const send = require('send')
const fp = require('fastify-plugin')
function fastifyStatic (fastify, opts, next) {
const error = checkRootPathForErrors(fastify, opts.root)
if (error !== undefined) return next(error)
const setHeaders = opts.setHeaders
if (setHeaders !== undefined && typeof setHeaders !== 'function') {
return next(new TypeError('The `setHeaders` option must be a function'))
}
const sendOptions = {
root: opts.root,
acceptRanges: opts.acceptRanges,
cacheControl: opts.cacheControl,
dotfiles: opts.dotfiles,
etag: opts.etag,
extensions: opts.extensions,
immutable: opts.immutable,
index: opts.index,
lastModified: opts.lastModified,
maxAge: opts.maxAge
}
function pumpSendToReply (request, reply, pathname, rootPath) {
var options = Object.assign({}, sendOptions)
if (rootPath) {
options.root = rootPath
}
const stream = send(request.raw, pathname, options)
var resolvedFilename
stream.on('file', function (file) {
resolvedFilename = file
})
const wrap = new PassThrough({
flush (cb) {
this.finished = true
if (reply.res.statusCode === 304) {
reply.send('')
}
cb()
}
})
wrap.getHeader = reply.getHeader.bind(reply)
wrap.setHeader = reply.header.bind(reply)
wrap.socket = request.raw.socket
wrap.finished = false
Object.defineProperty(wrap, 'filename', {
get () {
return resolvedFilename
}
})
Object.defineProperty(wrap, 'statusCode', {
get () {
return reply.res.statusCode
},
set (code) {
reply.code(code)
}
})
wrap.on('pipe', function () {
reply.send(wrap)
})
if (setHeaders !== undefined) {
stream.on('headers', setHeaders)
}
stream.on('directory', function (res, path) {
if (opts.redirect === true) {
const parsed = url.parse(request.raw.url)
reply.redirect(301, parsed.pathname + '/' + (parsed.search || ''))
} else {
reply.callNotFound()
}
})
stream.on('error', function (err) {
if (err) {
if (err.code === 'ENOENT') {
return reply.callNotFound()
}
reply.send(err)
}
})
// we cannot use pump, because send error
// handling is not compatible
stream.pipe(wrap)
}
if (opts.prefix === undefined) opts.prefix = '/'
let prefix = opts.prefix
if (!opts.prefixAvoidTrailingSlash) {
prefix = opts.prefix[opts.prefix.length - 1] === '/' ? opts.prefix : (opts.prefix + '/')
}
// Set the schema hide property if defined in opts or true by default
const schema = { schema: { hide: typeof opts.schemaHide !== 'undefined' ? opts.schemaHide : true } }
if (opts.decorateReply !== false) {
fastify.decorateReply('sendFile', function (filePath, rootPath) {
pumpSendToReply(this.request, this, filePath, rootPath)
})
}
if (opts.serve !== false) {
if (opts.wildcard === undefined || opts.wildcard === true) {
fastify.get(prefix + '*', schema, function (req, reply) {
pumpSendToReply(req, reply, '/' + req.params['*'])
})
if (opts.redirect === true && prefix !== opts.prefix) {
fastify.get(opts.prefix, schema, function (req, reply) {
const parsed = url.parse(req.raw.url)
reply.redirect(301, parsed.pathname + '/' + (parsed.search || ''))
})
}
} else {
const globPattern = typeof opts.wildcard === 'string' ? opts.wildcard : '**/*'
glob(path.join(sendOptions.root, globPattern), { nodir: true }, function (err, files) {
if (err) {
return next(err)
}
const indexDirs = new Set()
const indexes = typeof opts.index === 'undefined' ? ['index.html'] : [].concat(opts.index || [])
for (let file of files) {
file = file.replace(sendOptions.root.replace(/\\/g, '/'), '').replace(/^\//, '')
const route = (prefix + file).replace(/\/\//g, '/')
fastify.get(route, schema, function (req, reply) {
pumpSendToReply(req, reply, '/' + file)
})
if (indexes.includes(path.posix.basename(route))) {
indexDirs.add(path.posix.dirname(route))
}
}
indexDirs.forEach(function (dirname) {
const pathname = dirname + (dirname.endsWith('/') ? '' : '/')
const file = '/' + pathname.replace(prefix, '')
fastify.get(pathname, schema, function (req, reply) {
pumpSendToReply(req, reply, file)
})
if (opts.redirect === true) {
fastify.get(pathname.replace(/\/$/, ''), schema, function (req, reply) {
pumpSendToReply(req, reply, file.replace(/\/$/, ''))
})
}
})
next()
})
// return early to avoid calling next afterwards
return
}
}
next()
}
function checkRootPathForErrors (fastify, rootPath) {
if (rootPath === undefined) {
return new Error('"root" option is required')
}
if (typeof rootPath !== 'string') {
return new Error('"root" option must be a string')
}
if (path.isAbsolute(rootPath) === false) {
return new Error('"root" option must be an absolute path')
}
var pathStat
try {
pathStat = statSync(rootPath)
} catch (e) {
if (e.code === 'ENOENT') {
fastify.log.warn(`"root" path "${rootPath}" must exist`)
return
}
return e
}
if (pathStat.isDirectory() === false) {
return new Error('"root" option must point to a directory')
}
}
module.exports = fp(fastifyStatic, {
fastify: '>=2.0.0',
name: 'fastify-static'
})