hfs 0.38.2 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -2
- package/admin/assets/index-d654b69c.js +510 -0
- package/{frontend/assets/sha512-57b80c96.js → admin/assets/sha512-e2cf8296.js} +1 -1
- package/admin/index.html +1 -1
- package/frontend/assets/index-31f0430a.js +94 -0
- package/frontend/assets/index-ffbcd4c9.css +1 -0
- package/{admin/assets/sha512-7ecaaeed.js → frontend/assets/sha512-5acbfc40.js} +1 -1
- package/frontend/fontello.css +10 -12
- package/frontend/fontello.woff2 +0 -0
- package/frontend/index.html +2 -2
- package/package.json +1 -1
- package/plugins/vhosting/plugin.js +1 -1
- package/src/api.auth.js +1 -1
- package/src/api.file_list.js +3 -1
- package/src/api.lang.js +8 -4
- package/src/api.monitor.js +9 -4
- package/src/api.vfs.js +7 -2
- package/src/apiMiddleware.js +4 -2
- package/src/const.js +1 -1
- package/src/index.js +1 -0
- package/src/log.js +39 -35
- package/src/middlewares.js +7 -6
- package/src/misc.js +6 -2
- package/src/plugins.js +17 -8
- package/src/serveFile.js +47 -49
- package/src/serveGuiFiles.js +14 -12
- package/src/throttler.js +2 -0
- package/src/vfs.js +8 -7
- package/admin/assets/index-c7c100fa.js +0 -509
- package/frontend/assets/index-4b763f31.js +0 -94
- package/frontend/assets/index-6d4e81f7.css +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@charset "UTF-8";:root{height:100dvh;--bg: #fff;--text: #555;--ghost-contrast: #8882;--faint-contrast: #8884;--mild-contrast: #8886;--good-contrast: #000a;--button-bg: #68a;--button-text: #eaeaea;--focus-color: #468;--separator: " \2013 "}:root .theme-dark{--bg: #000;--text: #999;--good-contrast: #fffa;--button-bg: #345;--button-text: #999;color-scheme:dark}:root .theme-dark a{color:#8ac}:root .theme-dark .dialog-closer{background:#633}:root .theme-dark .dialog-icon{color:#ccc}:root .theme-dark .dialog-icon .icon{color:#aaa;margin-left:-1px;font-size:95%}:root .theme-dark .dialog-backdrop{background:rgba(51,51,51,.7333333333)}:root .theme-dark .error-msg{color:#b88;background-color:#623}:root .theme-dark button.toggled{color:#eee}body{background:var(--bg);margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body,button,select,input{font-size:12pt}#root{max-width:50em;margin:auto;min-height:100vh;display:flex;flex-direction:column}body,input{color:var(--text)}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}input:not([type=checkbox]),select{padding:.3em .4em;border-radius:.5em;background:var(--bg);border-color:var(--mild-contrast);color:var(--good-contrast);max-width:100%;box-sizing:border-box;width:100%}input[type=checkbox]{transform:scale(1.7);accent-color:var(--button-bg)}label input[type=checkbox]{margin-right:.8em}select{text-align:center}.hidden{display:none!important}.icon{font-size:1.2em}.icon.mirror:before{transform:scaleX(-1)}a{text-decoration:none;color:#57a}button{background-color:var(--button-bg);color:var(--button-text);padding:.5em 1em;border:transparent;text-decoration:none;border-radius:.3em;vertical-align:middle;cursor:pointer}button.toggled{color:#fff;text-shadow:0 0 3px #fff}button:focus-visible,.breadcrumb:focus-visible{outline:3px solid var(--focus-color)}a>button{width:100%}input:focus-visible,select:focus-visible,ul a:focus-visible{border-radius:.3em;border-color:transparent;outline:2px solid var(--focus-color)}.error-msg{background-color:#faa;color:#833;padding:.5em 1em}header{position:sticky;top:0;background:var(--bg);padding:.2em;z-index:1}.before-sliding{width:0!important;flex:0!important;margin:0!important;height:0!important;padding:0!important;overflow:hidden!important;transition:all .5s}.show-sliding{transition:all .5s;overflow:clip;flex:1;white-space:nowrap}.ani-working{animation:1s blink infinite}@keyframes blink{0%{opacity:1}50%{opacity:.2}}@keyframes spin{to{transform:rotate(360deg)}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}.spinner,.icon.spinner:before{animation:1.5s spin infinite linear}.icon.emoji.spinner{display:inline-block}.breadcrumb{padding:.1em .6em .2em;line-height:1.8em;border-radius:.7em;background-color:var(--button-bg);color:var(--button-text);border-top:1px solid #666;margin-right:-.1em}.breadcrumb:nth-child(-n+3) .icon{padding:0 .2em}#folder-stats,#filter-bar>span{font-size:90%}#folder-stats{margin:.4em 0 0 .5em;float:right}#folder-stats .icon{margin-right:.3em}#filter{flex:1;box-sizing:border-box}#filter-bar{display:flex;align-items:center;gap:.8em;margin:.2em 0 0;padding:2px 0 1px 11px;height:1.8em}#filter-bar input[type=checkbox]{margin-top:.5em}#filter-bar span:empty{display:none}ul.dir{flex:1;padding:0;margin:0;clear:both}ul.dir>p{text-align:center}ul.dir li{display:block;list-style-type:none;padding:.3em .3em .4em;border-bottom:1px solid var(--faint-contrast)}ul.dir li:nth-of-type(odd){background-color:var(--ghost-contrast)}ul.dir li input[type=checkbox]{margin:0 .8em}ul.dir li a:last-of-type{word-break:break-word;padding-right:.3em}ul.dir li a .icon{margin-right:.3em}ul.dir li a:hover{text-decoration:underline}ul.dir li .entry-props{float:right;font-size:90%;margin-left:4px;margin-top:.3em;font-variant-numeric:tabular-nums}ul.dir li .entry-props .entry-size-unit{margin-left:.3em}ul.dir li>div:last-of-type{clear:both}ul.dir li.page-separator{margin-top:1em;position:relative}ul.dir li.page-separator:before{content:attr(label);position:absolute;top:-1.8em;font-size:smaller;margin-left:calc(50% - 1em);opacity:.9}#menu-bar{display:flex;justify-content:space-evenly;flex-wrap:wrap}#menu-bar>*{flex:1;margin:.1em}#menu-bar button{padding-left:0;padding-right:0}#searched{margin:.2em}#user-panel{display:flex;flex-direction:column;gap:1em}#user-panel a>button{width:100%}button label{cursor:inherit;margin-left:.5em}.dialog-backdrop.working{font-size:5em;animation:1s fade-in}.dialog-content{padding:.2em}.dialog-alert .dialog-content{text-align:center}.dialog-alert .dialog-content p{text-align:left;display:inline-block}.dialog{min-width:11em;--color: var(--button-bg)}#paging{position:sticky;bottom:0;display:flex;gap:.1em;background:var(--bg);padding:0 .2em .2em}#paging>button{z-index:1}#paging button{box-shadow:0 0 .3em .3em #0003}#paging #paging-middle{padding:0 .5em;margin:0 -.3em;display:flex;gap:.5em;flex:1;overflow-x:auto}#paging #paging-middle>button{flex:1;padding-top:0;padding-bottom:0}#paging button{background:var(--button-bg);text-align:center;white-space:nowrap;padding:.5em}.upload-progress:before{content:var(--separator)}.entry-size:after{content:var(--separator)}.upload-progress{min-width:4em;display:inline-block;margin-left:.5em}.upload-list td:nth-child(1){width:0}.upload-list td:nth-child(2){text-align:right;width:0;white-space:nowrap;padding-left:.5em}.upload-list td:nth-child(3){padding:.2em .5em;word-break:break-word}.dialog-login form{display:flex;flex-direction:column;gap:1.2em}.dialog-login label{display:block;margin-bottom:.5em;margin-left:.1em}@media (min-width: 42em){body{scrollbar-width:thin;scrollbar-color:var(--button-bg) var(--ghost-contrast)}body::-webkit-scrollbar{width:12px}body::-webkit-scrollbar-track{background:var(--ghost-contrast)}body::-webkit-scrollbar-thumb{background-color:var(--button-bg);border-radius:20px;border:1px solid var(--ghost-contrast)}}@media (max-width: 42em){:root{--ghost-contrast: #8883}body,button,select{font-size:14pt}#menu-bar button label,#filter-bar button label{display:none}#filter-bar{margin-top:.4em}#filter-bar button{width:17.6vw;height:2.3em}.breadcrumb{word-break:break-all}.breadcrumb .icon{font-size:24px}}.dialog-backdrop{position:fixed;inset:0;background:#888a;display:flex;justify-content:center;align-items:center;z-index:1000}.dialog{background:#fff;background:var(--bg);padding:max(.5em,1vw);border-radius:1em;position:relative;margin:0 3vw;overflow:hidden;max-height:calc(100vh - 2em);display:flex;justify-content:center}.dialog-icon{color:#fff;background-color:var(--color);position:absolute;top:0;width:1.8em;height:1.7em;text-align:center;border-radius:.8em 0}.dialog-title{margin-top:-.4em;font-size:110%;position:absolute}.dialog-icon~.dialog-title{text-align:center}.dialog-closer{border-radius:0 .8em;right:0;padding:0;background-color:#c88}.dialog-icon~.dialog-content{margin-top:2em}.dialog-type{left:0;top:0;overflow:hidden;line-height:1.7em;opacity:.8}.dialog-content{overflow:auto;max-height:calc(100vh - 4.5em)}.dialog-content p{white-space:pre-wrap;margin:.5em 0}.dialog-confirm .dialog-content button{margin-top:1em}.dialog-alert-info{--color: #282 }.dialog-alert-warning{--color: #c91 }.dialog-alert-error{--color: #822}@media (max-width: 42em){.dialog-icon{font-size:120%}.dialog-icon~.dialog-content{margin-top:2.5em}.dialog-title{margin-top:-.2em}}.dialog-prompt label{display:block;margin-bottom:.5em;margin-left:.1em}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as SF}from"./index-
|
|
1
|
+
import{c as SF}from"./index-31f0430a.js";function OF(iF,hF){for(var eF=0;eF<hF.length;eF++){const tF=hF[eF];if(typeof tF!="string"&&!Array.isArray(tF)){for(const w in tF)if(w!=="default"&&!(w in iF)){const lF=Object.getOwnPropertyDescriptor(tF,w);lF&&Object.defineProperty(iF,w,lF.get?lF:{enumerable:!0,get:()=>tF[w]})}}}return Object.freeze(Object.defineProperty(iF,Symbol.toStringTag,{value:"Module"}))}var EF={},UF={get exports(){return EF},set exports(iF){EF=iF}};/*
|
|
2
2
|
* [js-sha512]{@link https://github.com/emn178/js-sha512}
|
|
3
3
|
*
|
|
4
4
|
* @version 0.8.0
|
package/frontend/fontello.css
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
@font-face {
|
|
2
2
|
font-family: 'fontello';
|
|
3
|
-
src: url('fontello.woff2?
|
|
4
|
-
font-weight: normal;
|
|
3
|
+
src: url('fontello.woff2?66723712') format('woff2'); font-weight: normal;
|
|
5
4
|
font-style: normal;
|
|
6
5
|
}
|
|
7
6
|
/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
|
|
@@ -10,7 +9,7 @@
|
|
|
10
9
|
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
|
11
10
|
@font-face {
|
|
12
11
|
font-family: 'fontello';
|
|
13
|
-
src: url('../font/fontello.svg?
|
|
12
|
+
src: url('../font/fontello.svg?66723712#fontello') format('svg');
|
|
14
13
|
}
|
|
15
14
|
}
|
|
16
15
|
*/
|
|
@@ -50,11 +49,9 @@
|
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
.fa-cog:before { content: '\e800'; } /* '' */
|
|
53
|
-
.fa-check-circled:before { content: '\e801'; } /* '' */
|
|
54
52
|
.fa-doc:before { content: '\e802'; } /* '' */
|
|
55
53
|
.fa-stop:before { content: '\e803'; } /* '' */
|
|
56
|
-
.fa-
|
|
57
|
-
.fa-upload:before { content: '\e805'; } /* '' */
|
|
54
|
+
.fa-play:before { content: '\e804'; } /* '' */
|
|
58
55
|
.fa-cancel:before { content: '\e806'; } /* '' */
|
|
59
56
|
.fa-edit:before { content: '\e807'; } /* '' */
|
|
60
57
|
.fa-check:before { content: '\e808'; } /* '' */
|
|
@@ -62,18 +59,19 @@
|
|
|
62
59
|
.fa-user:before { content: '\e80a'; } /* '' */
|
|
63
60
|
.fa-home:before { content: '\e80b'; } /* '' */
|
|
64
61
|
.fa-key:before { content: '\e80c'; } /* '' */
|
|
65
|
-
.fa-
|
|
62
|
+
.fa-trash:before { content: '\e80d'; } /* '' */
|
|
63
|
+
.fa-to_start:before { content: '\e80e'; } /* '' */
|
|
66
64
|
.fa-retweet:before { content: '\e80f'; } /* '' */
|
|
67
|
-
.fa-
|
|
68
|
-
.fa-cancel-circled:before { content: '\e811'; } /* '' */
|
|
65
|
+
.fa-to_end:before { content: '\e810'; } /* '' */
|
|
69
66
|
.fa-search:before { content: '\e813'; } /* '' */
|
|
70
67
|
.fa-logout:before { content: '\e814'; } /* '' */
|
|
71
68
|
.fa-spin6:before { content: '\e839'; } /* '' */
|
|
72
69
|
.fa-crown:before { content: '\e844'; } /* '' */
|
|
70
|
+
.fa-download:before { content: '\f02e'; } /* '' */
|
|
71
|
+
.fa-upload:before { content: '\f02f'; } /* '' */
|
|
73
72
|
.fa-filter:before { content: '\f0b0'; } /* '' */
|
|
74
73
|
.fa-menu:before { content: '\f0c9'; } /* '' */
|
|
75
|
-
.fa-quote
|
|
74
|
+
.fa-quote:before { content: '\f10d'; } /* '' */
|
|
76
75
|
.fa-unlink:before { content: '\f127'; } /* '' */
|
|
77
76
|
.fa-level-up:before { content: '\f148'; } /* '' */
|
|
78
|
-
.fa-
|
|
79
|
-
.fa-trash:before { content: '\e80d'; } /* '' */
|
|
77
|
+
.fa-archive:before { content: '\f1c6'; } /* '' */
|
package/frontend/fontello.woff2
CHANGED
|
Binary file
|
package/frontend/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0" />
|
|
6
6
|
<link href="/fontello.css" rel="stylesheet" />
|
|
7
7
|
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-31f0430a.js"></script>
|
|
9
|
+
<link rel="stylesheet" href="/assets/index-ffbcd4c9.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
package/package.json
CHANGED
|
@@ -27,7 +27,7 @@ exports.init = api => {
|
|
|
27
27
|
if (!ctx.path.startsWith(api.const.API_URI) || ctx.params.path === undefined) return
|
|
28
28
|
let { referer } = ctx.headers
|
|
29
29
|
referer &&= new URL(referer).pathname
|
|
30
|
-
if (referer?.startsWith(api.const.ADMIN_URI)) return
|
|
30
|
+
if (referer?.startsWith(ctx.state.revProxyPath + api.const.ADMIN_URI)) return
|
|
31
31
|
toModify = ctx.params
|
|
32
32
|
}
|
|
33
33
|
const hosts = api.getConfig('hosts')
|
package/src/api.auth.js
CHANGED
|
@@ -117,7 +117,7 @@ exports.logout = logout;
|
|
|
117
117
|
const refresh_session = async ({}, ctx) => {
|
|
118
118
|
return !ctx.session ? new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR) : {
|
|
119
119
|
username: (0, perm_1.getCurrentUsername)(ctx),
|
|
120
|
-
adminUrl: (0, adminApis_1.ctxAdminAccess)(ctx) ? const_1.ADMIN_URI : undefined,
|
|
120
|
+
adminUrl: (0, adminApis_1.ctxAdminAccess)(ctx) ? ctx.state.revProxyPath + const_1.ADMIN_URI : undefined,
|
|
121
121
|
...makeExp(),
|
|
122
122
|
};
|
|
123
123
|
};
|
package/src/api.file_list.js
CHANGED
|
@@ -22,7 +22,9 @@ const file_list = async ({ path, offset, limit, search, omit, sse }, ctx) => {
|
|
|
22
22
|
if ((0, misc_1.dirTraversal)(search))
|
|
23
23
|
return fail(const_1.HTTP_FOOL);
|
|
24
24
|
if (node.default)
|
|
25
|
-
return (sse ? list.custom : lodash_1.default.identity)({
|
|
25
|
+
return (sse ? list.custom : lodash_1.default.identity)({
|
|
26
|
+
redirect: path // tell the browser to access the folder (instead of using this api), so it will get the default file
|
|
27
|
+
});
|
|
26
28
|
if (!await (0, vfs_1.nodeIsDirectory)(node))
|
|
27
29
|
return fail(const_1.HTTP_METHOD_NOT_ALLOWED);
|
|
28
30
|
offset = Number(offset);
|
package/src/api.lang.js
CHANGED
|
@@ -8,8 +8,8 @@ const apiMiddleware_1 = require("./apiMiddleware");
|
|
|
8
8
|
const lodash_1 = __importDefault(require("lodash"));
|
|
9
9
|
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
10
10
|
const promises_1 = require("fs/promises");
|
|
11
|
-
const util_files_1 = require("./util-files");
|
|
12
11
|
const const_1 = require("./const");
|
|
12
|
+
const misc_1 = require("./misc");
|
|
13
13
|
const PREFIX = 'hfs-lang-';
|
|
14
14
|
const SUFFIX = '.json';
|
|
15
15
|
const apis = {
|
|
@@ -44,7 +44,11 @@ const apis = {
|
|
|
44
44
|
if (code.endsWith(SUFFIX)) // filename, actually
|
|
45
45
|
code = code.slice(PREFIX.length, -SUFFIX.length);
|
|
46
46
|
validateCode(code);
|
|
47
|
-
|
|
47
|
+
const fn = code2file(code);
|
|
48
|
+
const s = content = String(content);
|
|
49
|
+
if (!(0, misc_1.tryJson)(s))
|
|
50
|
+
return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_ACCEPTABLE, "bad content for file " + fn);
|
|
51
|
+
await (0, promises_1.writeFile)(fn, s, 'utf8');
|
|
48
52
|
}
|
|
49
53
|
return {};
|
|
50
54
|
}
|
|
@@ -54,6 +58,6 @@ function code2file(code) {
|
|
|
54
58
|
return PREFIX + code.toLowerCase() + SUFFIX;
|
|
55
59
|
}
|
|
56
60
|
function validateCode(code) {
|
|
57
|
-
if (
|
|
58
|
-
throw new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'bad code');
|
|
61
|
+
if (!/^(\w\w)(-\w\w)*$/.test(code))
|
|
62
|
+
throw new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'bad code/filename');
|
|
59
63
|
}
|
package/src/api.monitor.js
CHANGED
|
@@ -12,12 +12,17 @@ const throttler_1 = require("./throttler");
|
|
|
12
12
|
const perm_1 = require("./perm");
|
|
13
13
|
const apis = {
|
|
14
14
|
async disconnect({ ip, port, wait }) {
|
|
15
|
+
var _a, _b;
|
|
15
16
|
const match = lodash_1.default.matches({ ip, port });
|
|
16
17
|
const c = (0, connections_1.getConnections)().find(c => match(getConnAddress(c)));
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
if (c) {
|
|
19
|
+
const waiter = (0, misc_1.pendingPromise)();
|
|
20
|
+
c.socket.end(waiter.resolve);
|
|
21
|
+
(_a = c.ctx) === null || _a === void 0 ? void 0 : _a.res.end();
|
|
22
|
+
(_b = c.ctx) === null || _b === void 0 ? void 0 : _b.req.socket.end('');
|
|
23
|
+
if (wait)
|
|
24
|
+
await waiter;
|
|
25
|
+
}
|
|
21
26
|
return { result: Boolean(c) };
|
|
22
27
|
},
|
|
23
28
|
get_connections({}, ctx) {
|
package/src/api.vfs.js
CHANGED
|
@@ -25,6 +25,7 @@ const apis = {
|
|
|
25
25
|
defaultPerms: vfs_1.defaultPerms,
|
|
26
26
|
};
|
|
27
27
|
async function recur(node) {
|
|
28
|
+
var _a;
|
|
28
29
|
const stats = Boolean(node.source) && await (0, promises_1.stat)(node.source).catch(e => false);
|
|
29
30
|
const isDir = !node.source || stats && stats.isDirectory();
|
|
30
31
|
const copyStats = stats ? lodash_1.default.pick(stats, ['size', 'ctime', 'mtime'])
|
|
@@ -35,7 +36,8 @@ const apis = {
|
|
|
35
36
|
return {
|
|
36
37
|
...copyStats,
|
|
37
38
|
...node,
|
|
38
|
-
website:
|
|
39
|
+
website: Boolean((_a = node.children) === null || _a === void 0 ? void 0 : _a.find((0, vfs_1.isSameFilenameAs)('index.html')))
|
|
40
|
+
|| isDir && node.source && await (0, promises_1.stat)((0, path_1.join)(node.source, 'index.html')).then(() => true, () => undefined)
|
|
39
41
|
|| undefined,
|
|
40
42
|
name: isRoot ? undefined : (0, vfs_1.getNodeName)(node),
|
|
41
43
|
type: isDir ? 'folder' : undefined,
|
|
@@ -58,6 +60,8 @@ const apis = {
|
|
|
58
60
|
return new apiMiddleware_1.ApiError(const_1.HTTP_CONFLICT, 'item with same name already present in destination');
|
|
59
61
|
const oldParent = await urlToNodeOriginal((0, path_1.dirname)(from));
|
|
60
62
|
lodash_1.default.pull(oldParent.children, fromNode);
|
|
63
|
+
if (lodash_1.default.isEmpty(oldParent.children))
|
|
64
|
+
delete oldParent.children;
|
|
61
65
|
(parentNode.children || (parentNode.children = [])).push(fromNode);
|
|
62
66
|
await (0, vfs_1.saveVfs)();
|
|
63
67
|
return {};
|
|
@@ -91,7 +95,8 @@ const apis = {
|
|
|
91
95
|
if ((0, misc_1.isWindowsDrive)(source))
|
|
92
96
|
source += '\\'; // slash must be included, otherwise it will refer to the cwd of that drive
|
|
93
97
|
n.children || (n.children = []);
|
|
94
|
-
|
|
98
|
+
const sameName = (0, vfs_1.isSameFilenameAs)(name);
|
|
99
|
+
if (n.children.find(x => source && source === x.source || sameName(x)))
|
|
95
100
|
return new apiMiddleware_1.ApiError(const_1.HTTP_CONFLICT, 'already present');
|
|
96
101
|
n.children.unshift({ source, name });
|
|
97
102
|
await (0, vfs_1.saveVfs)();
|
package/src/apiMiddleware.js
CHANGED
|
@@ -31,8 +31,10 @@ function apiMiddleware(apis) {
|
|
|
31
31
|
// we don't rely on SameSite cookie option because it's https-only
|
|
32
32
|
let res;
|
|
33
33
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
if (params.path)
|
|
35
|
+
params.path = (0, misc_1.removeStarting)(ctx.state.revProxyPath, params.path);
|
|
36
|
+
res = csrf && csrf !== params.csrf ? new ApiError(const_1.HTTP_UNAUTHORIZED, 'csrf')
|
|
37
|
+
: await apiFun(params || {}, ctx);
|
|
36
38
|
}
|
|
37
39
|
catch (e) {
|
|
38
40
|
res = e;
|
package/src/const.js
CHANGED
|
@@ -38,7 +38,7 @@ exports.DEV = process.env.DEV || exports.argv.dev ? 'DEV' : '';
|
|
|
38
38
|
exports.ORIGINAL_CWD = process.cwd();
|
|
39
39
|
exports.HFS_STARTED = new Date();
|
|
40
40
|
const PKG_PATH = (0, path_1.join)(__dirname, '..', 'package.json');
|
|
41
|
-
exports.BUILD_TIMESTAMP = "2023-03-
|
|
41
|
+
exports.BUILD_TIMESTAMP = "2023-03-13T19:06:29.589Z";
|
|
42
42
|
const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf8'));
|
|
43
43
|
exports.VERSION = pkg.version;
|
|
44
44
|
exports.DAY = 86400000;
|
package/src/index.js
CHANGED
|
@@ -43,6 +43,7 @@ function errorHandler(err) {
|
|
|
43
43
|
if (const_1.DEV && code === 'ENOENT' && err.path.endsWith('sockjs-node'))
|
|
44
44
|
return; // spam out dev stuff
|
|
45
45
|
if (code === 'ECANCELED' || code === 'ECONNRESET' || code === 'ECONNABORTED' || code === 'EPIPE'
|
|
46
|
+
|| code === 'ERR_STREAM_WRITE_AFTER_END' // happens disconnecting uploads, don't care
|
|
46
47
|
|| code === 'HPE_INVALID_EOF_STATE')
|
|
47
48
|
return; // someone interrupted, don't care
|
|
48
49
|
console.error('server error', err);
|
package/src/log.js
CHANGED
|
@@ -28,6 +28,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
28
28
|
};
|
|
29
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
30
|
exports.log = exports.loggers = void 0;
|
|
31
|
+
const stream_1 = require("stream");
|
|
31
32
|
const config_1 = require("./config");
|
|
32
33
|
const fs_1 = require("fs");
|
|
33
34
|
const util = __importStar(require("util"));
|
|
@@ -81,44 +82,47 @@ const logRotation = (0, config_1.defineConfig)('log_rotation', 'weekly');
|
|
|
81
82
|
function log() {
|
|
82
83
|
const debounce = lodash_1.default.debounce(cb => cb(), 1000);
|
|
83
84
|
return async (ctx, next) => {
|
|
84
|
-
var _a, _b;
|
|
85
|
-
await next();
|
|
86
|
-
const isError = ctx.status >= 400;
|
|
87
|
-
const logger = isError && accessErrorLog || accessLogger;
|
|
88
|
-
const rotate = (_a = logRotation.get()) === null || _a === void 0 ? void 0 : _a[0];
|
|
89
|
-
let { stream, last, path } = logger;
|
|
90
|
-
if (!stream)
|
|
91
|
-
return;
|
|
92
85
|
const now = new Date();
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
86
|
+
await next();
|
|
87
|
+
console.debug(ctx.status, ctx.method, ctx.path);
|
|
88
|
+
Promise.race([(0, stream_1.once)(ctx.res, 'finish'), (0, stream_1.once)(ctx.res, 'close')]).then(() => {
|
|
89
|
+
var _a, _b, _c, _d;
|
|
90
|
+
const isError = ctx.status >= 400;
|
|
91
|
+
const logger = isError && accessErrorLog || accessLogger;
|
|
92
|
+
const rotate = (_a = logRotation.get()) === null || _a === void 0 ? void 0 : _a[0];
|
|
93
|
+
let { stream, last, path } = logger;
|
|
94
|
+
if (!stream)
|
|
95
|
+
return;
|
|
96
|
+
logger.last = now;
|
|
97
|
+
if (rotate && last) { // rotation enabled and a file exists?
|
|
98
|
+
const passed = Number(now) - Number(last)
|
|
99
|
+
- 3600000; // be pessimistic and count a possible DST change
|
|
100
|
+
if (rotate === 'm' && (passed >= 31 * const_1.DAY || now.getMonth() !== last.getMonth())
|
|
101
|
+
|| rotate === 'd' && (passed >= const_1.DAY || now.getDate() !== last.getDate()) // checking passed will solve the case when the day of the month is the same but a month has passed
|
|
102
|
+
|| rotate === 'w' && (passed >= 7 * const_1.DAY || now.getDay() < last.getDay())) {
|
|
103
|
+
stream.end();
|
|
104
|
+
const postfix = last.getFullYear() + '-' + doubleDigit(last.getMonth() + 1) + '-' + doubleDigit(last.getDate());
|
|
105
|
+
try { // other logging requests shouldn't happen while we are renaming. Since this is very infrequent we can tolerate solving this by making it sync.
|
|
106
|
+
(0, fs_1.renameSync)(path, path + '-' + postfix);
|
|
107
|
+
}
|
|
108
|
+
catch (e) { // ok, rename failed, but this doesn't mean we ain't gonna log
|
|
109
|
+
console.error(e);
|
|
110
|
+
}
|
|
111
|
+
stream = logger.reopen(); // keep variable updated
|
|
112
|
+
if (!stream)
|
|
113
|
+
return;
|
|
108
114
|
}
|
|
109
|
-
stream = logger.reopen(); // keep variable updated
|
|
110
|
-
if (!stream)
|
|
111
|
-
return;
|
|
112
115
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
const format = '%s - %s [%s] "%s %s HTTP/%s" %d %s\n'; // Apache's Common Log Format
|
|
117
|
+
const a = now.toString().split(' ');
|
|
118
|
+
const date = a[2] + '/' + a[1] + '/' + a[3] + ':' + a[4] + ' ' + ((_b = a[5]) === null || _b === void 0 ? void 0 : _b.slice(3));
|
|
119
|
+
const user = (0, perm_1.getCurrentUsername)(ctx);
|
|
120
|
+
const length = (_c = ctx.state.length) !== null && _c !== void 0 ? _c : ctx.length;
|
|
121
|
+
events_1.default.emit(logger.name, Object.assign(lodash_1.default.pick(ctx, ['ip', 'method', 'status']), { length, user, ts: now, uri: ctx.path }));
|
|
122
|
+
debounce(() => // once in a while we check if the file is still good (not deleted, etc), or we'll reopen it
|
|
123
|
+
(0, promises_1.stat)(logger.path).catch(() => logger.reopen())); // async = smoother but we may lose some entries
|
|
124
|
+
stream.write(util.format(format, ctx.ip, user || '-', date, ctx.method, ctx.path, ctx.req.httpVersion, ctx.status, (_d = length === null || length === void 0 ? void 0 : length.toString()) !== null && _d !== void 0 ? _d : '-'));
|
|
125
|
+
});
|
|
122
126
|
};
|
|
123
127
|
}
|
|
124
128
|
exports.log = log;
|
package/src/middlewares.js
CHANGED
|
@@ -79,8 +79,8 @@ const serveGuiAndSharedFiles = async (ctx, next) => {
|
|
|
79
79
|
return next();
|
|
80
80
|
if (path.startsWith(const_2.FRONTEND_URI))
|
|
81
81
|
return serveFrontendPrefixed(ctx, next);
|
|
82
|
-
if (path
|
|
83
|
-
return ctx.redirect(const_1.ADMIN_URI);
|
|
82
|
+
if (path.length === const_1.ADMIN_URI.length - 1 && const_1.ADMIN_URI.startsWith(path))
|
|
83
|
+
return ctx.redirect(ctx.state.revProxyPath + const_1.ADMIN_URI);
|
|
84
84
|
if (path.startsWith(const_1.ADMIN_URI))
|
|
85
85
|
return serveAdminPrefixed(ctx, next);
|
|
86
86
|
if (ctx.method === 'PUT') { // curl -T file url/
|
|
@@ -97,7 +97,7 @@ const serveGuiAndSharedFiles = async (ctx, next) => {
|
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
99
|
if (ctx.originalUrl === '/favicon.ico' && adminApis_1.favicon.get()) // originalUrl to not be subject to changes (vhosting plugin)
|
|
100
|
-
return (0, serveFile_1.serveFile)(adminApis_1.favicon.get())
|
|
100
|
+
return (0, serveFile_1.serveFile)(ctx, adminApis_1.favicon.get());
|
|
101
101
|
const node = await (0, vfs_1.urlToNode)(path, ctx);
|
|
102
102
|
if (!node)
|
|
103
103
|
return ctx.status = const_1.HTTP_NOT_FOUND;
|
|
@@ -115,9 +115,9 @@ const serveGuiAndSharedFiles = async (ctx, next) => {
|
|
|
115
115
|
const canRead = (0, vfs_1.hasPermission)(node, 'can_read', ctx);
|
|
116
116
|
const isFolder = await (0, vfs_1.nodeIsDirectory)(node);
|
|
117
117
|
if (isFolder && !path.endsWith('/'))
|
|
118
|
-
return ctx.redirect(ctx.originalUrl + '/');
|
|
118
|
+
return ctx.redirect(ctx.state.revProxyPath + ctx.originalUrl + '/');
|
|
119
119
|
if (canRead && !isFolder)
|
|
120
|
-
return node.source ? (0, serveFile_1.serveFileNode)(
|
|
120
|
+
return node.source ? (0, serveFile_1.serveFileNode)(ctx, node)
|
|
121
121
|
: next();
|
|
122
122
|
if (!canRead) {
|
|
123
123
|
ctx.status = (0, vfs_1.cantReadStatusCode)(node);
|
|
@@ -136,7 +136,7 @@ const serveGuiAndSharedFiles = async (ctx, next) => {
|
|
|
136
136
|
if (node.default) {
|
|
137
137
|
const def = await (0, vfs_1.urlToNode)(path + node.default, ctx);
|
|
138
138
|
return !def ? next()
|
|
139
|
-
: (0, vfs_1.hasPermission)(def, 'can_read', ctx) ? (0, serveFile_1.serveFileNode)(
|
|
139
|
+
: (0, vfs_1.hasPermission)(def, 'can_read', ctx) ? (0, serveFile_1.serveFileNode)(ctx, def)
|
|
140
140
|
: ctx.status = (0, vfs_1.cantReadStatusCode)(def);
|
|
141
141
|
}
|
|
142
142
|
return serveFrontendFiles(ctx, next);
|
|
@@ -173,6 +173,7 @@ const prepareState = async (ctx, next) => {
|
|
|
173
173
|
// calculate these once and for all
|
|
174
174
|
ctx.state.account = (_a = await getHttpAccount(ctx)) !== null && _a !== void 0 ? _a : (0, perm_1.getAccount)((_b = ctx.session) === null || _b === void 0 ? void 0 : _b.username, false);
|
|
175
175
|
const conn = ctx.state.connection = (0, connections_1.socket2connection)(ctx.socket);
|
|
176
|
+
ctx.state.revProxyPath = ctx.get('x-forwarded-prefix');
|
|
176
177
|
await next();
|
|
177
178
|
if (conn)
|
|
178
179
|
(0, connections_1.updateConnection)(conn, { ctx });
|
package/src/misc.js
CHANGED
|
@@ -18,7 +18,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
18
18
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
19
|
};
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.try_ = exports.stream2string = exports.tryJson = exports.same = exports.isLocalHost = exports.with_ = exports.typedKeys = exports.objRenameKey = exports.onOff = exports.pendingPromise = exports.onlyTruthy = exports.truthy = exports.pattern2filter = exports.onFirstEvent = exports.onProcessExit = exports.randomId = exports.getOrSet = exports.wantArray = exports.waitFor = exports.wait = exports.newObj = exports.setHidden = exports.prefix = exports.enforceFinal = exports.debounceAsync = void 0;
|
|
21
|
+
exports.try_ = exports.stream2string = exports.tryJson = exports.same = exports.isLocalHost = exports.with_ = exports.typedKeys = exports.objRenameKey = exports.onOff = exports.pendingPromise = exports.onlyTruthy = exports.truthy = exports.pattern2filter = exports.onFirstEvent = exports.onProcessExit = exports.randomId = exports.getOrSet = exports.wantArray = exports.waitFor = exports.wait = exports.newObj = exports.setHidden = exports.prefix = exports.removeStarting = exports.enforceFinal = exports.debounceAsync = void 0;
|
|
22
22
|
const path_1 = require("path");
|
|
23
23
|
const lodash_1 = __importDefault(require("lodash"));
|
|
24
24
|
const assert_1 = __importDefault(require("assert"));
|
|
@@ -31,6 +31,10 @@ function enforceFinal(sub, s) {
|
|
|
31
31
|
return s.endsWith(sub) ? s : s + sub;
|
|
32
32
|
}
|
|
33
33
|
exports.enforceFinal = enforceFinal;
|
|
34
|
+
function removeStarting(sub, s) {
|
|
35
|
+
return s.startsWith(sub) ? s.slice(sub.length) : s;
|
|
36
|
+
}
|
|
37
|
+
exports.removeStarting = removeStarting;
|
|
34
38
|
function prefix(pre, v, post = '') {
|
|
35
39
|
return v ? pre + v + post : '';
|
|
36
40
|
}
|
|
@@ -104,7 +108,7 @@ onFirstEvent(process, ['exit', 'SIGQUIT', 'SIGTERM', 'SIGINT', 'SIGHUP'], signal
|
|
|
104
108
|
function onFirstEvent(emitter, events, cb) {
|
|
105
109
|
let already = false;
|
|
106
110
|
for (const e of events)
|
|
107
|
-
emitter.
|
|
111
|
+
emitter.once(e, (...args) => {
|
|
108
112
|
if (already)
|
|
109
113
|
return;
|
|
110
114
|
already = true;
|
package/src/plugins.js
CHANGED
|
@@ -90,7 +90,7 @@ exports.getPluginConfigFields = getPluginConfigFields;
|
|
|
90
90
|
function pluginsMiddleware() {
|
|
91
91
|
return async (ctx, next) => {
|
|
92
92
|
var _a;
|
|
93
|
-
const after =
|
|
93
|
+
const after = {};
|
|
94
94
|
// run middleware plugins
|
|
95
95
|
for (const [id, pl] of Object.entries(plugins))
|
|
96
96
|
try {
|
|
@@ -98,11 +98,10 @@ function pluginsMiddleware() {
|
|
|
98
98
|
if (res === true)
|
|
99
99
|
ctx.pluginStopped = true;
|
|
100
100
|
if (typeof res === 'function')
|
|
101
|
-
after
|
|
101
|
+
after[id] = res;
|
|
102
102
|
}
|
|
103
103
|
catch (e) {
|
|
104
|
-
|
|
105
|
-
console.debug(e);
|
|
104
|
+
printError(id, e);
|
|
106
105
|
}
|
|
107
106
|
// expose public plugins' files
|
|
108
107
|
const { path } = ctx;
|
|
@@ -111,14 +110,23 @@ function pluginsMiddleware() {
|
|
|
111
110
|
const a = path.substring(const_1.PLUGINS_PUB_URI.length).split('/');
|
|
112
111
|
const name = a.shift();
|
|
113
112
|
if (plugins.hasOwnProperty(name)) // do it only if the plugin is loaded
|
|
114
|
-
await (0, serveFile_1.serveFile)(plugins[name].folder + '/public/' + a.join('/'), 'auto')
|
|
113
|
+
await (0, serveFile_1.serveFile)(ctx, plugins[name].folder + '/public/' + a.join('/'), 'auto');
|
|
115
114
|
return;
|
|
116
115
|
}
|
|
117
116
|
await next();
|
|
118
117
|
}
|
|
119
|
-
for (const f of after)
|
|
120
|
-
|
|
118
|
+
for (const [id, f] of Object.entries(after))
|
|
119
|
+
try {
|
|
120
|
+
await f();
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
printError(id, e);
|
|
124
|
+
}
|
|
121
125
|
};
|
|
126
|
+
function printError(id, e) {
|
|
127
|
+
console.log('error middleware plugin', id, String(e));
|
|
128
|
+
console.debug(e);
|
|
129
|
+
}
|
|
122
130
|
}
|
|
123
131
|
exports.pluginsMiddleware = pluginsMiddleware;
|
|
124
132
|
class Plugin {
|
|
@@ -222,10 +230,11 @@ async function rescan() {
|
|
|
222
230
|
continue;
|
|
223
231
|
const module = (0, path_1.resolve)(f);
|
|
224
232
|
const { unwatch } = (0, watchLoad_1.watchLoad)(f, async () => {
|
|
233
|
+
var _a;
|
|
225
234
|
try {
|
|
226
235
|
const alreadyRunning = plugins[id];
|
|
227
236
|
console.log(alreadyRunning ? "reloading plugin" : "loading plugin", id);
|
|
228
|
-
const { init, ...data } = await Promise.resolve().then(() => __importStar(require(
|
|
237
|
+
const { init, ...data } = await (_a = module, Promise.resolve().then(() => __importStar(require(_a))));
|
|
229
238
|
delete data.default;
|
|
230
239
|
deleteModule(require.resolve(module)); // avoid caching at next import
|
|
231
240
|
calculateBadApi(data);
|
package/src/serveFile.js
CHANGED
|
@@ -15,63 +15,61 @@ const lodash_1 = __importDefault(require("lodash"));
|
|
|
15
15
|
const path_1 = __importDefault(require("path"));
|
|
16
16
|
const util_1 = require("util");
|
|
17
17
|
const allowedReferer = (0, config_1.defineConfig)('allowed_referer', '');
|
|
18
|
-
function serveFileNode(node) {
|
|
18
|
+
function serveFileNode(ctx, node) {
|
|
19
|
+
var _a;
|
|
19
20
|
const { source, mime } = node;
|
|
20
21
|
const name = (0, vfs_1.getNodeName)(node);
|
|
21
22
|
const mimeString = typeof mime === 'string' ? mime
|
|
22
23
|
: lodash_1.default.find(mime, (val, mask) => (0, micromatch_1.isMatch)(name, mask));
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const s = ctx.get('host');
|
|
33
|
-
return s[0] === '[' ? s.slice(1, s.indexOf(']')) : s === null || s === void 0 ? void 0 : s.split(':')[0];
|
|
34
|
-
}
|
|
24
|
+
const allowed = allowedReferer.get();
|
|
25
|
+
if (allowed) {
|
|
26
|
+
const ref = (_a = /\/\/([^:/]+)/.exec(ctx.get('referer'))) === null || _a === void 0 ? void 0 : _a[1]; // extract host from url
|
|
27
|
+
if (ref && ref !== host() // automatic accept if referer is basically the hosting domain
|
|
28
|
+
&& !(0, micromatch_1.isMatch)(ref, allowed))
|
|
29
|
+
return ctx.status = const_1.HTTP_FORBIDDEN;
|
|
30
|
+
function host() {
|
|
31
|
+
const s = ctx.get('host');
|
|
32
|
+
return s[0] === '[' ? s.slice(1, s.indexOf(']')) : s === null || s === void 0 ? void 0 : s.split(':')[0];
|
|
35
33
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
}
|
|
35
|
+
ctx.vfsNode = node; // useful to tell service files from files shared by the user
|
|
36
|
+
return serveFile(ctx, source || '', mimeString);
|
|
39
37
|
}
|
|
40
38
|
exports.serveFileNode = serveFileNode;
|
|
41
39
|
const mimeCfg = (0, config_1.defineConfig)('mime', { '*': 'auto' });
|
|
42
|
-
function serveFile(source, mime, content) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
40
|
+
async function serveFile(ctx, source, mime, content) {
|
|
41
|
+
if (!source)
|
|
42
|
+
return;
|
|
43
|
+
const fn = path_1.default.basename(source);
|
|
44
|
+
if (ctx.params.dl !== undefined) // please, download
|
|
45
|
+
ctx.attachment(fn);
|
|
46
|
+
mime = mime !== null && mime !== void 0 ? mime : lodash_1.default.find(mimeCfg.get(), (v, k) => k > '' && (0, micromatch_1.isMatch)(fn, k)); // isMatch throws on an empty string
|
|
47
|
+
if (mime === vfs_1.MIME_AUTO)
|
|
48
|
+
mime = mime_types_1.default.lookup(source) || '';
|
|
49
|
+
if (mime)
|
|
50
|
+
ctx.type = mime;
|
|
51
|
+
if (ctx.method === 'OPTIONS') {
|
|
52
|
+
ctx.status = const_1.HTTP_NO_CONTENT;
|
|
53
|
+
ctx.set({ Allow: 'OPTIONS, GET, HEAD' });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (ctx.method !== 'GET')
|
|
57
|
+
return ctx.status = const_1.HTTP_METHOD_NOT_ALLOWED;
|
|
58
|
+
try {
|
|
59
|
+
const stats = await (0, util_1.promisify)(fs_1.stat)(source); // using fs's function instead of fs/promises, because only the former is supported by pkg
|
|
60
|
+
ctx.set('Last-Modified', stats.mtime.toUTCString());
|
|
61
|
+
ctx.fileSource = source;
|
|
62
|
+
ctx.status = const_1.HTTP_OK;
|
|
63
|
+
if (ctx.fresh)
|
|
64
|
+
return ctx.status = const_1.HTTP_NOT_MODIFIED;
|
|
65
|
+
if (content !== undefined)
|
|
66
|
+
return ctx.body = content;
|
|
67
|
+
const range = getRange(ctx, stats.size);
|
|
68
|
+
ctx.body = (0, fs_1.createReadStream)(source, range);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
return ctx.status = const_1.HTTP_NOT_FOUND;
|
|
72
|
+
}
|
|
75
73
|
}
|
|
76
74
|
exports.serveFile = serveFile;
|
|
77
75
|
function getRange(ctx, totalSize) {
|