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
 |