hfs 0.37.0 → 0.38.2

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.
@@ -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{word-break:break-word;padding-right:.3em}ul.dir li a .icon{margin-right:.3em}ul.dir li a.container-folder: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-3867a36f.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}};/*
1
+ import{c as SF}from"./index-4b763f31.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
@@ -4,16 +4,12 @@
4
4
  <meta charset="utf-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
- <link rel="icon"/>
8
- <script>SESSION = _HFS_SESSION_</script>
9
- <title>_HFS_TITLE_</title>
10
- <script type="module" crossorigin src="/assets/index-420a7286.js"></script>
11
- <link rel="stylesheet" href="/assets/index-216f7ea9.css">
7
+
8
+ <script type="module" crossorigin src="/assets/index-4b763f31.js"></script>
9
+ <link rel="stylesheet" href="/assets/index-6d4e81f7.css">
12
10
  </head>
13
11
  <body>
14
12
  <noscript>You need to enable JavaScript to run this app.</noscript>
15
-
16
13
  <div id="root"></div>
17
- <div hidden>_HFS_PLUGINS_</div>
18
14
  </body>
19
15
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hfs",
3
- "version": "0.37.0",
3
+ "version": "0.38.2",
4
4
  "description": "HTTP File Server",
5
5
  "keywords": [
6
6
  "file server",
@@ -28,6 +28,7 @@
28
28
  "dist-modules": "cp package*.json dist && cd dist && npm ci --omit=dev && npm run dist-crclib && rm package-lock.json && cd .. && node prune_modules",
29
29
  "dist-crclib": "npm i -f --no-save --omit=dev @node-rs/crc32-win32-x64-msvc @node-rs/crc32-darwin-arm64 @node-rs/crc32-darwin-x64 ",
30
30
  "dist-win": "cp package*.json dist && cd dist && npm ci --omit=dev && npm i -f --no-save --omit=dev @node-rs/crc32-win32 && pkg . -C gzip -t node16-win-x64",
31
+ "dist-mac": "cp package*.json dist && cd dist && npm ci --omit=dev && pkg . -C gzip -t node16-macos-arm64",
31
32
  "dist-node": "npm run dist-modules && cd dist && zip hfs-node.zip -r * -x *.zip *.exe hfs-* *.log logs"
32
33
  },
33
34
  "engines": {
@@ -59,7 +60,7 @@
59
60
  },
60
61
  "dependencies": {
61
62
  "@koa/router": "^10.1.1",
62
- "@node-rs/crc32": "^1.5.1",
63
+ "@node-rs/crc32": "^1.6.0",
63
64
  "basic-auth": "^2.0.1",
64
65
  "buffer-crc32": "^0.2.13",
65
66
  "cidr-tools": "^4.3.0",
@@ -1 +1 @@
1
- .download-counter::after { content: " — " }
1
+ .download-counter::after { content: var(--separator) }
package/src/adminApis.js CHANGED
@@ -39,7 +39,6 @@ const api_monitor_1 = __importDefault(require("./api.monitor"));
39
39
  const api_lang_1 = __importDefault(require("./api.lang"));
40
40
  const connections_1 = require("./connections");
41
41
  const misc_1 = require("./misc");
42
- const lodash_1 = __importDefault(require("lodash"));
43
42
  const events_1 = __importDefault(require("./events"));
44
43
  const perm_1 = require("./perm");
45
44
  const middlewares_1 = require("./middlewares");
@@ -49,6 +48,7 @@ const readline = __importStar(require("readline"));
49
48
  const log_1 = require("./log");
50
49
  const child_process_1 = require("child_process");
51
50
  const util_1 = require("util");
51
+ const customHtml_1 = require("./customHtml");
52
52
  exports.adminApis = {
53
53
  ...api_vfs_1.default,
54
54
  ...api_accounts_1.default,
@@ -56,44 +56,47 @@ exports.adminApis = {
56
56
  ...api_monitor_1.default,
57
57
  ...api_lang_1.default,
58
58
  async set_config({ values: v }) {
59
- var _a, _b;
59
+ var _a;
60
60
  if (v) {
61
- const st = (0, listen_1.getStatus)();
62
- const noHttp = ((_a = v.port) !== null && _a !== void 0 ? _a : listen_1.portCfg.get()) < 0 || !st.httpSrv.listening;
63
- const noHttps = ((_b = v.https_port) !== null && _b !== void 0 ? _b : listen_1.httpsPortCfg.get()) < 0 || !st.httpsSrv.listening;
64
- if (noHttp && noHttps)
65
- return new apiMiddleware_1.ApiError(const_1.HTTP_FORBIDDEN, "You cannot switch off both http and https ports");
66
61
  await (0, config_1.setConfig)(v);
62
+ if (v.port === 0 || v.https_port === 0)
63
+ return (_a = await (0, misc_1.waitFor)(async () => {
64
+ const st = await (0, listen_1.getServerStatus)();
65
+ // wait for all random ports to be done, so we communicate new numbers
66
+ if ((v.port !== 0 || st.http.listening)
67
+ && (v.https_port !== 0 || st.https.listening))
68
+ return st;
69
+ }, { timeout: 1000 })) !== null && _a !== void 0 ? _a : new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR, "something went wrong changing ports");
67
70
  }
68
71
  return {};
69
72
  },
70
73
  get_config: config_1.getWholeConfig,
74
+ get_custom_html() {
75
+ return {
76
+ sections: Object.fromEntries([
77
+ ...customHtml_1.customHtmlSections.map(k => [k, '']),
78
+ ...customHtml_1.customHtmlState.sections
79
+ ])
80
+ };
81
+ },
82
+ async set_custom_html({ sections }) {
83
+ await (0, customHtml_1.saveCustomHtml)(sections);
84
+ return {};
85
+ },
71
86
  async get_status() {
72
- const st = (0, listen_1.getStatus)();
73
87
  return {
74
88
  started: const_1.HFS_STARTED,
75
89
  build: const_1.BUILD_TIMESTAMP,
76
90
  version: const_1.VERSION,
77
91
  apiVersion: const_1.API_VERSION,
78
92
  compatibleApiVersion: const_1.COMPATIBLE_API_VERSION,
79
- http: await serverStatus(st.httpSrv, listen_1.portCfg.get()),
80
- https: await serverStatus(st.httpsSrv, listen_1.httpsPortCfg.get()),
93
+ ...await (0, listen_1.getServerStatus)(),
81
94
  urls: (0, listen_1.getUrls)(),
82
95
  proxyDetected: (0, middlewares_1.getProxyDetected)(),
83
96
  frpDetected: exports.localhostAdmin.get() && !(0, middlewares_1.getProxyDetected)()
84
97
  && (0, connections_1.getConnections)().every(misc_1.isLocalHost)
85
98
  && await frpDebounced(),
86
99
  };
87
- async function serverStatus(h, configuredPort) {
88
- var _a;
89
- const busy = await h.busy;
90
- await (0, misc_1.wait)(0); // simple trick to wait for also .error to be updated. If this trickery becomes necessary elsewhere, then we should make also error a Promise.
91
- return {
92
- ...lodash_1.default.pick(h, ['listening', 'error']),
93
- busy,
94
- port: ((_a = h === null || h === void 0 ? void 0 : h.address()) === null || _a === void 0 ? void 0 : _a.port) || configuredPort,
95
- };
96
- }
97
100
  },
98
101
  async save_pem({ cert, private_key, name = 'self' }) {
99
102
  if (!cert || !private_key)
@@ -79,7 +79,9 @@ const apis = {
79
79
  user: (0, perm_1.getCurrentUsername)(ctx),
80
80
  agent: getBrowser(ctx.get('user-agent')),
81
81
  archive: ctx.state.archive,
82
- path: (ctx.fileSource || ctx.state.archive) && ctx.path // only for downloading files
82
+ upload: ctx.state.uploadProgress,
83
+ path: ctx.state.uploadPath
84
+ || (ctx.fileSource || ctx.state.archive) && ctx.path // only uploads and downloads
83
85
  };
84
86
  }
85
87
  },
@@ -62,7 +62,7 @@ const apis = {
62
62
  return {
63
63
  enabled: plugins_1.enablePlugins.get().includes(id),
64
64
  config: {
65
- ...(0, misc_1.objSameKeys)((0, plugins_1.getPluginConfigFields)(id) || {}, v => v === null || v === void 0 ? void 0 : v.defaultValue),
65
+ ...(0, misc_1.newObj)((0, plugins_1.getPluginConfigFields)(id) || {}, v => v === null || v === void 0 ? void 0 : v.defaultValue),
66
66
  ...plugins_1.pluginsConfig.get()[id]
67
67
  }
68
68
  };
package/src/api.vfs.js CHANGED
@@ -73,7 +73,7 @@ const apis = {
73
73
  if ((_a = parent === null || parent === void 0 ? void 0 : parent.children) === null || _a === void 0 ? void 0 : _a.find(x => (0, vfs_1.getNodeName)(x) === props.name))
74
74
  return new apiMiddleware_1.ApiError(const_1.HTTP_CONFLICT, 'name already present');
75
75
  }
76
- props = (0, misc_1.objSameKeys)(props, v => v === null ? undefined : v); // null is a way to serialize undefined, that will restore default values
76
+ props = (0, misc_1.newObj)(props, v => v === null ? undefined : v); // null is a way to serialize undefined, that will restore default values
77
77
  if (props.masks && typeof props.masks !== 'object')
78
78
  delete props.masks;
79
79
  Object.assign(n, props);
package/src/config.js CHANGED
@@ -101,7 +101,7 @@ function getConfig(k) {
101
101
  }
102
102
  exports.getConfig = getConfig;
103
103
  function getWholeConfig({ omit, only }) {
104
- const defs = (0, misc_1.objSameKeys)(configProps, x => x.defaultValue);
104
+ const defs = (0, misc_1.newObj)(configProps, x => x.defaultValue);
105
105
  let copy = lodash_1.default.defaults({}, state, defs);
106
106
  if (omit === null || omit === void 0 ? void 0 : omit.length)
107
107
  copy = lodash_1.default.omit(copy, omit);
@@ -113,7 +113,7 @@ exports.getWholeConfig = getWholeConfig;
113
113
  // pass a value to `save` to force saving decision, or leave undefined for auto. Passing false will also reset previously loaded configs.
114
114
  function setConfig(newCfg, save) {
115
115
  if (!started) { // first time we consider also CLI args
116
- const argCfg = lodash_1.default.pickBy((0, misc_1.objSameKeys)(configProps, (x, k) => const_1.argv[k]), x => x !== undefined);
116
+ const argCfg = lodash_1.default.pickBy((0, misc_1.newObj)(configProps, (x, k) => const_1.argv[k]), x => x !== undefined);
117
117
  if (!lodash_1.default.isEmpty(argCfg)) {
118
118
  (0, exports.saveConfigAsap)().then(); // don't set `save` argument, as it would interfere below at check `save===false`
119
119
  Object.assign(newCfg, argCfg);
@@ -12,6 +12,7 @@ class Connection {
12
12
  this.socket = socket;
13
13
  this.started = new Date();
14
14
  this.sent = 0;
15
+ this.got = 0;
15
16
  all.push(this);
16
17
  socket.on('close', () => {
17
18
  all.splice(all.indexOf(this), 1);
package/src/const.js CHANGED
@@ -38,12 +38,12 @@ 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-02-24T21:59:58.935Z";
41
+ exports.BUILD_TIMESTAMP = "2023-03-07T12:16:23.728Z";
42
42
  const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf8'));
43
43
  exports.VERSION = pkg.version;
44
44
  exports.DAY = 86400000;
45
45
  exports.SESSION_DURATION = exports.DAY;
46
- exports.API_VERSION = 5; // afterEntryName
46
+ exports.API_VERSION = 6; // config.frontend
47
47
  exports.COMPATIBLE_API_VERSION = 1; // while changes in the api are not breaking, this number stays the same, otherwise is made equal to API_VERSION
48
48
  exports.SPECIAL_URI = '/~/';
49
49
  exports.FRONTEND_URI = exports.SPECIAL_URI + 'frontend/';
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.saveCustomHtml = exports.getSection = exports.customHtmlState = exports.customHtmlSections = void 0;
7
+ const fs_1 = require("fs");
8
+ const events_1 = __importDefault(require("./events"));
9
+ const misc_1 = require("./misc");
10
+ const frontEndApis_1 = require("./frontEndApis");
11
+ const watchLoad_1 = require("./watchLoad");
12
+ const valtio_1 = require("valtio");
13
+ const promises_1 = require("fs/promises");
14
+ exports.customHtmlSections = ['top', 'bottom', 'beforeHeader', 'afterHeader',
15
+ 'afterMenuBar', 'afterEntryName', 'afterList'];
16
+ exports.customHtmlState = (0, valtio_1.proxy)({
17
+ sections: new Map()
18
+ });
19
+ const FILE = 'custom.html';
20
+ if (!(0, fs_1.existsSync)(FILE))
21
+ events_1.default.once('config ready', () => {
22
+ const legacy = (0, misc_1.prefix)('[beforeHeader]\n', frontEndApis_1.customHeader.get());
23
+ (0, fs_1.writeFileSync)(FILE, legacy);
24
+ frontEndApis_1.customHeader.set(undefined); // get rid of it
25
+ });
26
+ (0, watchLoad_1.watchLoad)(FILE, data => {
27
+ var _a;
28
+ const re = /^\[(\w+)] *$/gm;
29
+ exports.customHtmlState.sections.clear();
30
+ if (!data)
31
+ return;
32
+ let name = 'top';
33
+ do {
34
+ let last = re.lastIndex;
35
+ const match = re.exec(data);
36
+ const content = data.slice(last, !match ? undefined : re.lastIndex - (((_a = match === null || match === void 0 ? void 0 : match[0]) === null || _a === void 0 ? void 0 : _a.length) || 0)).trim();
37
+ if (content)
38
+ exports.customHtmlState.sections.set(name, content);
39
+ name = match === null || match === void 0 ? void 0 : match[1];
40
+ } while (name);
41
+ });
42
+ function getSection(name) {
43
+ return exports.customHtmlState.sections.get(name) || '';
44
+ }
45
+ exports.getSection = getSection;
46
+ async function saveCustomHtml(sections) {
47
+ const text = Object.entries(sections).filter(([k, v]) => v === null || v === void 0 ? void 0 : v.trim()).map(([k, v]) => `[${k}]\n${v}\n\n`).join('');
48
+ await (0, promises_1.writeFile)(FILE, text);
49
+ exports.customHtmlState.sections.clear();
50
+ for (const [k, v] of Object.entries(sections))
51
+ if (v)
52
+ exports.customHtmlState.sections.set(k, v);
53
+ }
54
+ exports.saveCustomHtml = saveCustomHtml;
@@ -27,7 +27,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
27
27
  return (mod && mod.__esModule) ? mod : { "default": mod };
28
28
  };
29
29
  Object.defineProperty(exports, "__esModule", { value: true });
30
- exports.notifyClient = exports.frontEndApis = void 0;
30
+ exports.notifyClient = exports.frontEndApis = exports.customHeader = void 0;
31
31
  const apiMiddleware_1 = require("./apiMiddleware");
32
32
  const api_file_list_1 = require("./api.file_list");
33
33
  const api_auth = __importStar(require("./api.auth"));
@@ -39,13 +39,10 @@ const vfs_1 = require("./vfs");
39
39
  const promises_1 = require("fs/promises");
40
40
  const path_1 = require("path");
41
41
  const misc_1 = require("./misc");
42
- const customHeader = (0, config_1.defineConfig)('custom_header');
42
+ exports.customHeader = (0, config_1.defineConfig)('custom_header');
43
43
  exports.frontEndApis = {
44
44
  file_list: api_file_list_1.file_list,
45
45
  ...api_auth,
46
- config() {
47
- return Object.fromEntries([customHeader].map(x => [x.key(), x.get()]));
48
- },
49
46
  get_notifications({ channel }, ctx) {
50
47
  apiAssertTypes({ string: { channel } });
51
48
  const list = new apiMiddleware_1.SendListReadable();
package/src/github.js CHANGED
@@ -75,7 +75,7 @@ async function apiGithub(uri) {
75
75
  throw res.statusCode;
76
76
  return JSON.parse(res.body);
77
77
  }
78
- async function* searchPlugins(text) {
78
+ async function* searchPlugins(text = '') {
79
79
  var _a;
80
80
  const res = await apiGithub('search/repositories?q=topic:hfs-plugin+' + encodeURI(text));
81
81
  for (const it of res.items) {
package/src/listen.js CHANGED
@@ -27,7 +27,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
27
27
  return (mod && mod.__esModule) ? mod : { "default": mod };
28
28
  };
29
29
  Object.defineProperty(exports, "__esModule", { value: true });
30
- exports.getUrls = exports.getStatus = exports.httpsPortCfg = exports.openAdmin = exports.portCfg = void 0;
30
+ exports.getUrls = exports.getServerStatus = exports.httpsPortCfg = exports.openAdmin = exports.portCfg = void 0;
31
31
  const http = __importStar(require("http"));
32
32
  const config_1 = require("./config");
33
33
  const index_1 = require("./index");
@@ -191,13 +191,23 @@ function stopServer(srv) {
191
191
  });
192
192
  });
193
193
  }
194
- function getStatus() {
194
+ async function getServerStatus() {
195
195
  return {
196
- httpSrv,
197
- httpsSrv,
196
+ http: await serverStatus(httpSrv, exports.portCfg.get()),
197
+ https: await serverStatus(httpsSrv, exports.httpsPortCfg.get()),
198
198
  };
199
+ async function serverStatus(h, configuredPort) {
200
+ var _a;
201
+ const busy = await h.busy;
202
+ await (0, misc_1.wait)(0); // simple trick to wait for also .error to be updated. If this trickery becomes necessary elsewhere, then we should make also error a Promise.
203
+ return {
204
+ ...lodash_1.default.pick(h, ['listening', 'error']),
205
+ busy,
206
+ port: ((_a = h === null || h === void 0 ? void 0 : h.address()) === null || _a === void 0 ? void 0 : _a.port) || configuredPort,
207
+ };
208
+ }
199
209
  }
200
- exports.getStatus = getStatus;
210
+ exports.getServerStatus = getServerStatus;
201
211
  const ignore = /^(lo|.*loopback.*|virtualbox.*|.*\(wsl\).*|llw\d|awdl\d|utun\d|anpi\d)$/i; // avoid giving too much information
202
212
  function getUrls() {
203
213
  return Object.fromEntries((0, misc_1.onlyTruthy)([httpSrv, httpsSrv].map(srv => {
@@ -198,7 +198,7 @@ async function srpCheck(username, password) {
198
198
  const paramsDecoder = async (ctx, next) => {
199
199
  ctx.params = ctx.method === 'POST' && ctx.originalUrl.startsWith(const_1.API_URI)
200
200
  ? (0, misc_1.tryJson)(await (0, misc_1.stream2string)(ctx.req))
201
- : (0, misc_1.objSameKeys)(ctx.query, x => Array.isArray(x) ? x : (0, misc_1.tryJson)(x));
201
+ : (0, misc_1.newObj)(ctx.query, x => Array.isArray(x) ? x : (0, misc_1.tryJson)(x));
202
202
  await next();
203
203
  };
204
204
  exports.paramsDecoder = paramsDecoder;
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.wait = exports.objSameKeys = 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.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"));
@@ -36,21 +36,45 @@ function prefix(pre, v, post = '') {
36
36
  }
37
37
  exports.prefix = prefix;
38
38
  function setHidden(dest, src) {
39
- return Object.defineProperties(dest, objSameKeys(src, value => ({
39
+ return Object.defineProperties(dest, newObj(src, value => ({
40
40
  enumerable: false,
41
41
  writable: true,
42
42
  value,
43
43
  })));
44
44
  }
45
45
  exports.setHidden = setHidden;
46
- function objSameKeys(src, newValue) {
47
- return Object.fromEntries(Object.entries(src).map(([k, v]) => [k, newValue(v, k)]));
46
+ function newObj(src, newValue) {
47
+ if (!src)
48
+ return {};
49
+ const pairs = Object.entries(src).map(([k, v]) => {
50
+ if (typeof k === 'symbol')
51
+ return;
52
+ let _k = k;
53
+ const newV = newValue(v, k, (newK) => {
54
+ _k = newK;
55
+ return true; // for convenient expression concatenation
56
+ });
57
+ return _k !== undefined && [_k, newV];
58
+ });
59
+ return Object.fromEntries(onlyTruthy(pairs));
48
60
  }
49
- exports.objSameKeys = objSameKeys;
61
+ exports.newObj = newObj;
50
62
  function wait(ms) {
51
63
  return new Promise(res => setTimeout(res, ms));
52
64
  }
53
65
  exports.wait = wait;
66
+ async function waitFor(cb, { interval = 200, timeout = Infinity } = {}) {
67
+ const started = Date.now();
68
+ while (1) {
69
+ const res = await cb();
70
+ if (res)
71
+ return res;
72
+ if (Date.now() - started >= timeout)
73
+ return;
74
+ await wait(interval);
75
+ }
76
+ }
77
+ exports.waitFor = waitFor;
54
78
  function wantArray(x) {
55
79
  return x == null ? [] : Array.isArray(x) ? x : [x];
56
80
  }
@@ -37,11 +37,15 @@ const apiMiddleware_1 = require("./apiMiddleware");
37
37
  const path_1 = require("path");
38
38
  const misc_1 = require("./misc");
39
39
  const adminApis_1 = require("./adminApis");
40
+ const valtio_1 = require("valtio");
41
+ const customHtml_1 = require("./customHtml");
42
+ const lodash_1 = __importDefault(require("lodash"));
40
43
  // in case of dev env we have our static files within the 'dist' folder'
41
44
  const DEV_STATIC = process.env.DEV ? 'dist/' : '';
42
45
  function serveStatic(uri) {
43
46
  const folder = uri.slice(2, -1); // we know folder is very similar to uri
44
- const cache = {};
47
+ let cache = {};
48
+ (0, valtio_1.subscribe)(customHtml_1.customHtmlState, () => cache = {}); // reset cache at every change
45
49
  return async (ctx, next) => {
46
50
  if (ctx.method === 'OPTIONS') {
47
51
  ctx.status = const_1.HTTP_NO_CONTENT;
@@ -79,13 +83,54 @@ function adjustBundlerLinks(path, uri, data) {
79
83
  async function treatIndex(ctx, body, filesUri) {
80
84
  const session = await (0, api_auth_1.refresh_session)({}, ctx);
81
85
  ctx.set('etag', '');
82
- return body
86
+ const isFrontend = filesUri === const_1.FRONTEND_URI;
87
+ const css = (0, plugins_1.mapPlugins)((plug, k) => { var _a; return (_a = (isFrontend ? plug.frontend_css : null)) === null || _a === void 0 ? void 0 : _a.map(f => const_1.PLUGINS_PUB_URI + k + '/' + f); }).flat().filter(Boolean);
88
+ const js = (0, plugins_1.mapPlugins)((plug, k) => { var _a; return (_a = (isFrontend ? plug.frontend_js : null)) === null || _a === void 0 ? void 0 : _a.map(f => const_1.PLUGINS_PUB_URI + k + '/' + f); }).flat().filter(Boolean);
89
+ // expose plugins' configs that are declared with 'frontend' attribute
90
+ const plugins = Object.fromEntries((0, misc_1.onlyTruthy)((0, plugins_1.mapPlugins)((pl, name) => {
91
+ var _a, _b;
92
+ let configs = (0, misc_1.newObj)((0, plugins_1.getPluginConfigFields)(name), (v, k, skip) => {
93
+ var _a, _b, _c, _d, _e;
94
+ return !v.frontend ? skip() :
95
+ ((_c = (_b = (_a = plugins_1.pluginsConfig.get()) === null || _a === void 0 ? void 0 : _a[name]) === null || _b === void 0 ? void 0 : _b[k]) !== null && _c !== void 0 ? _c : (_e = (_d = pl.getData().config) === null || _d === void 0 ? void 0 : _d[k]) === null || _e === void 0 ? void 0 : _e.defaultValue);
96
+ });
97
+ configs = ((_b = (_a = (0, plugins_1.getPluginInfo)(name)).onFrontendConfig) === null || _b === void 0 ? void 0 : _b.call(_a, configs)) || configs;
98
+ return !lodash_1.default.isEmpty(configs) && [name, configs];
99
+ })));
100
+ let ret = body
83
101
  .replace(/((?:src|href) *= *['"])\/?(?![a-z]+:\/\/)/g, '$1' + filesUri)
84
- .replace('<link rel="icon"/>', `<link rel="icon" href="${adminApis_1.favicon.get() ? '/favicon.ico' : 'data:;'}" />`)
85
- .replace('_HFS_TITLE_', adminApis_1.title.get())
86
- .replace('_HFS_SESSION_', session instanceof apiMiddleware_1.ApiError ? 'null' : JSON.stringify(session))
87
- // replacing this text allow us to avoid injecting in frontends that don't support plugins. Don't use a <--comment--> or it will be removed by webpack
88
- .replace('_HFS_PLUGINS_', pluginsInjection);
102
+ .replace('</head>', () => `
103
+ ${!isFrontend ? '' : `
104
+ <title>${adminApis_1.title.get()}</title>
105
+ <link rel="icon" href="${adminApis_1.favicon.get() ? '/favicon.ico' : 'data:;'}" />
106
+ `}
107
+ <script>
108
+ HFS = ${JSON.stringify({
109
+ VERSION: const_1.VERSION,
110
+ API_VERSION: const_1.API_VERSION,
111
+ session: session instanceof apiMiddleware_1.ApiError ? null : session,
112
+ plugins,
113
+ customHtml: lodash_1.default.omit(Object.fromEntries(customHtml_1.customHtmlState.sections), ['top', 'bottom']), // excluding sections we apply in this phase
114
+ }, null, 4)}
115
+ document.documentElement.setAttribute('ver', '${const_1.VERSION.split('-')[0] /*for style selectors*/}')
116
+ </script>
117
+ <style>
118
+ :root {
119
+ ${lodash_1.default.map(plugins, (configs, pluginName) => lodash_1.default.map(configs, (v, k) => `--${pluginName}-${k}: ${serializeCss(v)};`).join('\n')).join('')}
120
+ }
121
+ </style>
122
+ ${css.map(uri => `<link rel='stylesheet' type='text/css' href='${uri}'/>`).join('\n')}
123
+ ${js.map(uri => `<script defer src='${uri}'></script>`).join('\n')}
124
+ </head>`);
125
+ if (isFrontend)
126
+ ret = ret
127
+ .replace('<body>', '<body>' + (0, customHtml_1.getSection)('top'))
128
+ .replace('</body>', (0, customHtml_1.getSection)('bottom') + '</body>');
129
+ return ret;
130
+ }
131
+ function serializeCss(v) {
132
+ return typeof v === 'string' && /^#[0-9a-fA-F]{3,8}|rgba?\(.+\)$/.test(v) ? v
133
+ : JSON.stringify(v);
89
134
  }
90
135
  function serveProxied(port, uri) {
91
136
  if (!port)
@@ -104,12 +149,6 @@ function serveProxied(port, uri) {
104
149
  return proxy.apply(this, arguments);
105
150
  };
106
151
  }
107
- function pluginsInjection() {
108
- const css = (0, plugins_1.mapPlugins)((plug, k) => { var _a; return (_a = plug.frontend_css) === null || _a === void 0 ? void 0 : _a.map(f => const_1.PLUGINS_PUB_URI + k + '/' + f); }).flat().filter(Boolean);
109
- const js = (0, plugins_1.mapPlugins)((plug, k) => { var _a; return (_a = plug.frontend_js) === null || _a === void 0 ? void 0 : _a.map(f => const_1.PLUGINS_PUB_URI + k + '/' + f); }).flat().filter(Boolean);
110
- return css.map(uri => `\n<link rel='stylesheet' type='text/css' href='${uri}'/>`).join('')
111
- + js.map(uri => `\n<script defer src='${uri}'></script>`).join('');
112
- }
113
152
  function serveGuiFiles(proxyPort, uri) {
114
153
  return serveProxied(proxyPort, uri) || serveStatic(uri);
115
154
  }
package/src/throttler.js CHANGED
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.totalInSpeed = exports.totalOutSpeed = exports.totalGot = exports.totalSent = exports.throttler = void 0;
7
+ exports.totalInSpeed = exports.totalOutSpeed = exports.totalGot = exports.totalSent = exports.roundSpeed = exports.throttler = void 0;
8
8
  const stream_1 = require("stream");
9
9
  const ThrottledStream_1 = require("./ThrottledStream");
10
10
  const config_1 = require("./config");
@@ -39,7 +39,7 @@ const throttler = async (ctx, next) => {
39
39
  const DELAY = 1000;
40
40
  const update = lodash_1.default.debounce(() => {
41
41
  const ts = conn[SymThrStr];
42
- const outSpeed = roundKb(ts.getSpeed());
42
+ const outSpeed = roundSpeed(ts.getSpeed());
43
43
  (0, connections_1.updateConnection)(conn, { outSpeed, sent: ts.getBytesSent() });
44
44
  /* in case this stream stands still for a while (before the end), we'll have neither 'sent' or 'close' events,
45
45
  * so who will take care to updateConnection? This artificial next-call will ensure just that */
@@ -67,9 +67,10 @@ const throttler = async (ctx, next) => {
67
67
  ctx.response.length = bak;
68
68
  };
69
69
  exports.throttler = throttler;
70
- function roundKb(n) {
70
+ function roundSpeed(n) {
71
71
  return lodash_1.default.round(n, 1) || lodash_1.default.round(n, 3); // further precision if necessary
72
72
  }
73
+ exports.roundSpeed = roundSpeed;
73
74
  exports.totalSent = 0;
74
75
  exports.totalGot = 0;
75
76
  exports.totalOutSpeed = 0;
@@ -85,7 +86,7 @@ setInterval(() => {
85
86
  lastSent = exports.totalSent;
86
87
  const deltaGotKb = (exports.totalGot - lastGot) / 1000;
87
88
  lastGot = exports.totalGot;
88
- exports.totalOutSpeed = roundKb(deltaSentKb / past);
89
- exports.totalInSpeed = roundKb(deltaGotKb / past);
89
+ exports.totalOutSpeed = roundSpeed(deltaSentKb / past);
90
+ exports.totalInSpeed = roundSpeed(deltaGotKb / past);
90
91
  }, 1000);
91
92
  events_1.default.on('connection', (c) => c.socket.on('data', data => exports.totalGot += data.length));
package/src/upload.js CHANGED
@@ -12,6 +12,9 @@ const misc_1 = require("./misc");
12
12
  const frontEndApis_1 = require("./frontEndApis");
13
13
  const config_1 = require("./config");
14
14
  const util_os_1 = require("./util-os");
15
+ const connections_1 = require("./connections");
16
+ const throttler_1 = require("./throttler");
17
+ const lodash_1 = __importDefault(require("lodash"));
15
18
  exports.deleteUnfinishedUploadsAfter = (0, config_1.defineConfig)('delete_unfinished_uploads_after');
16
19
  exports.minAvailableMb = (0, config_1.defineConfig)('min_available_mb', 100);
17
20
  const dontOverwriteUploading = (0, config_1.defineConfig)('dont_overwrite_uploading', false);
@@ -62,7 +65,8 @@ function uploadWriter(base, path, ctx) {
62
65
  tempName = resumable;
63
66
  }
64
67
  cancelDeletion(tempName);
65
- ret.on('close', () => {
68
+ trackProgress();
69
+ ret.once('close', () => {
66
70
  if (!ctx.req.aborted) {
67
71
  let dest = fullPath;
68
72
  if (dontOverwriteUploading.get() && fs_1.default.existsSync(dest)) {
@@ -87,6 +91,24 @@ function uploadWriter(base, path, ctx) {
87
91
  delayedDelete(tempName, sec);
88
92
  });
89
93
  return ret;
94
+ function trackProgress() {
95
+ let lastGot = 0;
96
+ let lastGotTime = 0;
97
+ const conn = (0, connections_1.socket2connection)(ctx.socket);
98
+ if (!conn)
99
+ return () => { };
100
+ ctx.state.uploadPath = ctx.path + path;
101
+ (0, connections_1.updateConnection)(conn, { ctx });
102
+ const h = setInterval(() => {
103
+ const now = Date.now();
104
+ const got = ret.bytesWritten;
105
+ const inSpeed = (0, throttler_1.roundSpeed)((got - lastGot) / (now - lastGotTime));
106
+ lastGot = got;
107
+ lastGotTime = now;
108
+ (0, connections_1.updateConnection)(conn, { inSpeed, got, uploadProgress: lodash_1.default.round(got / reqSize, 3) });
109
+ }, 1000);
110
+ ret.once('close', () => clearInterval(h));
111
+ }
90
112
  function delayedDelete(path, secs, cb) {
91
113
  clearTimeout(waitingToBeDeleted[path]);
92
114
  waitingToBeDeleted[path] = setTimeout(() => {