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 |