fastify-static
Advanced tools
Comparing version 4.3.0 to 4.4.0
@@ -6,2 +6,3 @@ // Definitions by: Jannik <https://github.com/jannikkeye> | ||
import { FastifyPluginCallback, FastifyReply } from 'fastify'; | ||
import { Stats } from 'fs'; | ||
@@ -19,5 +20,16 @@ declare module "fastify" { | ||
interface ExtendedInformation { | ||
fileCount: number; | ||
totalFileCount: number; | ||
folderCount: number; | ||
totalFolderCount: number; | ||
totalSize: number; | ||
lastModified: number; | ||
} | ||
interface ListDir { | ||
href: string; | ||
name: string; | ||
stats: Stats; | ||
extendedInfo?: ExtendedInformation; | ||
} | ||
@@ -28,2 +40,3 @@ | ||
name: string; | ||
stats: Stats; | ||
} | ||
@@ -39,2 +52,4 @@ | ||
render: ListRender; | ||
extendedFolderInfo?: boolean; | ||
jsonFormat?: 'names' | 'extended'; | ||
} | ||
@@ -41,0 +56,0 @@ |
13
index.js
@@ -145,3 +145,3 @@ 'use strict' | ||
if (opts.list) { | ||
return dirList.send({ | ||
dirList.send({ | ||
reply, | ||
@@ -151,3 +151,4 @@ dir: path, | ||
route: pathname | ||
}) | ||
}).catch((err) => reply.send(err)) | ||
return | ||
} | ||
@@ -166,3 +167,9 @@ | ||
if (opts.list && dirList.handle(pathname, opts.list)) { | ||
return dirList.send({ reply, dir: dirList.path(opts.root, pathname), options: opts.list, route: pathname }) | ||
dirList.send({ | ||
reply, | ||
dir: dirList.path(opts.root, pathname), | ||
options: opts.list, | ||
route: pathname | ||
}).catch((err) => reply.send(err)) | ||
return | ||
} | ||
@@ -169,0 +176,0 @@ |
'use strict' | ||
const path = require('path') | ||
const fs = require('fs') | ||
const fs = require('fs').promises | ||
const pLimit = require('p-limit') | ||
@@ -13,32 +14,80 @@ const dirList = { | ||
*/ | ||
list: function (dir, callback) { | ||
list: async function (dir, options) { | ||
const entries = { dirs: [], files: [] } | ||
fs.readdir(dir, (err, files) => { | ||
if (err) { | ||
return callback(err) | ||
} | ||
if (files.length < 1) { | ||
callback(null, entries) | ||
const files = await fs.readdir(dir) | ||
if (files.length < 1) { | ||
return entries | ||
} | ||
const limit = pLimit(4) | ||
await Promise.all(files.map(filename => limit(async () => { | ||
let stats | ||
try { | ||
stats = await fs.stat(path.join(dir, filename)) | ||
} catch (error) { | ||
return | ||
} | ||
let j = 0 | ||
for (let i = 0; i < files.length; i++) { | ||
const filename = files[i] | ||
fs.stat(path.join(dir, filename), (err, file) => { | ||
if (!err) { | ||
if (file.isDirectory()) { | ||
entries.dirs.push(filename) | ||
} else { | ||
entries.files.push(filename) | ||
} | ||
const entry = { name: filename, stats } | ||
if (stats.isDirectory()) { | ||
if (options.extendedFolderInfo) { | ||
entry.extendedInfo = await getExtendedInfo(path.join(dir, filename)) | ||
} | ||
entries.dirs.push(entry) | ||
} else { | ||
entries.files.push(entry) | ||
} | ||
}))) | ||
async function getExtendedInfo (folderPath) { | ||
const depth = folderPath.split(path.sep).length | ||
let totalSize = 0 | ||
let fileCount = 0 | ||
let totalFileCount = 0 | ||
let folderCount = 0 | ||
let totalFolderCount = 0 | ||
let lastModified = 0 | ||
async function walk (dir) { | ||
const files = await fs.readdir(dir) | ||
const limit = pLimit(4) | ||
await Promise.all(files.map(filename => limit(async () => { | ||
const filePath = path.join(dir, filename) | ||
let stats | ||
try { | ||
stats = await fs.stat(filePath) | ||
} catch (error) { | ||
return | ||
} | ||
if (j++ >= files.length - 1) { | ||
entries.dirs.sort() | ||
entries.files.sort() | ||
callback(null, entries) | ||
if (stats.isDirectory()) { | ||
totalFolderCount++ | ||
if (filePath.split(path.sep).length === depth + 1) { | ||
folderCount++ | ||
} | ||
await walk(filePath) | ||
} else { | ||
totalSize += stats.size | ||
totalFileCount++ | ||
if (filePath.split(path.sep).length === depth + 1) { | ||
fileCount++ | ||
} | ||
lastModified = Math.max(lastModified, stats.mtimeMs) | ||
} | ||
}) | ||
}))) | ||
} | ||
}) | ||
await walk(folderPath) | ||
return { | ||
totalSize, | ||
fileCount, | ||
totalFileCount, | ||
folderCount, | ||
totalFolderCount, | ||
lastModified | ||
} | ||
} | ||
entries.dirs.sort((a, b) => a.name.localeCompare(b.name)) | ||
entries.files.sort((a, b) => a.name.localeCompare(b.name)) | ||
return entries | ||
}, | ||
@@ -53,19 +102,27 @@ | ||
*/ | ||
send: function ({ reply, dir, options, route }) { | ||
dirList.list(dir, (err, entries) => { | ||
if (err) { | ||
reply.callNotFound() | ||
return | ||
} | ||
send: async function ({ reply, dir, options, route }) { | ||
let entries | ||
try { | ||
entries = await dirList.list(dir, options) | ||
} catch (error) { | ||
return reply.callNotFound() | ||
} | ||
const format = reply.request.query.format || options.format | ||
if (format !== 'html') { | ||
if (options.jsonFormat !== 'extended') { | ||
const nameEntries = { dirs: [], files: [] } | ||
entries.dirs.forEach(entry => nameEntries.dirs.push(entry.name)) | ||
entries.files.forEach(entry => nameEntries.files.push(entry.name)) | ||
if (options.format !== 'html') { | ||
reply.send(nameEntries) | ||
} else { | ||
reply.send(entries) | ||
return | ||
} | ||
return | ||
} | ||
const html = options.render( | ||
entries.dirs.map(entry => dirList.htmlInfo(entry, route)), | ||
entries.files.map(entry => dirList.htmlInfo(entry, route))) | ||
reply.type('text/html').send(html) | ||
}) | ||
const html = options.render( | ||
entries.dirs.map(entry => dirList.htmlInfo(entry, route)), | ||
entries.files.map(entry => dirList.htmlInfo(entry, route))) | ||
reply.type('text/html').send(html) | ||
}, | ||
@@ -75,3 +132,3 @@ | ||
* provide the html information about entry and route, to get name and full route | ||
* @param {string} entry file or dir name | ||
* @param entry file or dir name and stats | ||
* @param {string} route request route | ||
@@ -81,3 +138,3 @@ * @return {ListFile} | ||
htmlInfo: function (entry, route) { | ||
return { href: path.join(path.dirname(route), entry).replace(/\\/g, '/'), name: entry } | ||
return { href: path.join(path.dirname(route), entry.name).replace(/\\/g, '/'), name: entry.name, stats: entry.stats, extendedInfo: entry.extendedInfo } | ||
}, | ||
@@ -84,0 +141,0 @@ |
{ | ||
"name": "fastify-static", | ||
"version": "4.3.0", | ||
"version": "4.4.0", | ||
"description": "Plugin for serving static files as fast as possible.", | ||
@@ -36,2 +36,3 @@ "main": "index.js", | ||
"glob": "^7.1.4", | ||
"p-limit": "^3.1.0", | ||
"readable-stream": "^3.4.0", | ||
@@ -38,0 +39,0 @@ "send": "^0.17.1" |
@@ -225,2 +225,10 @@ # fastify-static | ||
You can override the option with URL parameter `format`. Options are `html` and `json`. | ||
```bash | ||
GET .../public/assets?format=json | ||
``` | ||
will return the response as json independent of `list.format`. | ||
**Example:** | ||
@@ -304,2 +312,75 @@ | ||
#### `list.extendedFolderInfo` | ||
Default: `undefined` | ||
If `true` some extended information for folders will be accessible in `list.render` and in the json response. | ||
```js | ||
render(dirs, files) { | ||
const dir = dirs[0]; | ||
dir.fileCount // number of files in this folder | ||
dir.totalFileCount // number of files in this folder (recursive) | ||
dir.folderCount // number of folders in this folder | ||
dir.totalFolderCount // number of folders in this folder (recursive) | ||
dir.totalSize // size of all files in this folder (recursive) | ||
dir.lastModified // most recent last modified timestamp of all files in this folder (recursive) | ||
} | ||
``` | ||
Warning: This will slightly decrease the performance, especially for deeply nested file structures. | ||
#### `list.jsonFormat` | ||
Default: `names` | ||
Options: `names`, `extended` | ||
This option determines the output format when `json` is selected. | ||
`names`: | ||
```json | ||
{ | ||
"dirs": [ | ||
"dir1", | ||
"dir2" | ||
], | ||
"files": [ | ||
"file1.txt", | ||
"file2.txt" | ||
] | ||
} | ||
``` | ||
`extended`: | ||
```json | ||
{ | ||
"dirs": [ | ||
{ | ||
"name": "dir1", | ||
"stats": { | ||
"dev": 2100, | ||
"size": 4096, | ||
... | ||
}, | ||
"extendedInfo": { | ||
"fileCount": 4, | ||
"totalSize": 51233, | ||
... | ||
} | ||
} | ||
], | ||
"files": [ | ||
{ | ||
"name": "file1.txt", | ||
"stats": { | ||
"dev": 2200, | ||
"size": 554, | ||
... | ||
} | ||
} | ||
] | ||
} | ||
``` | ||
#### `preCompressed` | ||
@@ -306,0 +387,0 @@ |
@@ -15,4 +15,7 @@ 'use strict' | ||
arrange: function (t, options, f) { | ||
return helper.arrangeModule(t, options, fastifyStatic, f) | ||
}, | ||
arrangeModule: function (t, options, mock, f) { | ||
const fastify = Fastify() | ||
fastify.register(fastifyStatic, options) | ||
fastify.register(mock, options) | ||
t.teardown(fastify.close.bind(fastify)) | ||
@@ -241,2 +244,82 @@ fastify.listen(0, err => { | ||
t.test('dir list html format - stats', t => { | ||
t.plan(7) | ||
const options1 = { | ||
root: path.join(__dirname, '/static'), | ||
prefix: '/public', | ||
index: false, | ||
list: { | ||
format: 'html', | ||
render (dirs, files) { | ||
t.ok(dirs.length > 0) | ||
t.ok(files.length > 0) | ||
t.ok(dirs.every(every)) | ||
t.ok(files.every(every)) | ||
function every (value) { | ||
return value.stats && | ||
value.stats.atime && | ||
!value.extendedInfo | ||
} | ||
} | ||
} | ||
} | ||
const route = '/public/' | ||
helper.arrange(t, options1, (url) => { | ||
simple.concat({ | ||
method: 'GET', | ||
url: url + route | ||
}, (err, response, body) => { | ||
t.error(err) | ||
t.equal(response.statusCode, 200) | ||
}) | ||
}) | ||
}) | ||
t.test('dir list html format - extended info', t => { | ||
t.plan(4) | ||
const route = '/public/' | ||
const options = { | ||
root: path.join(__dirname, '/static'), | ||
prefix: '/public', | ||
index: false, | ||
list: { | ||
format: 'html', | ||
extendedFolderInfo: true, | ||
render (dirs, files) { | ||
t.test('dirs', t => { | ||
t.plan(dirs.length * 7) | ||
for (const value of dirs) { | ||
t.ok(value.extendedInfo) | ||
t.equal(typeof value.extendedInfo.fileCount, 'number') | ||
t.equal(typeof value.extendedInfo.totalFileCount, 'number') | ||
t.equal(typeof value.extendedInfo.folderCount, 'number') | ||
t.equal(typeof value.extendedInfo.totalFolderCount, 'number') | ||
t.equal(typeof value.extendedInfo.totalSize, 'number') | ||
t.equal(typeof value.extendedInfo.lastModified, 'number') | ||
} | ||
}) | ||
} | ||
} | ||
} | ||
helper.arrange(t, options, (url) => { | ||
simple.concat({ | ||
method: 'GET', | ||
url: url + route | ||
}, (err, response, body) => { | ||
t.error(err) | ||
t.equal(response.statusCode, 200) | ||
}) | ||
}) | ||
}) | ||
t.test('dir list json format', t => { | ||
@@ -275,2 +358,88 @@ t.plan(2) | ||
t.test('dir list json format - extended info', t => { | ||
t.plan(2) | ||
const options = { | ||
root: path.join(__dirname, '/static'), | ||
prefix: '/public', | ||
prefixAvoidTrailingSlash: true, | ||
list: { | ||
format: 'json', | ||
names: ['index', 'index.json', '/'], | ||
extendedFolderInfo: true, | ||
jsonFormat: 'extended' | ||
} | ||
} | ||
const routes = ['/public/shallow/'] | ||
helper.arrange(t, options, (url) => { | ||
for (const route of routes) { | ||
t.test(route, t => { | ||
t.plan(5) | ||
simple.concat({ | ||
method: 'GET', | ||
url: url + route | ||
}, (err, response, body) => { | ||
t.error(err) | ||
t.equal(response.statusCode, 200) | ||
const bodyObject = JSON.parse(body.toString()) | ||
t.equal(bodyObject.dirs[0].name, 'empty') | ||
t.equal(typeof bodyObject.dirs[0].stats.atime, 'string') | ||
t.equal(typeof bodyObject.dirs[0].extendedInfo.totalSize, 'number') | ||
}) | ||
}) | ||
} | ||
}) | ||
}) | ||
t.test('dir list - url parameter format', t => { | ||
t.plan(13) | ||
const options = { | ||
root: path.join(__dirname, '/static'), | ||
prefix: '/public', | ||
index: false, | ||
list: { | ||
format: 'html', | ||
render (dirs, files) { | ||
return 'html' | ||
} | ||
} | ||
} | ||
const route = '/public/' | ||
helper.arrange(t, options, (url) => { | ||
simple.concat({ | ||
method: 'GET', | ||
url: url + route | ||
}, (err, response, body) => { | ||
t.error(err) | ||
t.equal(response.statusCode, 200) | ||
t.equal(body.toString(), 'html') | ||
t.ok(response.headers['content-type'].includes('text/html')) | ||
}) | ||
simple.concat({ | ||
method: 'GET', | ||
url: url + route + '?format=html' | ||
}, (err, response, body) => { | ||
t.error(err) | ||
t.equal(response.statusCode, 200) | ||
t.equal(body.toString(), 'html') | ||
t.ok(response.headers['content-type'].includes('text/html')) | ||
}) | ||
simple.concat({ | ||
method: 'GET', | ||
url: url + route + '?format=json' | ||
}, (err, response, body) => { | ||
t.error(err) | ||
t.equal(response.statusCode, 200) | ||
t.ok(body.toString()) | ||
t.ok(response.headers['content-type'].includes('application/json')) | ||
}) | ||
}) | ||
}) | ||
t.test('dir list on empty dir', t => { | ||
@@ -393,1 +562,40 @@ t.plan(2) | ||
}) | ||
t.test('dir list error', t => { | ||
t.plan(7) | ||
const options = { | ||
root: path.join(__dirname, '/static'), | ||
prefix: '/public', | ||
prefixAvoidTrailingSlash: true, | ||
index: false, | ||
list: { | ||
format: 'html', | ||
names: ['index', 'index.htm'], | ||
render: () => '' | ||
} | ||
} | ||
const errorMessage = 'mocking send' | ||
const dirList = require('../lib/dirList') | ||
dirList.send = async () => { throw new Error(errorMessage) } | ||
const mock = t.mock('..', { | ||
'../lib/dirList.js': dirList | ||
}) | ||
const routes = ['/public/', '/public/index.htm'] | ||
helper.arrangeModule(t, options, mock, (url) => { | ||
for (const route of routes) { | ||
simple.concat({ | ||
method: 'GET', | ||
url: url + route | ||
}, (err, response, body) => { | ||
t.error(err) | ||
t.equal(JSON.parse(body.toString()).message, errorMessage) | ||
t.equal(response.statusCode, 500) | ||
}) | ||
} | ||
}) | ||
}) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
346324
4232
439
7
+ Addedp-limit@^3.1.0
+ Addedp-limit@3.1.0(transitive)
+ Addedyocto-queue@0.1.0(transitive)