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.

195 lines
5.5 KiB

'use strict'
const path = require('path')
const fs = require('fs').promises
const pLimit = require('p-limit')
const dirList = {
/**
* get files and dirs from dir, or error
* @param {string} dir full path fs dir
* @param {function(error, entries)} callback
* note: can't use glob because don't get error on non existing dir
*/
list: async function (dir, options) {
const entries = { dirs: [], files: [] }
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
}
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 (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
},
/**
* send dir list content, or 404 on error
* @param {Fastify.Reply} reply
* @param {string} dir full path fs dir
* @param {ListOptions} options
* @param {string} route request route
*/
send: async function ({ reply, dir, options, route, prefix }) {
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))
reply.send(nameEntries)
} else {
reply.send(entries)
}
return
}
const html = options.render(
entries.dirs.map(entry => dirList.htmlInfo(entry, route, prefix, options)),
entries.files.map(entry => dirList.htmlInfo(entry, route, prefix, options)))
reply.type('text/html').send(html)
},
/**
* provide the html information about entry and route, to get name and full route
* @param entry file or dir name and stats
* @param {string} route request route
* @return {ListFile}
*/
htmlInfo: function (entry, route, prefix, options) {
if (options.names && options.names.includes(path.basename(route))) {
route = path.normalize(path.join(route, '..'))
}
return {
href: path.join(prefix, route, entry.name).replace(/\\/g, '/'),
name: entry.name,
stats: entry.stats,
extendedInfo: entry.extendedInfo
}
},
/**
* say if the route can be handled by dir list or not
* @param {string} route request route
* @param {ListOptions} options
* @return {boolean}
*/
handle: function (route, options) {
if (!options.names) {
return false
}
return options.names.includes(path.basename(route)) ||
// match trailing slash
(options.names.includes('/') && route[route.length - 1] === '/')
},
/**
* get path from route and fs root paths, considering trailing slash
* @param {string} root fs root path
* @param {string} route request route
*/
path: function (root, route) {
const _route = route[route.length - 1] === '/'
? route + 'none'
: route
return path.dirname(path.join(root, _route))
},
/**
* validate options
* @return {Error}
*/
validateOptions: function (options) {
if (!options) {
return
}
if (options.format && options.format !== 'json' && options.format !== 'html') {
return new TypeError('The `list.format` option must be json or html')
}
if (options.names && !Array.isArray(options.names)) {
return new TypeError('The `list.names` option must be an array')
}
if (options.format === 'html' && typeof options.render !== 'function') {
return new TypeError('The `list.render` option must be a function and is required with html format')
}
}
}
module.exports = dirList