Skip to content

Commit

Permalink
feat: add support for multiple root directories under a single prefix (
Browse files Browse the repository at this point in the history
…#169)

* feat: add support for multiple root directories under a single prefix

* revert: undo test changes to the server example

* wip: support glob paths when using multiple root path array

* feat: add support for glob filenames on multiple root paths

* chore: add missing newline and additional note to readme.

* tests: add back in tests that got moved or removed during dev

* fix: change async for util.promisify and update glob function

* fix: switch to async/await and use promises for glob routes

* chore: move promisify glob to top of file

* Update README.md

Co-authored-by: Simone Busoli <simone.busoli@gmail.com>

* fix: cleanups and extra docs on examples

Co-authored-by: Simone Busoli <simone.busoli@gmail.com>
  • Loading branch information
nigelhanlon and simoneb authored Jan 14, 2021
1 parent 756b95d commit 38638bc
Show file tree
Hide file tree
Showing 13 changed files with 407 additions and 136 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ The absolute path of the directory that contains the files to serve.
The file to serve will be determined by combining `req.url` with the
provided root directory.

You can also provide an array of directories containing files to serve.
This is useful for serving multiple static directories under a single prefix. Files are served in a "first found, first served" manner, so the order in which you list the directories is important. For best performance, you should always list your main asset directory first. Duplicate paths will raise an error.

#### `prefix`

Default: `'/'`
Expand Down
4 changes: 4 additions & 0 deletions example/public2/test.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
body {
background-color: black;
color: white;
}
8 changes: 8 additions & 0 deletions example/public2/test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="test.css"></link>
</head>
<body>
<h1>Test 2</h1>
</body>
</html>
7 changes: 5 additions & 2 deletions example/server-compress.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ const path = require('path')
const fastify = require('fastify')({ logger: { level: 'trace' } })

fastify
// compress everything
// Compress everything.
.register(require('fastify-compress'), { threshold: 0 })
.register(require('../'), { root: path.join(__dirname, '/public') })
.register(require('../'), {
// An absolute path containing static files to serve.
root: path.join(__dirname, '/public')
})
.listen(3000, err => {
if (err) throw err
})
43 changes: 25 additions & 18 deletions example/server-dir-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,42 @@ const Handlebars = require('handlebars')

const fastify = require('fastify')({ logger: { level: 'trace' } })

// Handlebar template for listing files and directories.
const template = `
<html><body>
dirs
<ul>
{{#dirs}}
<li><a href="{{href}}">{{name}}</a></li>
{{/dirs}}
</ul>
list
<ul>
{{#files}}
<li><a href="{{href}}" target="_blank">{{name}}</a></li>
{{/files}}
</ul>
</body></html>
<html>
<body>
dirs
<ul>
{{#dirs}}
<li><a href="{{href}}">{{name}}</a></li>
{{/dirs}}
</ul>
list
<ul>
{{#files}}
<li><a href="{{href}}" target="_blank">{{name}}</a></li>
{{/files}}
</ul>
</body>
</html>
`
const handlebarTemplate = Handlebars.compile(template)

fastify
.register(require('..'), {
// An absolute path containing static files to serve.
root: path.join(__dirname, '/public'),
// Do not append a trailing slash to prefixes.
prefixAvoidTrailingSlash: true,
// Return a directory listing with a handlebar template.
list: {
// html or json response? html requires a render method.
format: 'html',
// A list of filenames that trigger a directory list response.
names: ['index', 'index.html', 'index.htm', '/'],
// You can provide your own render method as needed.
render: (dirs, files) => handlebarTemplate({ dirs, files })
}
})
Expand Down
49 changes: 49 additions & 0 deletions example/server-multi-dir-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use strict'

const path = require('path')
const Handlebars = require('handlebars')

const fastify = require('fastify')({ logger: { level: 'trace' } })

// Handlebar template for listing files and directories.
const template = `
<html>
<body>
dirs
<ul>
{{#dirs}}
<li><a href="{{href}}">{{name}}</a></li>
{{/dirs}}
</ul>
list
<ul>
{{#files}}
<li><a href="{{href}}" target="_blank">{{name}}</a></li>
{{/files}}
</ul>
</body>
</html>
`
const handlebarTemplate = Handlebars.compile(template)

fastify
.register(require('..'), {
// Array of absolute paths containing static files to serve
root: [path.join(__dirname, '/public'), path.join(__dirname, '/public2')],
// Do not append a trailing slash to prefixes
prefixAvoidTrailingSlash: true,
// Return a directory listing with a handlebar template
list: {
// html or json response? html requires a render method.
format: 'html',
// A list of filenames that trigger a directory list response.
names: ['index', 'index.html', 'index.htm', '/'],
// You can provide your own render method as needed.
render: (dirs, files) => handlebarTemplate({ dirs, files })
}
})
.listen(3000, err => {
if (err) throw err
})
5 changes: 4 additions & 1 deletion example/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ const path = require('path')
const fastify = require('fastify')({ logger: { level: 'trace' } })

fastify
.register(require('../'), { root: path.join(__dirname, '/public') })
.register(require('../'), {
// An absolute path containing static files to serve.
root: path.join(__dirname, '/public')
})
.listen(3000, err => {
if (err) throw err
})
2 changes: 1 addition & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ interface ListOptions {
}

export interface FastifyStaticOptions {
root: string;
root: string | string[];
prefix?: string;
prefixAvoidTrailingSlash?: boolean;
serve?: boolean;
Expand Down
87 changes: 60 additions & 27 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,23 @@ const { PassThrough } = require('readable-stream')
const glob = require('glob')
const send = require('send')
const fp = require('fastify-plugin')
const util = require('util')
const globPromise = util.promisify(glob)

const dirList = require('./lib/dirList')

function fastifyStatic (fastify, opts, next) {
const error = checkRootPathForErrors(fastify, opts.root)
if (error !== undefined) return next(error)
async function fastifyStatic (fastify, opts) {
checkRootPathForErrors(fastify, opts.root)

const setHeaders = opts.setHeaders

if (setHeaders !== undefined && typeof setHeaders !== 'function') {
return next(new TypeError('The `setHeaders` option must be a function'))
throw new TypeError('The `setHeaders` option must be a function')
}

const invalidDirListOpts = dirList.validateOptions(opts.list)
if (invalidDirListOpts) {
return next(invalidDirListOpts)
throw invalidDirListOpts
}

const sendOptions = {
Expand All @@ -38,11 +39,15 @@ function fastifyStatic (fastify, opts, next) {
maxAge: opts.maxAge
}

function pumpSendToReply (request, reply, pathname, rootPath) {
function pumpSendToReply (request, reply, pathname, rootPath, rootPathOffset = 0) {
const options = Object.assign({}, sendOptions)

if (rootPath) {
options.root = rootPath
if (Array.isArray(rootPath)) {
options.root = rootPath[rootPathOffset]
} else {
options.root = rootPath
}
}

const stream = send(request.raw, pathname, options)
Expand Down Expand Up @@ -108,6 +113,12 @@ function fastifyStatic (fastify, opts, next) {
if (opts.list && dirList.handle(pathname, opts.list)) {
return dirList.send({ reply, dir: dirList.path(opts.root, pathname), options: opts.list, route: pathname })
}

// root paths left to try?
if (Array.isArray(rootPath) && rootPathOffset < (rootPath.length - 1)) {
return pumpSendToReply(request, reply, pathname, rootPath, rootPathOffset + 1)
}

return reply.callNotFound()
}
reply.send(err)
Expand Down Expand Up @@ -152,7 +163,7 @@ function fastifyStatic (fastify, opts, next) {
if (opts.serve !== false) {
if (opts.wildcard === undefined || opts.wildcard === true) {
fastify.get(prefix + '*', routeOpts, function (req, reply) {
pumpSendToReply(req, reply, '/' + req.params['*'])
pumpSendToReply(req, reply, '/' + req.params['*'], sendOptions.root)
})
if (opts.redirect === true && prefix !== opts.prefix) {
fastify.get(opts.prefix, routeOpts, function (req, reply) {
Expand All @@ -163,57 +174,79 @@ function fastifyStatic (fastify, opts, next) {
}
} 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)
}

async function addGlobRoutes (rootPath) {
const files = await globPromise(path.join(rootPath, globPattern), { nodir: true })
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(/^\//, '')
file = file.replace(rootPath.replace(/\\/g, '/'), '').replace(/^\//, '')
const route = encodeURI(prefix + file).replace(/\/\//g, '/')
fastify.get(route, routeOpts, function (req, reply) {
pumpSendToReply(req, reply, '/' + file)
pumpSendToReply(req, reply, '/' + file, rootPath)
})

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, routeOpts, function (req, reply) {
pumpSendToReply(req, reply, file)
pumpSendToReply(req, reply, file, rootPath)
})

if (opts.redirect === true) {
fastify.get(pathname.replace(/\/$/, ''), routeOpts, function (req, reply) {
pumpSendToReply(req, reply, file.replace(/\/$/, ''))
pumpSendToReply(req, reply, file.replace(/\/$/, ''), rootPath)
})
}
})
next()
})
}

// return early to avoid calling next afterwards
return
if (Array.isArray(sendOptions.root)) {
await Promise.all(sendOptions.root.map(addGlobRoutes))
} else {
await addGlobRoutes(sendOptions.root)
}
}
}

next()
}

function checkRootPathForErrors (fastify, rootPath) {
if (rootPath === undefined) {
return new Error('"root" option is required')
throw new Error('"root" option is required')
}

if (Array.isArray(rootPath)) {
if (!rootPath.length) { throw new Error('"root" option array requires one or more paths') }

if ([...new Set(rootPath)].length !== rootPath.length) {
throw new Error('"root" option array contains one or more duplicate paths')
}

// check each path and fail at first invalid
rootPath.map(path => checkPath(fastify, path))
return
}

if (typeof rootPath === 'string') {
return checkPath(fastify, rootPath)
}

throw new Error('"root" option must be a string or array of strings')
}

function checkPath (fastify, rootPath) {
if (typeof rootPath !== 'string') {
return new Error('"root" option must be a string')
throw new Error('"root" option must be a string')
}
if (path.isAbsolute(rootPath) === false) {
return new Error('"root" option must be an absolute path')
throw new Error('"root" option must be an absolute path')
}

let pathStat
Expand All @@ -226,11 +259,11 @@ function checkRootPathForErrors (fastify, rootPath) {
return
}

return e
throw e
}

if (pathStat.isDirectory() === false) {
return new Error('"root" option must point to a directory')
throw new Error('"root" option must point to a directory')
}
}

Expand Down
Loading

0 comments on commit 38638bc

Please sign in to comment.