1 | const fs = require('fs');
|
2 | const { join, resolve } = require('path');
|
3 | const tglob = require('tiny-glob/sync');
|
4 | const parser = require('@polka/url');
|
5 | const mime = require('mime/lite');
|
6 |
|
7 | const FILES = {};
|
8 | const noop = () => {};
|
9 |
|
10 | function toAssume(uri, extns) {
|
11 | let i=0, x, len=uri.length - 1;
|
12 | if (uri.charCodeAt(len) === 47) uri=uri.substring(0, len);
|
13 |
|
14 | let arr=[], tmp=`${uri}/index`;
|
15 | for (; i < extns.length; i++) {
|
16 | x = '.' + extns[i];
|
17 | if (uri) arr.push(uri + x);
|
18 | arr.push(tmp + x);
|
19 | }
|
20 |
|
21 | return arr;
|
22 | }
|
23 |
|
24 | function find(uri, extns) {
|
25 | if (!!~uri.lastIndexOf('.')) return FILES[uri];
|
26 | let i=0, data, arr=toAssume(uri, extns);
|
27 | for (; i < arr.length; i++) {
|
28 | if (data=FILES[arr[i]]) break;
|
29 | }
|
30 | return data;
|
31 | }
|
32 |
|
33 | function toEtag(obj) {
|
34 | return `W/"${obj.size.toString(16)}-${obj.mtime.getTime().toString(16)}"`;
|
35 | }
|
36 |
|
37 | function is404(res) {
|
38 | return (res.statusCode=404,res.end());
|
39 | }
|
40 |
|
41 | module.exports = function (dir, opts={}) {
|
42 | dir = resolve(dir || '.');
|
43 |
|
44 | let isNotFound = opts.onNoMatch || is404;
|
45 | let extensions = opts.extensions || ['html', 'htm'];
|
46 |
|
47 | if (opts.dev) {
|
48 | return function (req, res, next) {
|
49 | let uri = decodeURIComponent(req.path || req.pathname || parser(req).pathname);
|
50 | let arr = uri.includes('.') ? [uri] : toAssume(uri, extensions);
|
51 | let file = arr.map(x => join(dir, x)).find(fs.existsSync);
|
52 | if (!file) return next ? next() : isNotFound(res);
|
53 | res.setHeader('Content-Type', mime.getType(file));
|
54 | fs.createReadStream(file).pipe(res);
|
55 | }
|
56 | }
|
57 |
|
58 | let setHeaders = opts.setHeaders || noop;
|
59 | let cc = opts.maxAge != null && `public,max-age=${opts.maxAge}`;
|
60 | if (cc && opts.immutable) cc += ',immutable';
|
61 |
|
62 | opts.cwd = dir;
|
63 | let abs, stats, headers;
|
64 | opts.dot = !!opts.dotfiles;
|
65 | tglob('**/*.*', opts).forEach(str => {
|
66 | abs = join(dir, str);
|
67 | stats = fs.statSync(abs);
|
68 | headers = {
|
69 | 'Content-Length': stats.size,
|
70 | 'Content-Type': mime.getType(str),
|
71 | 'Last-Modified': stats.mtime.toUTCString()
|
72 | };
|
73 | if (cc) headers['Cache-Control'] = cc;
|
74 | if (opts.etag) headers['ETag'] = toEtag(stats);
|
75 | FILES['/' + str.replace(/\\+/g, '/')] = { abs, stats, headers };
|
76 | });
|
77 |
|
78 | return function (req, res, next) {
|
79 | let pathname = decodeURIComponent(req.path || req.pathname || parser(req).pathname);
|
80 | let data = find(pathname, extensions);
|
81 | if (!data) return next ? next() : isNotFound(res);
|
82 |
|
83 | setHeaders(res, pathname, data.stats);
|
84 | res.writeHead(200, data.headers);
|
85 |
|
86 | fs.createReadStream(data.abs).pipe(res);
|
87 | }
|
88 | }
|