hfs 0.46.1 → 0.47.0-alpha6

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.
Files changed (50) hide show
  1. package/README.md +4 -4
  2. package/admin/assets/{index-35f6e3dc.css → index-20dd43f3.css} +1 -1
  3. package/admin/assets/index-d55dfed9.js +518 -0
  4. package/{frontend/assets/sha512-d9635c9d.js → admin/assets/sha512-9867267f.js} +1 -1
  5. package/admin/index.html +2 -2
  6. package/frontend/assets/index-3a32a9b9.css +1 -0
  7. package/frontend/assets/index-ad78cbad.js +116 -0
  8. package/{admin/assets/sha512-3412b98f.js → frontend/assets/sha512-4c1cbf45.js} +1 -1
  9. package/frontend/fontello.css +5 -2
  10. package/frontend/fontello.woff2 +0 -0
  11. package/frontend/index.html +2 -3
  12. package/package.json +2 -2
  13. package/plugins/download-counter/plugin.js +15 -7
  14. package/plugins/download-counter/public/main.js +1 -1
  15. package/plugins/list-uploader/plugin.js +1 -1
  16. package/plugins/list-uploader/public/main.js +9 -10
  17. package/plugins/vhosting/plugin.js +5 -2
  18. package/src/QuickZipStream.js +3 -0
  19. package/src/adminApis.js +20 -5
  20. package/src/api.auth.js +0 -3
  21. package/src/api.file_list.js +3 -2
  22. package/src/api.monitor.js +36 -33
  23. package/src/api.vfs.js +37 -34
  24. package/src/apiMiddleware.js +44 -15
  25. package/src/commands.js +2 -2
  26. package/src/config.js +16 -13
  27. package/src/connections.js +3 -4
  28. package/src/consoleLog.js +17 -0
  29. package/src/const.js +4 -3
  30. package/src/frontEndApis.js +17 -10
  31. package/src/github.js +1 -1
  32. package/src/index.js +3 -1
  33. package/src/langs/embedded.js +3 -1
  34. package/src/langs/hfs-lang-es.json +118 -0
  35. package/src/langs/hfs-lang-it.json +7 -5
  36. package/src/langs/hfs-lang-nl.json +118 -0
  37. package/src/langs/hfs-lang-ru.json +3 -3
  38. package/src/langs/hfs-lang-zh.json +25 -5
  39. package/src/log.js +5 -3
  40. package/src/middlewares.js +2 -2
  41. package/src/misc.js +5 -1
  42. package/src/perm.js +1 -1
  43. package/src/serveFile.js +3 -1
  44. package/src/throttler.js +2 -1
  45. package/src/update.js +45 -7
  46. package/src/util-http.js +8 -3
  47. package/src/vfs.js +36 -12
  48. package/admin/assets/index-7f1741ba.js +0 -517
  49. package/frontend/assets/index-b49b8a16.js +0 -94
  50. package/frontend/assets/index-f2f8fd6b.css +0 -1
@@ -1,4 +1,4 @@
1
- import{g as OF,c as UF}from"./index-7f1741ba.js";function gF(sF,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 sF)){const lF=Object.getOwnPropertyDescriptor(tF,w);lF&&Object.defineProperty(sF,w,lF.get?lF:{enumerable:!0,get:()=>tF[w]})}}}return Object.freeze(Object.defineProperty(sF,Symbol.toStringTag,{value:"Module"}))}var dF={exports:{}};/*
1
+ import{g as OF,c as UF}from"./index-ad78cbad.js";function gF(sF,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 sF)){const lF=Object.getOwnPropertyDescriptor(tF,w);lF&&Object.defineProperty(sF,w,lF.get?lF:{enumerable:!0,get:()=>tF[w]})}}}return Object.freeze(Object.defineProperty(sF,Symbol.toStringTag,{value:"Module"}))}var dF={exports:{}};/*
2
2
  * [js-sha512]{@link https://github.com/emn178/js-sha512}
3
3
  *
4
4
  * @version 0.8.0
@@ -1,6 +1,6 @@
1
1
  @font-face {
2
2
  font-family: 'fontello';
3
- src: url('fontello.woff2?26713191') format('woff2');
3
+ src: url('fontello.woff2?44288867') format('woff2');
4
4
  font-weight: normal;
5
5
  font-style: normal;
6
6
  }
@@ -10,7 +10,7 @@
10
10
  @media screen and (-webkit-min-device-pixel-ratio:0) {
11
11
  @font-face {
12
12
  font-family: 'fontello';
13
- src: url('../font/fontello.svg?26713191#fontello') format('svg');
13
+ src: url('../font/fontello.svg?44288867#fontello') format('svg');
14
14
  }
15
15
  }
16
16
  */
