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
			| 
								 
											3 years ago
										 
									 | 
							
								'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
							 |