UNPKG

express-autoindex

Version:

express-autoindex produce a directory listing like Nginx, Apache or another, but just with express

15 lines (14 loc) 11.8 kB
/** * @license * express-autoindex * Copyright (c) 2023-present, c-bertran (Clément Bertrand) (https://github.com/c-bertran) * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import e from"chardet";import{readFileSync as t,accessSync as s,constants as r,statSync as i,createReadStream as a}from"fs";import{stat as o,readdir as n}from"fs/promises";import{STATUS_CODES as h}from"http";import l from"mime";import{platform as d}from"os";import{resolve as m,posix as c,win32 as p}from"path";class g{isProduction;errorCode;htmlPage;rowTemplateBase;rowTemplateWithDate;rowTemplateWithSize;rowTemplateWithBoth;month;savePage;savePageDeadline;dateFormat;dateFormatCache;dateFormatterIntl;dateRegexParse;dateRegexParseCompiled;htmlDateCache=new Map;escapeHtmlMap={"&":"&#38;","\n":"&#10;","<":"&#60;",">":"&#62;","'":"&#39;",'"':"&#34;"};options;jsonOption;path;root;constructor(e,a,o){if(this.isProduction=void 0!==process.env.NODE_ENV&&"production"===process.env.NODE_ENV,this.errorCode=(()=>{const e=new Map([["EBADF",{message:"fd is not a valid open file descriptor"}],["EFAULT",{message:"Bad address"}],["EINVAL",{message:"Invalid flag specified in flag"}],["ELOOP",{message:"Too many symbolic links encountered while traversing the path"}],["ENOMEM",{message:"Out of memory"}],["EOVERFLOW",{message:"pathname or fd refers to a file whose size, inode number, or number of blocks cannot be represented in, respectively, the types off_t, ino_t, or blkcnt_t.\nThis error can occur when, for example, an application compiled on a 32-bit platform without -D_FILE_OFFSET_BITS=64 calls stat() on a file whose size exceeds (1<<31)-1 bytes."}]]),t=new Map([["EACCES",{message:"Permission denied"}],["EADDRINUSE",{message:"Address already in use"}],["ECONNREFUSED",{message:"Connection refused"}],["ECONNRESET",{message:"Connection reset by peer"}],["EEXIST",{message:"File exists"}],["EISDIR",{message:"Is a directory"}],["EMFILE",{message:"Too many open files in system"}],["ENAMETOOLONG",{message:h[414],httpCode:414}],["ENOENT",{message:"No such file or directory",httpCode:404}],["ENOTDIR",{message:"Not a directory",httpCode:404}],["ENOTEMPTY",{message:"Directory not empty"}],["ENOTFOUND",{message:"DNS lookup failed"}],["EPERM",{message:"Operation not permitted",httpCode:403}],["EPIPE",{message:"Broken pipe"}],["ETIMEDOUT",{message:h[408],httpCode:408}]]),s=new Map([...e,...t]);return s.forEach(((e,t)=>{e.httpCode||s.set(t,{message:e.message,httpCode:500})})),s})(),this.htmlPage='<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>{{title}}</title></head><body><h1>{{title}}</h1><hr/><table>{{content}}</table><hr/></body><style type="text/css">html{font-family:Arial,Helvetica,sans-serif}table{font-family:\'Courier New\',Courier,monospace;font-size:12px;font-weight:400;letter-spacing:normal;line-height:normal;font-style:normal}tr td:first-child{min-width:20%}td a{margin-right:1em}td.size{text-align:end}</style></html>',this.rowTemplateBase='<tr><td class="link"><a href="${href}">${name}</a></td></tr>',this.rowTemplateWithDate='<tr><td class="link"><a href="${href}">${name}</a></td><td class="time">${time}</td></tr>',this.rowTemplateWithSize='<tr><td class="link"><a href="${href}">${name}</a></td><td class="size">${size}</td></tr>',this.rowTemplateWithBoth='<tr><td class="link"><a href="${href}">${name}</a></td><td class="time">${time}</td><td class="size">${size}</td></tr>',this.month=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],this.savePage=new Map,this.savePageDeadline=3e5,this.dateFormatCache=new Map,this.dateFormatterIntl=new Intl.DateTimeFormat("en-US",{calendar:"iso8601",timeZone:"UTC",weekday:"short"}),this.dateFormat=new Map([["%wd",e=>this.dateFormatterIntl.format(e)],["%d",e=>e.day],["%mo",e=>this.month[Number(e.month)-1]],["%y",e=>e.year],["%h",e=>e.hours],["%mi",e=>e.minutes],["%s",e=>e.seconds],["%ms",e=>e.milliseconds]]),this.dateRegexParse="^(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})T(?<hours>\\d{2}):(?<minutes>\\d{2}):(?<seconds>\\d{2})\\.(?<milliseconds>\\d{3})Z$",this.dateRegexParseCompiled=new RegExp(this.dateRegexParse,"gm"),this.options={alwaysThrowError:o?.alwaysThrowError??!1,cache:o?.cache,dateFormat:o?.dateFormat??"%d?-%mo-%y %h:%mi",dirAtTop:o?.dirAtTop??!0,displayDate:o?.displayDate??!0,displayDotfile:o?.displayDotfile??!1,displaySize:o?.displaySize??!0,exclude:o?.exclude,json:o?.json??!1,strict:o?.strict??!0},this.jsonOption=o?.customJsonFormat,this.path=a.length?a:void 0,this.root=e,o?.customTemplate){const e=m(".",o.customTemplate);try{this.htmlPage=t(e,{encoding:"utf-8",flag:"r"})}catch{throw new Error(`customTemplate path is incorrect: ${e}`)}}if(this.options?.cache&&"boolean"!=typeof this.options.cache&&(this.savePageDeadline=this.options.cache),!e)throw new TypeError("root is required");s(e,r.R_OK);if(!i(e).isDirectory())throw new Error("root is not a directory");if(this.path&&"/"!==this.path.charAt(0))throw new Error(`path '${this.path}' not start with /`)}formatError(e,t){let s=t??"";return this.isProduction||(t&&(s+="\n→ "),s+=`(${e.message})`),s}throwError(e){return!!this.options.alwaysThrowError||e>=500}LRUCache(e,t,s,r){if(e.has(t))e.delete(t);else if(e.size>=r){const t=e.keys().next().value;t&&e.delete(t)}e.set(t,s)}error(e,t){if("number"==typeof e)return h[e]&&t.status(e),this.throwError(e)?new Error(h[e]??`System error code ${e} not recognized`):void 0;const s=this.errorCode.get(e.code??"__DEFAULT__");return s?(t.status(s.httpCode),this.throwError(s.httpCode)?new Error(this.formatError(e,s.message)):void 0):new Error(this.formatError(e,`System error code ${e} not recognized`))}parsePath(e){return e.includes("//")||e.includes("/./")||e.includes("/../")?("/"===(e=c.normalize(e.normalize())).charAt(e.length-1)&&e.length>1&&(e=e.slice(0,e.length-1)),e):"/"===e.charAt(e.length-1)&&e.length>1?e.slice(0,e.length-1):e}serve(e,t,s){const r=e=>"/"===e.at(0)?e.slice(1):e,i=this.path?r(e).replace(r(this.path),""):r(e),a=i.split("/").filter((e=>e.length)),n={path:e,savePath:i,serverPath:decodeURI("win32"===d()?p.normalize(p.resolve(this.root,...a)):c.normalize(c.resolve(this.root,...a))),title:r(i).length?`/${decodeURI(r(i))}/`:"/"};o(n.serverPath).then((e=>{if(e.isFile())this.file(n,e,t,s);else{if(!e.isDirectory()){const e=new Error(`ENOENT: no such file or directory, stat '${n.serverPath}'`);throw e.code="ENOENT",e.syscall="stat",e.errno=-4058,e}this.directory(n,t,s)}})).catch((e=>s(this.error(e,t))))}send(e,t){if(t.status(200),"string"==typeof e)return t.setHeader("Content-Length",Buffer.byteLength(e)),t.send(e);t.setHeader("Content-Length",Buffer.byteLength(JSON.stringify(e))),t.json(e)}dateToHTMLDate(e){const t=e.getTime().toString(),s=this.htmlDateCache.get(t);if(s)return s;this.dateRegexParseCompiled.lastIndex=0;const r=this.dateRegexParseCompiled.exec(e.toISOString());let i;if(r&&r.groups){const t={day:this.dateFormatterIntl.format(e),dayNumber:r.groups.day,month:this.month[Number(r.groups.month)-1],year:r.groups.year,hours:r.groups.hours,minutes:r.groups.minutes,seconds:r.groups.seconds};i=`${t.day}, ${t.dayNumber} ${t.month} ${t.year} ${t.hours}:${t.minutes}:${t.seconds} GMT`}return i&&this.htmlDateCache.size<500&&this.htmlDateCache.set(t,i),i}file(t,s,r,i){const o=l.getType(t.serverPath)??"application/octet-stream",n=this.dateToHTMLDate(s.mtime);e.detectFile(t.serverPath,{sampleSize:256}).then((e=>{r.setHeader("Content-Length",s.size),r.setHeader("Content-Type",`${o}; charset=${e??"UTF-8"}`),n&&r.setHeader("Last-Modified",n),r.writeHead(200),a(t.serverPath).pipe(r)})).catch((e=>i(this.error(e,r))))}checkSavePage(e){const t=(new Date).getTime(),s=this.savePage.get(e);if(s&&s.deadline.getTime()>=t)return s;s&&this.savePage.delete(e)}genTime(e){const t=e.mtime.getTime().toString(),s=this.dateFormatCache.get(t);if(s)return this.LRUCache(this.dateFormatCache,t,s,1e3),s;this.dateRegexParseCompiled.lastIndex=0;const r=this.dateRegexParseCompiled.exec(e.mtime.toISOString());let i=this.options.dateFormat,a=0;if(r&&r.groups)for(const e of this.dateFormat)for(;(a=i.indexOf(e[0]))>-1;){const t=e[1](r.groups),s=a+e[0].length;!t||t.length<=0?("?"===i.charAt(s)&&(i=i.slice(0,s)+i.slice(s+2)),i=i.replace(e[0],"")):("?"===i.charAt(s)&&(i=i.slice(0,s)+i.slice(s+1)),i=i.replace(e[0],t))}return this.LRUCache(this.dateFormatCache,t,i,1e3),i}escapeHtml(e){return e.replace(/[\x26\x0A<>'"]/g,(e=>this.escapeHtmlMap[e]||`&#${e.charCodeAt(0)};`))}generateRow(e){const t=e.el.dirent[0],s=e.el.dirent[1];return this.options.displayDate&&this.options.displaySize?this.rowTemplateWithBoth.replace("${href}",t).replace("${name}",s).replace("${time}",this.escapeHtml(e.el.time)).replace("${size}",e.el.size):this.options.displayDate?this.rowTemplateWithDate.replace("${href}",t).replace("${name}",s).replace("${time}",this.escapeHtml(e.el.time)):this.options.displaySize?this.rowTemplateWithSize.replace("${href}",t).replace("${name}",s).replace("${size}",e.el.size):this.rowTemplateBase.replace("${href}",t).replace("${name}",s)}generateJson(e){const t=e.dirent.isDirectory();if(!this.jsonOption)return{isDir:t,name:e.el.dirent[1],path:e.el.dirent[0],time:e.el.time,size:Number(e.el.size)};const s={},r=this.jsonOption;return"isDir"in r&&(s[r.isDir]=t),"name"in r&&(s[r.name]=e.el.dirent[1]),"path"in r&&(s[r.path]=e.el.dirent[0]),"time"in r&&(s[r.time]=e.el.time),"size"in r&&(s[r.size]=Number(e.el.size)),s}directory(e,t,s){const r=this.checkSavePage(e.savePath),i=[],a=[],h=[],l=[];let d;if(void 0!==this.options.cache&&r)return this.send(r.data,t);n(e.serverPath,{encoding:"utf-8",withFileTypes:!0}).then((async s=>{const r=s.filter((e=>(e.isDirectory()||e.isFile())&&(this.options.displayDotfile||"."!==e.name.charAt(0))&&(!this.options.exclude||!this.options.exclude.test(e.name)))),n=await Promise.all(r.map((async t=>({dirent:t,stat:await o(m(e.serverPath,t.name))}))));for(const{dirent:t,stat:s}of n)i.push({dirent:t,el:{dirent:[`${this.path?this.parsePath(`${this.path}${e.title}${t.name}`):this.parsePath(this.path)}`,`${t.name}${t.isDirectory()?"/":""}`],time:this.genTime(s),size:t.isFile()?String(s.size):"-"}});if(this.options.json)d=i.map((e=>this.generateJson(e)));else{0!==e.title.localeCompare("/")&&l.push(`<tr><td><a href="${e.path.replace(/[^/]+$/,"")}">../</a></td></tr>`);for(const e of i)this.options.dirAtTop?e.dirent.isDirectory()?h.push(this.generateRow(e)):e.dirent.isFile()&&a.push(this.generateRow(e)):l.push(this.generateRow(e));if(this.options.dirAtTop){for(const e of h)l.push(e);for(const e of a)l.push(e)}d=this.htmlPage.replaceAll(/{{\s?title\s?}}/g,`Index of ${e.title}`).replaceAll(/{{\s?content\s?}}/g,l.join(""))}return void 0!==this.options.cache&&this.savePage.set(e.savePath,{json:this.options.json??!1,data:d,deadline:new Date((new Date).getTime()+this.savePageDeadline),path:e.savePath}),this.send(d,t)})).catch((e=>s(this.error(e,t))))}}var f=(e,t=void 0)=>{const s=new g(e,"",t);return(e,t,r)=>{if(!e.baseUrl||s.path&&s.path===e.baseUrl||(s.path=e.baseUrl),s.options.strict&&"GET"!==e.method&&"HEAD"!==e.method)return t.status(405),t.setHeader("Allow","GET, HEAD"),t.setHeader("Content-Length","0"),void t.end();const i=s.path?s.parsePath(`${s.path}/${e.path}`):s.parsePath(e.path);s.path&&!i.length?r(s.error(400,t)):s.serve(i,t,r)}};export{f as default};