@@ -78,7 +78,10 @@
78
78
  .fa-download:before { content: '\f02e'; } /* '' */
79
79
  .fa-upload:before { content: '\f02f'; } /* '' */
80
80
  .fa-filter:before { content: '\f0b0'; } /* '' */
81
+ .fa-zoom:before { content: '\f0b2'; } /* '' */
81
82
  .fa-menu:before { content: '\f0c9'; } /* '' */
83
+ .fa-comment:before { content: '\f0e5'; } /* '' */
84
+ .fa-fullscreen:before { content: '\f108'; } /* '' */
82
85
  .fa-quote:before { content: '\f10d'; } /* '' */
83
86
  .fa-unlink:before { content: '\f127'; } /* '' */
84
87
  .fa-archive:before { content: '\f1c6'; } /* '' */
Binary file
@@ -4,10 +4,9 @@
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
- <script>HFS={}</script>
8
7
 
9
- <script type="module" crossorigin src="/assets/index-b49b8a16.js"></script>
10
- <link rel="stylesheet" href="/assets/index-f2f8fd6b.css">
8
+ <script type="module" crossorigin src="/assets/index-ad78cbad.js"></script>
9
+ <link rel="stylesheet" href="/assets/index-3a32a9b9.css">
11
10
  </head>
12
11
  <body>
13
12
  <noscript>You need to enable JavaScript to run this app.</noscript>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hfs",
3
- "version": "0.46.1",
3
+ "version": "0.47.0-alpha6",
4
4
  "description": "HTTP File Server",
5
5
  "keywords": [
6
6
  "file server",
@@ -68,7 +68,7 @@
68
68
  "fast-glob": "^3.2.7",
69
69
  "find-process": "^1.4.7",
70
70
  "formidable": "^2.1.1",
71
- "fs-x-attributes": "^1.0.0",
71
+ "fs-x-attributes": "^1.0.2",
72
72
  "koa": "^2.13.4",
73
73
  "koa-compress": "^5.1.0",
74
74
  "koa-mount": "^4.0.0",
@@ -1,13 +1,14 @@
1
1
  // other plugins can use ctx.state.download_counter_ignore to mark downloads that shouldn't be counted
2
2
 
3
3
  exports.description = "Counts downloads for each file, and displays the total in the list or file menu"
4
- exports.version = 4.1 // fix: different cases and encodings with urls weren't properly counted
5
- exports.apiRequired = 8
4
+ exports.version = 5 // count files in archive
5
+ exports.apiRequired = 8.3
6
6
 
7
7
  exports.config = {
8
8
  where: { frontend: true, type: 'select', defaultValue: 'menu',
9
9
  options: ['list', { value: 'menu', label: "file menu" }],
10
- }
10
+ },
11
+ archives: { defaultValue: true, type: 'boolean', label: "Count files in zip/archives" },
11
12
  }
