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
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
|