12
13
  exports.configDialog = {
13
14
  sx: { maxWidth: '20em' },
@@ -45,10 +46,17 @@ exports.init = async api => {
45
46
  unload: () => save.flush(), // we may have pending savings
46
47
  middleware: (ctx) =>
47
48
  () => { // execute after other middlewares are done
48
- if (ctx.status >= 300 || !ctx.vfsNode || ctx.state.download_counter_ignore) return
49
- const k = uri2key(ctx.path)
50
- counters[k] = counters[k] + 1 || 1
51
- save()
49
+ if (ctx.status >= 300 || ctx.state.download_counter_ignore || ctx.state.includesLastByte === false) return
50
+ if (!(ctx.vfsNode || api.getConfig('archives') && ctx.state.archive)) return
51
+ ctx.state.completed.then(() => {
52
+ const key = uri2key(ctx.path)
53
+ const entries = ctx.vfsNode ? [key]
54
+ : ctx.state.originalStream?.getArchiveEntries?.().filter(x => x.at(-1) !== '/').map(x => key + uri2key(x))
55
+ if (!entries) return
56
+ for (const k of entries)
57
+ counters[k] = counters[k] + 1 || 1
58
+ save()
59
+ })
52
60
  },
53
61
  onDirEntry: ({ entry, listUri }) => {
54
62
  const k = uri2key(listUri + entry.n)
@@ -7,7 +7,7 @@
7
7
  hits && !inMenu && `<span class="download-counter" title="${label}">${hits}</span>`)
8
8
 
9
9
  HFS.onEvent('fileMenu', ({ entry, props }) => {
10
- if (inMenu && !entry.isFolder)
10
+ if (!entry.isFolder)
11
11
  props.push([label, entry.hits || 0])
12
12
  })
13
13
  }
@@ -1,6 +1,6 @@
1
1
  exports.version = 1.0
2
2
  exports.description = "Show uploader info in list"
3
- exports.apiRequired = 8.23
3
+ exports.apiRequired = 8.3 // useBatch
4
4
  exports.frontend_js = "main.js"
5
5
 
6
6
  exports.configDialog = {
@@ -1,27 +1,26 @@
1
1
  "use strict";{
2
+ const { React } = HFS
2
3
  const { display } = HFS.getPluginConfig()
3
4
 
4
5
  HFS.onEvent('additionalEntryDetails', ({ entry }) =>
5
6
  HFS.h(Uploader, entry))
6
7
 
7
- const cache = {}
8
-
9
8
  function Uploader({ uri }) {
10
- const fullUri = location.pathname + uri
11
- const cachedData = cache[fullUri]
12
- const [freshData, error] = HFS.useApi(!cachedData && 'get_file_details', { uri: fullUri })
13
- if (!cachedData)
14
- cache[fullUri] = freshData || Boolean(error)
15
- const data = freshData || cachedData
16
- const text = HFS.React.useMemo(() => {
9
+ const { data } = HFS.useBatch(getDetails, uri)
10
+ const text = React.useMemo(() => {
17
11
  if (!data || data === true) return ''
18
12
  const { upload: x } = data
19
13
  return !x ? ''
20
14
  : display === 'user' ? x.username
21
15
  : display === 'ip' || !x.username ? x.ip
22
16
  : x.ip + ' (' + x.username + ')'
23
- })
17
+ }, [data])
24
18
  return text && HFS.h('span', { className: 'uploader', title: HFS.t`Uploader` },
25
19
  HFS.hIcon('upload'), ' ', text, ' – ')
26
20
  }
21
+
22
+ function getDetails(batched) {
23
+ return batched.length && HFS.apiCall('get_file_details', { uris: batched }).then(x => x.details)
24
+ }
25
+
27
26
  }
@@ -1,5 +1,5 @@
1
1
  exports.description = "If you want to have different home folders, based on domain"
2
- exports.version = 3.11
2
+ exports.version = 3.12
3
3
  exports.apiRequired = 2 // 2 is for the config 'array'
4
4
 
5
5
  exports.config = {
@@ -44,12 +44,15 @@ exports.init = api => {
44
44
  let { root='' } = row
45
45
  if (root.endsWith('/'))
46
46
  root = root.slice(0, -1)
47
+ if (root && root[0] !== '/') // normalize
48
+ root = '/' + root
49
+ if (!root) return
47
50
  if (!params)
48
51
  ctx.path = root + ctx.path
49
52
  else
50
53
  for (const [k,v] of Object.entries(params))
51
54
  if (k.startsWith('uri'))
52
- params[k] = root + v
55
+ params[k] = Array.isArray(v) ? v.map(x => root + x) : root + v
53
56
  }
54
57
  }
55
58
  }
@@ -51,6 +51,9 @@ class QuickZipStream extends stream_1.Readable {
51
51
  this.skip = 0;
52
52
  this.now = new Date();
53
53
  }
54
+ getArchiveEntries() {
55
+ return this.entries.map(x => String(x.pathAsBuffer));
56
+ }
54
57
  earlyClose() {
55
58
  this.finished = true;
56
59
  this.push(null);
package/src/adminApis.js CHANGED
@@ -51,6 +51,7 @@ const util_1 = require("util");
51
51
  const customHtml_1 = require("./customHtml");
52
52
  const lodash_1 = __importDefault(require("lodash"));
53
53
  const update_1 = require("./update");
54
+ const consoleLog_1 = require("./consoleLog");
54
55
  exports.adminApis = {
55
56
  ...api_vfs_1.default,
56
57
  ...api_accounts_1.default,
@@ -73,8 +74,12 @@ exports.adminApis = {
73
74
  return {};
74
75
  },
75
76
  get_config: config_1.getWholeConfig,
76
- update: update_1.update,
77
- check_update: () => (0, update_1.getUpdate)().then(x => lodash_1.default.pick(x, 'name')),
77
+ update({ tag }) {
78
+ return (0, update_1.update)(tag);
79
+ },
80
+ async check_update() {
81
+ return { options: await (0, update_1.getUpdates)() };
82
+ },
78
83
  get_custom_html() {
79
84
  return {
80
85
  sections: Object.fromEntries([
@@ -101,7 +106,7 @@ exports.adminApis = {
101
106
  ...await (0, listen_1.getServerStatus)(),
102
107
  urls: (0, listen_1.getUrls)(),
103
108
  baseUrl: middlewares_1.baseUrl.get(),
104
- update: !(0, update_1.updateSupported)() ? false : await (0, update_1.localUpdateAvailable)() ? 'local' : true,
109
+ updatePossible: !(0, update_1.updateSupported)() ? false : await (0, update_1.localUpdateAvailable)() ? 'local' : true,
105
110
  proxyDetected: (0, middlewares_1.getProxyDetected)(),
106
111
  frpDetected: exports.localhostAdmin.get() && !(0, middlewares_1.getProxyDetected)()
107
112
  && (0, connections_1.getConnections)().every(misc_1.isLocalHost)
@@ -116,10 +121,20 @@ exports.adminApis = {
116
121
  await (0, promises_1.writeFile)(files.cert, cert);
117
122
  return files;
118
123
  },
119
- async get_log({ file = 'log' }, ctx) {
124
+ get_log({ file = 'log' }, ctx) {
120
125
  return new apiMiddleware_1.SendListReadable({
121
126
  bufferTime: 10,
122
- doAtStart(list) {
127
+ async doAtStart(list) {
128
+ if (file === 'console') {
129
+ for (const chunk of lodash_1.default.chunk(consoleLog_1.consoleLog, 1000)) { // avoid occupying the thread too long
130
+ for (const x of chunk)
131
+ list.add(x);
132
+ await (0, misc_1.wait)(0);
133
+ }
134
+ list.ready();
135
+ events_1.default.on('console', x => list.add(x));
136
+ return;
137
+ }
123
138
  const logger = log_1.loggers.find(l => l.name === file);
124
139
  if (!logger)
125
140
  return list.error(const_1.HTTP_NOT_FOUND, true);
package/src/api.auth.js CHANGED
@@ -7,7 +7,6 @@ const crypt_1 = require("./crypt");
7
7
  const apiMiddleware_1 = require("./apiMiddleware");
8
8
  const tssrp6a_1 = require("tssrp6a");
9
9
  const const_1 = require("./const");
10
- const misc_1 = require("./misc");
11
10
  const api_helpers_1 = require("./api.helpers");
12
11
  const adminApis_1 = require("./adminApis");
13
12
  const middlewares_1 = require("./middlewares");
@@ -22,12 +21,10 @@ async function loggedIn(ctx, username) {
22
21
  return ctx.throw(const_1.HTTP_SERVER_ERROR, 'session');
23
22
  if (username === false) {
24
23
  delete s.username;
25
- ctx.cookies.set('csrf', '');
26
24
  return;
27
25
  }
28
26
  s.username = (0, perm_1.normalizeUsername)(username);
29
27
  await (0, middlewares_1.prepareState)(ctx, async () => { }); // updating the state is necessary to send complete session data so that frontend shows admin button
30
- ctx.cookies.set('csrf', (0, misc_1.randomId)(), { signed: false, httpOnly: false });
31
28
  }
32
29
  function makeExp() {
33
30
  return !keepSessionAlive.get() ? undefined
@@ -39,7 +39,7 @@ const file_list = async ({ uri, offset, limit, search, c, sse }, ctx) => {
39
39
  return { ...props, list: await (0, misc_1.asyncGeneratorToArray)(produceEntries()) };
40
40
  setTimeout(async () => {
41
41
  if (can_upload || can_delete)
42
- list.custom({ props });
42
+ list.props(props);
43
43
  for await (const entry of produceEntries())
44
44
  list.add(entry);
45
45
  list.close();
@@ -103,12 +103,13 @@ async function nodeToDirEntry(ctx, node) {
103
103
  const pr = node.can_read === vfs_1.WHO_NO_ONE && !(folder && filesInsideCould()) ? 'r'
104
104
  : !(0, vfs_1.hasPermission)(node, 'can_read', ctx) ? 'R'
105
105
  : '';
106
+ const pd = (0, vfs_1.hasPermission)(node, 'can_delete', ctx) ? 'd' : '';
106
107
  return {
107
108
  n: name + (folder ? '/' : ''),
108
109
  c: ctime,
109
110
  m: Math.abs(+mtime - +ctime) < 1000 ? undefined : mtime,
110
111
  s: folder ? undefined : st.size,
111
- p: (pr + pl) || undefined
112
+ p: (pr + pl + pd) || undefined
112
113
  };
113
114
  }
114
115
  catch (_a) {
@@ -26,47 +26,47 @@ const apis = {
26
26
  return { result: Boolean(c) };
27
27
  },
28
28
  get_connections({}, ctx) {
29
- const list = new apiMiddleware_1.SendListReadable({ addAtStart: (0, connections_1.getConnections)().map(c => serializeConnection(c)) });
30
- const throttledUpdate = lodash_1.default.throttle(update, 1000 / 20); // try to avoid clogging with updates
31
- const state = Symbol('state'); // undefined=added, Timeout=add-pending, false=removed
29
+ const sent = Symbol('sent');
30
+ const list = new apiMiddleware_1.SendListReadable({
31
+ addAtStart: (0, connections_1.getConnections)().map(c => !ignore(c) && (c[sent] = serializeConnection(c))).filter(Boolean),
32
+ onEnd() {
33
+ for (const c of (0, connections_1.getConnections)())
34
+ delete c[sent];
35
+ }
36
+ });
32
37
  list.props({ you: ctx.ip });
33
38
  return list.events(ctx, {
34
39
  connection(conn) {
35
- conn[state] = setTimeout(() => add(conn), 100);
40
+ if (ignore(conn))
41
+ return;
42
+ list.add(conn[sent] = serializeConnection(conn));
36
43
  },
37
44
  connectionClosed(conn) {
38
- if (cancel(conn))
45
+ if (ignore(conn))
39
46
  return;
40
47
  list.remove(getConnAddress(conn));
41
- conn[state] = false;
48
+ delete conn[sent];
42
49
  },
43
50
  connectionUpdated(conn, change) {
44
- if (!change.ctx)
45
- return throttledUpdate(conn, change);
46
- Object.assign(change, fromCtx(change.ctx));
47
- change.ctx = undefined;
48
- if (!add(conn))
49
- throttledUpdate(conn, change);
51
+ if (ignore(conn) || ignore(change) || !conn[sent])
52
+ return;
53
+ if (change.ctx) {
54
+ Object.assign(change, fromCtx(change.ctx));
55
+ change.ctx = undefined;
56
+ }
57
+ if (change.opProgress)
58
+ change.opProgress = lodash_1.default.round(change.opProgress, 3);
59
+ // avoid sending non-changes
60
+ const last = conn[sent];
61
+ for (const [k, v] of (0, misc_1.typedEntries)(change))
62
+ if (v === last[k])
63
+ delete change[k];
64
+ if (lodash_1.default.isEmpty(change))
65
+ return;
66
+ Object.assign(last, change);
67
+ list.update(getConnAddress(conn), change);
50
68
  },
51
69
  });
52
- function add(conn) {
53
- if (!cancel(conn))
54
- return;
55
- list.add(serializeConnection(conn));
56
- return true;
57
- }
58
- function cancel(conn) {
59
- if (!conn[state])
60
- return;
61
- clearTimeout(conn[state]);
62
- conn[state] = undefined;
63
- return true;
64
- }
65
- function update(conn, change) {
66
- if (conn[state] === false)
67
- return;
68
- list.update(getConnAddress(conn), change);
69
- }
70
70
  function serializeConnection(conn) {
71
71
  var _a;
72
72
  const { socket, started, secure } = conn;
@@ -87,8 +87,8 @@ const apis = {
87
87
  agent: getBrowser(ctx.get('user-agent')),
88
88
  archive: ctx.state.archive,
89
89
  upload: ctx.state.uploadProgress,
90
- path: ctx.state.uploadPath
91
- || (ctx.fileSource || ctx.state.archive) && ctx.path // only uploads and downloads
90
+ path: ctx.state.uploadPath && decodeURIComponent(ctx.state.uploadPath)
91
+ || (ctx.fileSource || ctx.state.archive) && decodeURIComponent(ctx.path) // only uploads and downloads
92
92
  };
93
93
  }
94
94
  },
@@ -99,13 +99,16 @@ const apis = {
99
99
  inSpeed: throttler_1.totalInSpeed,
100
100
  got: throttler_1.totalGot,
101
101
  sent: throttler_1.totalSent,
102
- connections: (0, connections_1.getConnections)().length
102
+ connections: lodash_1.default.sumBy((0, connections_1.getConnections)(), x => ignore(x) ? 0 : 1),
103
103
  };
104
104
  await (0, misc_1.wait)(1000);
105
105
  }
106
106
  },
107
107
  };
108
108
  exports.default = apis;
109
+ function ignore(conn) {
110
+ return false; //conn.socket && isLocalHost(conn)
111
+ }
109
112
  function getConnAddress(conn) {
110
113
  return {
111
114
  ip: conn.ip,
package/src/api.vfs.js CHANGED
@@ -78,7 +78,7 @@ const apis = {
78
78
  const n = await urlToNodeOriginal(uri);
79
79
  if (!n)
80
80
  return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND, 'path not found');
81
- props = pickProps(props, ['name', 'source', 'masks', 'default', 'accept', 'propagate', ...vfs_1.PERM_KEYS]); // sanitize
81
+ props = pickProps(props, ['name', 'source', 'masks', 'default', 'accept', ...vfs_1.PERM_KEYS]); // sanitize
82
82
  if (props.name && props.name !== (0, vfs_1.getNodeName)(n)) {
83
83
  const parent = await urlToNodeOriginal((0, path_1.dirname)(uri));
84
84
  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))
@@ -151,44 +151,47 @@ const apis = {
151
151
  path = (0, path_1.dirname)(path);
152
152
  return { path };
153
153
  },
154
- async *ls({ path, files = true, fileMask }, ctx) {
155
- if (!path && const_1.IS_WINDOWS) {
156
- try {
157
- for (const n of await (0, util_os_1.getDrives)())
158
- yield { add: { n, k: 'd' } };
159
- }
160
- catch (error) {
161
- console.debug(error);
162
- }
163
- return;
164
- }
165
- try {
166
- const matching = (0, misc_1.makeMatcher)(fileMask);
167
- path = (0, misc_1.isWindowsDrive)(path) ? path + '\\' : (0, path_1.resolve)(path || '/');
168
- for await (const [name, isDir] of (0, misc_1.dirStream)(path)) {
169
- if (ctx.req.aborted)
154
+ ls({ path, files = true, fileMask }, ctx) {
155
+ return new apiMiddleware_1.SendListReadable({
156
+ async doAtStart(list) {
157
+ if (!path && const_1.IS_WINDOWS) {
158
+ try {
159
+ for (const n of await (0, util_os_1.getDrives)())
160
+ list.add({ n, k: 'd' });
161
+ }
162
+ catch (error) {
163
+ console.debug(error);
164
+ }
170
165
  return;
166
+ }
171
167
  try {
172
- if (!isDir)
173
- if (!files || fileMask && !matching(name))
174
- continue;
175
- const stats = await (0, promises_1.stat)((0, path_1.join)(path, name));
176
- yield {
177
- add: {
178
- n: name,
179
- s: stats.size,
180
- c: stats.ctime,
181
- m: stats.mtime,
182
- k: isDir ? 'd' : undefined,
168
+ const matching = (0, misc_1.makeMatcher)(fileMask);
169
+ path = (0, misc_1.isWindowsDrive)(path) ? path + '\\' : (0, path_1.resolve)(path || '/');
170
+ for await (const [name, isDir] of (0, misc_1.dirStream)(path)) {
171
+ if (ctx.req.aborted)
172
+ return;
173
+ if (!isDir)
174
+ if (!files || fileMask && !matching(name))
175
+ continue;
176
+ try {
177
+ const stats = await (0, promises_1.stat)((0, path_1.join)(path, name));
178
+ list.add({
179
+ n: name,
180
+ s: stats.size,
181
+ c: stats.ctime,
182
+ m: stats.mtime,
183
+ k: isDir ? 'd' : undefined,
184
+ });
183
185
  }
184
- };
186
+ catch (_a) { } // just ignore entries we can't stat
187
+ }
188
+ list.close();
189
+ }
190
+ catch (e) {
191
+ list.error(e.code || e.message || String(e), true);
185
192
  }
186
- catch (_a) { } // just ignore entries we can't stat
187
193
  }
188
- }
189
- catch (e) {
190
- yield { error: e.code || e.message || String(e) };
191
- }
194
+ });
192
195
  }
193
196
  };
194
197
  exports.default = apis;
@@ -35,11 +35,18 @@ function apiMiddleware(apis) {
35
35
  // we don't rely on SameSite cookie option because it's https-only
36
36
  let res;
37
37
  try {
38
- for (const [k, v] of Object.entries(params))
39
- if (k.startsWith('uri') && typeof v === 'string')
40
- params[k] = (0, misc_1.removeStarting)(ctx.state.revProxyPath, v);
38
+ if (ctx.state.revProxyPath)
39
+ for (const [k, v] of Object.entries(params))
40
+ if (k.startsWith('uri'))
41
+ if (typeof v === 'string')
42
+ fixUri(params, k);
43
+ else if (typeof (v === null || v === void 0 ? void 0 : v[0]) === 'string')
44
+ v.forEach((x, i) => fixUri(v, i));
41
45
  res = csrf && csrf !== params.csrf ? new ApiError(const_1.HTTP_UNAUTHORIZED, 'csrf')
42
46
  : await apiFun(params || {}, ctx);
47
+ function fixUri(o, k) {
48
+ o[k] = (0, misc_1.removeStarting)(ctx.state.revProxyPath, o[k]);
49
+ }
43
50
  }
44
51
  catch (e) {
45
52
  res = e;
@@ -69,18 +76,20 @@ function isAsyncGenerator(x) {
69
76
  return typeof (x === null || x === void 0 ? void 0 : x.next) === 'function';
70
77
  }
71
78
  class SendListReadable extends stream_1.Readable {
72
- constructor({ addAtStart, doAtStart, bufferTime } = {}) {
79
+ constructor({ addAtStart, doAtStart, bufferTime, onEnd } = {}) {
73
80
  super({ objectMode: true, read() { } });
74
81
  this.buffer = [];
75
82
  if (!bufferTime)
76
- bufferTime = 100;
83
+ bufferTime = 200;
77
84
  this.processBuffer = lodash_1.default.debounce(() => {
78
85
  this.push(this.buffer);
79
86
  this.buffer = [];
80
87
  }, bufferTime, { maxWait: bufferTime });
81
- this.on('end', () => this.destroy());
82
- if (doAtStart)
83
- setTimeout(() => doAtStart(this)); // work later, when list object has been received by Koa
88
+ this.on('end', () => {
89
+ onEnd === null || onEnd === void 0 ? void 0 : onEnd(this);
90
+ this.destroy();
91
+ });
92
+ setTimeout(() => doAtStart === null || doAtStart === void 0 ? void 0 : doAtStart(this)); // work later, when list object has been received by Koa
84
93
  if (addAtStart) {
85
94
  for (const x of addAtStart)
86
95
  this.add(x);
@@ -95,25 +104,45 @@ class SendListReadable extends stream_1.Readable {
95
104
  this.processBuffer();
96
105
  }
97
106
  add(rec) {
98
- this._push({ add: rec });
107
+ this._push(['add', rec]);
99
108
  }
100
- remove(key) {
101
- this._push({ remove: [key] });
109
+ remove(search) {
110
+ const match = lodash_1.default.matches(search);
111
+ const idx = lodash_1.default.findIndex(this.buffer, x => match(x[1]));
112
+ const found = this.buffer[idx];
113
+ const op = found === null || found === void 0 ? void 0 : found[0];
114
+ if (op === 'remove')
115
+ return;
116
+ if (found) {
117
+ this.buffer.splice(idx, 1);
118
+ if (op === 'add')
119
+ return;
120
+ }
121
+ this._push(['remove', search]);
102
122
  }
103
123
  update(search, change) {
104
- this._push({ update: [{ search, change }] });
124
+ if (lodash_1.default.isEmpty(change))
125
+ return;
126
+ const match = lodash_1.default.matches(search);
127
+ const found = lodash_1.default.find(this.buffer, x => match(x[1]));
128
+ const op = found === null || found === void 0 ? void 0 : found[0];
129
+ if (op === 'remove')
130
+ return;
131
+ if (op === 'add' || op === 'update')
132
+ return Object.assign(found[op === 'add' ? 1 : 2], change);
133
+ return this._push(['update', search, change]);
105
134
  }
106
135
  ready() {
107
- this._push('ready');
136
+ this._push(['ready']);
108
137
  }
109
138
  custom(data) {
110
139
  this._push(data);
111
140
  }
112
141
  props(props) {
113
- this._push({ props });
142
+ this._push(['props', props]);
114
143
  }
115
144
  error(msg, close = false, props) {
116
- this._push({ error: msg, ...props });
145
+ this._push(['error', msg, props]);
117
146
  this.lastError = msg;
118
147
  if (close)
119
148
  this.close();
package/src/commands.js CHANGED
@@ -111,8 +111,8 @@ const commands = {
111
111
  'check-update': {
112
112
  params: '',
113
113
  async cb() {
114
- const update = await (0, update_1.getUpdate)();
115
- if (update.name === const_1.VERSION)
114
+ const [update] = await (0, update_1.getUpdates)();
115
+ if (!update)
116
116
  throw "you already have the latest version: " + const_1.VERSION;
117
117
  console.log("new version available", update.name);
118
118
  }