hfs 0.56.0 → 0.57.0-beta2

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 (67) hide show
  1. package/admin/assets/{index-DlA4sSmp.js → index-Cl7PJ7_4.js} +263 -263
  2. package/admin/assets/{sha512--f9T-TYW.js → sha512-DwXzavpM.js} +1 -1
  3. package/admin/index.html +14 -2
  4. package/frontend/assets/index-legacy-0ZDBWQI9.js +50 -0
  5. package/frontend/assets/{sha512-legacy-CX3vTVvC.js → sha512-legacy-slBG5k4n.js} +1 -1
  6. package/frontend/index.html +1 -1
  7. package/package.json +2 -2
  8. package/plugins/antibrute/plugin.js +25 -27
  9. package/src/SendList.js +2 -2
  10. package/src/acme.js +1 -2
  11. package/src/adminApis.js +1 -0
  12. package/src/api.accounts.js +3 -0
  13. package/src/api.auth.js +27 -4
  14. package/src/api.get_file_list.js +2 -6
  15. package/src/api.log.js +9 -4
  16. package/src/api.monitor.js +1 -1
  17. package/src/api.plugins.js +13 -0
  18. package/src/apiMiddleware.js +1 -1
  19. package/src/auth.js +17 -1
  20. package/src/comments.js +31 -8
  21. package/src/config.js +2 -0
  22. package/src/connections.js +16 -5
  23. package/src/const.js +4 -4
  24. package/src/cross-const.js +2 -1
  25. package/src/cross.js +13 -3
  26. package/src/customHtml.js +4 -2
  27. package/src/events.js +7 -5
  28. package/src/fileAttr.js +10 -5
  29. package/src/frontEndApis.js +0 -12
  30. package/src/geo.js +1 -0
  31. package/src/langs/hfs-lang-ar.json +4 -1
  32. package/src/langs/hfs-lang-de.json +4 -1
  33. package/src/langs/hfs-lang-el.json +4 -1
  34. package/src/langs/hfs-lang-es.json +4 -1
  35. package/src/langs/hfs-lang-fi.json +4 -1
  36. package/src/langs/hfs-lang-fr.json +4 -1
  37. package/src/langs/hfs-lang-hu.json +4 -1
  38. package/src/langs/hfs-lang-it.json +4 -1
  39. package/src/langs/hfs-lang-ja.json +4 -1
  40. package/src/langs/hfs-lang-ko.json +4 -1
  41. package/src/langs/hfs-lang-ms.json +4 -1
  42. package/src/langs/hfs-lang-nl.json +4 -1
  43. package/src/langs/hfs-lang-pt-br.json +4 -1
  44. package/src/langs/hfs-lang-ro.json +4 -1
  45. package/src/langs/hfs-lang-ru.json +16 -13
  46. package/src/langs/hfs-lang-sr.json +4 -1
  47. package/src/langs/hfs-lang-th.json +4 -1
  48. package/src/langs/hfs-lang-tr.json +7 -1
  49. package/src/langs/hfs-lang-uk.json +4 -1
  50. package/src/langs/hfs-lang-vi.json +8 -5
  51. package/src/langs/hfs-lang-zh-tw.json +4 -1
  52. package/src/langs/hfs-lang-zh.json +4 -1
  53. package/src/listen.js +3 -2
  54. package/src/log.js +10 -3
  55. package/src/middlewares.js +7 -23
  56. package/src/misc.js +6 -35
  57. package/src/perm.js +5 -3
  58. package/src/plugins.js +44 -4
  59. package/src/roots.js +1 -3
  60. package/src/serveGuiAndSharedFiles.js +18 -10
  61. package/src/serveGuiFiles.js +2 -4
  62. package/src/throttler.js +3 -2
  63. package/src/update.js +1 -1
  64. package/src/upload.js +1 -1
  65. package/src/vfs.js +1 -1
  66. package/README.md +0 -177
  67. package/frontend/assets/index-legacy-3T51dA5w.js +0 -50
package/src/log.js CHANGED
@@ -88,7 +88,7 @@ const logUA = (0, config_1.defineConfig)(misc_1.CFG.log_ua, false);
88
88
  const logSpam = (0, config_1.defineConfig)(misc_1.CFG.log_spam, false);
89
89
  const debounce = lodash_1.default.debounce(cb => cb(), 1000); // with this technique, i'll be able to debounce some code respecting the references in its closure
90
90
  const logMw = async (ctx, next) => {
91
- const now = new Date();
91
+ const now = new Date(); // request start
92
92
  // do it now so it's available for returning plugins
93
93
  ctx.state.completed = Promise.race([(0, stream_1.once)(ctx.res, 'finish'), (0, stream_1.once)(ctx.res, 'close')]);
94
94
  await next();
@@ -140,8 +140,15 @@ const logMw = async (ctx, next) => {
140
140
  const user = (0, auth_1.getCurrentUsername)(ctx);
141
141
  const length = (_c = ctx.state.length) !== null && _c !== void 0 ? _c : ctx.length;
142
142
  const uri = ctx.originalUrl;
143
- ctx.logExtra(ctx.state.includesLastByte && ctx.vfsNode && ctx.res.finished && { dl: 1 }
144
- || ctx.state.uploadPath && { size: ctx.state.opTotal, ul: ctx.state.uploads });
143
+ const duration = (Date.now() - Number(now)) / 1000;
144
+ ctx.logExtra(ctx.vfsNode && {
145
+ speed: Math.round(length / duration),
146
+ ...ctx.state.includesLastByte && ctx.res.finished && { dl: 1 }
147
+ } || ctx.state.uploadPath && {
148
+ ul: ctx.state.uploads,
149
+ size: ctx.state.opTotal,
150
+ speed: Math.round((ctx.state.opTotal - (ctx.state.opOffset || 0)) / duration)
151
+ });
145
152
  if (conn === null || conn === void 0 ? void 0 : conn.country)
146
153
  ctx.logExtra({ country: conn.country });
147
154
  if (logUA.get())
@@ -111,42 +111,26 @@ const prepareState = async (ctx, next) => {
111
111
  ctx.state.revProxyPath = ctx.get('x-forwarded-prefix');
112
112
  (0, connections_1.updateConnectionForCtx)(ctx);
113
113
  await next();
114
- async function urlLogin() {
114
+ function urlLogin() {
115
115
  const { login } = ctx.query;
116
116
  if (!login)
117
117
  return;
118
118
  const [u, p] = (0, misc_1.splitAt)(':', String(login));
119
- const a = await doLogin(u, p, 'url');
120
- if (!a)
121
- return;
122
119
  ctx.redirect(ctx.originalUrl.slice(0, -ctx.querystring.length - 1)); // redirect to hide credentials
123
- return a;
120
+ return u && (0, auth_1.clearTextLogin)(ctx, u, p, 'url');
124
121
  }
125
122
  function getHttpAccount() {
126
- var _a;
123
+ var _a, _b;
127
124
  const b64 = allowAuthorizationHeader.get() && ((_a = ctx.get('authorization')) === null || _a === void 0 ? void 0 : _a.split(' ')[1]);
128
125
  if (!b64)
129
126
  return;
130
127
  try {
131
128
  const [u, p] = atob(b64).split(':');
132
- return doLogin(u, p || '', 'header');
133
- }
134
- catch (_b) { }
135
- }
136
- async function doLogin(u, p, via) {
137
- var _a, _b;
138
- if (!u || u === ((_a = ctx.session) === null || _a === void 0 ? void 0 : _a.username))
139
- return; // providing credentials, but not needed
140
- if ((_b = (await events_1.default.emitAsync('attemptingLogin', { ctx, username: u, via }))) === null || _b === void 0 ? void 0 : _b.isDefaultPrevented())
141
- return;
142
- const a = await (0, auth_1.srpCheck)(u, p);
143
- if (a) {
144
- await (0, auth_1.setLoggedIn)(ctx, a.username);
145
- ctx.headers['x-username'] = a.username; // give an easier way to determine if the login was successful
129
+ if (!u || u === ((_b = ctx.session) === null || _b === void 0 ? void 0 : _b.username))
130
+ return; // providing credentials, but not needed
131
+ return (0, auth_1.clearTextLogin)(ctx, u, p || '', 'header');
146
132
  }
147
- else if (u)
148
- events_1.default.emit('failedLogin', ctx, { username: u, via });
149
- return a;
133
+ catch (_c) { }
150
134
  }
151
135
  };
152
136
  exports.prepareState = prepareState;
package/src/misc.js CHANGED
@@ -18,9 +18,8 @@ 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.deleteNode = exports.createStreamLimiter = exports.apiAssertTypes = exports.AsapStream = exports.asyncGeneratorToReadable = exports.same = exports.makeNetMatcher = exports.netMatches = exports.isLocalHost = exports.pattern2filter = void 0;
21
+ exports.createStreamLimiter = exports.apiAssertTypes = exports.AsapStream = exports.asyncGeneratorToReadable = exports.same = exports.makeNetMatcher = exports.netMatches = exports.isLocalHost = exports.pattern2filter = void 0;
22
22
  const path_1 = require("path");
23
- const assert_1 = __importDefault(require("assert"));
24
23
  __exportStar(require("./util-http"), exports);
25
24
  __exportStar(require("./util-files"), exports);
26
25
  __exportStar(require("./fileAttr"), exports);
@@ -32,10 +31,6 @@ const apiMiddleware_1 = require("./apiMiddleware");
32
31
  const const_1 = require("./const");
33
32
  const cross_1 = require("./cross");
34
33
  const net_1 = require("net");
35
- const vfs_1 = require("./vfs");
36
- const events_1 = __importDefault(require("./events"));
37
- const promises_1 = require("fs/promises");
38
- const comments_1 = require("./comments");
39
34
  const lodash_1 = __importDefault(require("lodash"));
40
35
  function pattern2filter(pattern) {
41
36
  const matcher = (0, cross_1.makeMatcher)(pattern.includes('*') ? pattern // if you specify *, we'll respect its position
@@ -48,9 +43,11 @@ function isLocalHost(c) {
48
43
  return ip && (0, cross_1.isIpLocalHost)(ip);
49
44
  }
50
45
  exports.isLocalHost = isLocalHost;
51
- // this will memory-leak over mask, so be careful with what you use this
46
+ // this will memory-leak over mask, so be careful with what you use this. Object is 3x faster than _.memoize
52
47
  function netMatches(ip, mask, emptyMaskReturns = false) {
53
- return lodash_1.default.memoize(makeNetMatcher, (a, b) => `${a}\t${b ? 1 : 0}`)(mask, emptyMaskReturns)(ip); // cache the matcher
48
+ var _a, _b;
49
+ const cache = (_a = netMatches).cache || (_a.cache = {});
50
+ return (cache[_b = mask + (emptyMaskReturns ? '1' : '0')] || (cache[_b] = makeNetMatcher(mask, emptyMaskReturns)))(ip); // cache the matcher
54
51
  }
55
52
  exports.netMatches = netMatches;
56
53
  function makeNetMatcher(mask, emptyMaskReturns = false) {
@@ -108,13 +105,7 @@ function parseAddress(s) {
108
105
  return new node_net_1.SocketAddress({ address: s, family: (0, net_1.isIPv6)(s) ? 'ipv6' : 'ipv4' });
109
106
  }
110
107
  function same(a, b) {
111
- try {
112
- assert_1.default.deepStrictEqual(a, b);
113
- return true;
114
- }
115
- catch (_a) {
116
- return false;
117
- }
108
+ return lodash_1.default.isEqual(a, b);
118
109
  }
119
110
  exports.same = same;
120
111
  function asyncGeneratorToReadable(generator) {
@@ -175,23 +166,3 @@ function createStreamLimiter(limit) {
175
166
  });
176
167
  }
177
168
  exports.createStreamLimiter = createStreamLimiter;
178
- async function deleteNode(ctx, node, uri) {
179
- var _a;
180
- const { source } = node;
181
- if (!source)
182
- return const_1.HTTP_METHOD_NOT_ALLOWED;
183
- if ((0, vfs_1.statusCodeForMissingPerm)(node, 'can_delete', ctx))
184
- return ctx.status;
185
- try {
186
- if ((_a = (await events_1.default.emitAsync('deleting', { node, ctx }))) === null || _a === void 0 ? void 0 : _a.isDefaultPrevented())
187
- return null; // stop
188
- ctx.logExtra(null, { target: decodeURI(uri) });
189
- await (0, promises_1.rm)(source, { recursive: true });
190
- void (0, comments_1.setCommentFor)(source, '');
191
- return true;
192
- }
193
- catch (e) {
194
- return e;
195
- }
196
- }
197
- exports.deleteNode = deleteNode;
package/src/perm.js CHANGED
@@ -30,7 +30,8 @@ function expandUsername(who) {
30
30
  exports.expandUsername = expandUsername;
31
31
  // check if current username or any ancestor match the provided usernames
32
32
  function ctxBelongsTo(ctx, usernames) {
33
- return (0, misc_1.getOrSet)(ctx.state, 'usernames', () => expandUsername((0, auth_1.getCurrentUsername)(ctx))) // cache ancestors' usernames inside context state
33
+ var _a;
34
+ return ((_a = ctx.state).usernames || (_a.usernames = expandUsername((0, auth_1.getCurrentUsername)(ctx)))) // cache ancestors' usernames inside context state
34
35
  .some((u) => usernames.includes(u));
35
36
  }
36
37
  exports.ctxBelongsTo = ctxBelongsTo;
@@ -181,7 +182,8 @@ function accountHasPassword(account) {
181
182
  }
182
183
  exports.accountHasPassword = accountHasPassword;
183
184
  function accountCanLogin(account) {
184
- return accountHasPassword(account) && !allDisabled(account);
185
+ var _a;
186
+ return (accountHasPassword(account) || ((_a = account.plugin) === null || _a === void 0 ? void 0 : _a.auth)) && !allDisabled(account);
185
187
  }
186
188
  exports.accountCanLogin = accountCanLogin;
187
189
  function allDisabled(account) {
@@ -193,7 +195,7 @@ function allDisabled(account) {
193
195
  && account.belongs.map(u => getAccount(u, false)).every(a => a && allDisabled(a)));
194
196
  }
195
197
  function accountCanLoginAdmin(account) {
196
- return accountCanLogin(account) && Boolean(getFromAccount(account, a => a.admin));
198
+ return accountCanLogin(account) && getFromAccount(account, a => a.admin) || false;
197
199
  }
198
200
  exports.accountCanLoginAdmin = accountCanLoginAdmin;
199
201
  async function changeSrpHelper(account, salt, verifier) {
package/src/plugins.js CHANGED
@@ -133,14 +133,33 @@ function getPluginConfigFields(id) {
133
133
  exports.getPluginConfigFields = getPluginConfigFields;
134
134
  async function initPlugin(pl, morePassedToInit) {
135
135
  var _a;
136
+ const undoEvents = [];
137
+ const timeouts = [];
136
138
  const res = await ((_a = pl.init) === null || _a === void 0 ? void 0 : _a.call(pl, {
137
139
  Const,
138
140
  require,
139
141
  getConnections: connections_1.getConnections,
140
- events: events_1.default,
142
+ // intercept all subscriptions, so to be able to undo them on unload
143
+ events: Object.create(events_1.default, (0, misc_1.objFromKeys)(['on', 'once', 'multi'], k => ({
144
+ value() {
145
+ const ret = events_1.default[k](...arguments);
146
+ undoEvents.push(ret);
147
+ return ret;
148
+ }
149
+ }))),
141
150
  log: console.log,
142
151
  setError(msg) { setError((morePassedToInit === null || morePassedToInit === void 0 ? void 0 : morePassedToInit.id) || 'server_code', msg); },
143
152
  getHfsConfig: config_1.getConfig,
153
+ setInterval() {
154
+ const ret = setInterval(...arguments);
155
+ timeouts.push(ret); // intervals can be canceled by clearTimeout (source: MDN)
156
+ return ret;
157
+ },
158
+ setTimeout() {
159
+ const ret = setTimeout(...arguments);
160
+ timeouts.push(ret);
161
+ return ret;
162
+ },
144
163
  customApiCall,
145
164
  notifyClient: frontEndApis_1.notifyClient,
146
165
  addBlock: block_1.addBlock,
@@ -152,6 +171,14 @@ async function initPlugin(pl, morePassedToInit) {
152
171
  ...morePassedToInit
153
172
  }));
154
173
  Object.assign(pl, typeof res === 'function' ? { unload: res } : res);
174
+ (0, misc_1.patchKey)(pl, 'unload', was => () => {
175
+ for (const x of timeouts)
176
+ clearTimeout(x);
177
+ for (const cb of undoEvents)
178
+ cb();
179
+ if (typeof was === 'function')
180
+ return was(...arguments);
181
+ });
155
182
  events_1.default.emit('pluginInitialized', pl);
156
183
  return pl;
157
184
  }
@@ -233,6 +260,7 @@ class Plugin {
233
260
  this.started = new Date();
234
261
  if (!data)
235
262
  throw 'invalid data';
263
+ this.log = [];
236
264
  this.data = data = { ...data }; // clone to make object modifiable. Objects coming from import are not.
237
265
  // some validation
238
266
  for (const k of ['frontend_css', 'frontend_js']) {
@@ -479,6 +507,7 @@ function watchPlugin(id, path) {
479
507
  await (0, promises_1.mkdir)(storageDir, { recursive: true });
480
508
  const openDbs = [];
481
509
  const subbedConfigs = [];
510
+ const pluginReady = (0, misc_1.pendingPromise)();
482
511
  await initPlugin(pluginData, {
483
512
  id,
484
513
  srcDir: __dirname,
@@ -493,6 +522,14 @@ function watchPlugin(id, path) {
493
522
  },
494
523
  log(...args) {
495
524
  console.log('plugin', id + ':', ...args);
525
+ pluginReady.then(() => {
526
+ if (!plugin)
527
+ return;
528
+ plugin.log.unshift({ ts: new Date, msg: args.map(x => x && typeof x === 'object' ? JSON.stringify(x) : String(x)).join(' ') });
529
+ plugin.log.length = Math.min(100, plugin.log.length); // truncate
530
+ events_1.default.emit('pluginLog:' + id, plugin.log[0]);
531
+ events_1.default.emit('pluginLog', id, plugin.log[0]);
532
+ });
496
533
  },
497
534
  getConfig(cfgKey) {
498
535
  var _a, _b, _c, _d;
@@ -536,6 +573,7 @@ function watchPlugin(id, path) {
536
573
  await Promise.allSettled(openDbs.map(x => x.close()));
537
574
  openDbs.length = 0;
538
575
  });
576
+ pluginReady.resolve();
539
577
  if (alreadyRunning)
540
578
  events_1.default.emit('pluginUpdated', Object.assign(lodash_1.default.pick(plugin, 'started'), getPluginInfo(id)));
541
579
  else {
@@ -582,13 +620,14 @@ function setError(id, error) {
582
620
  }
583
621
  function deleteModule(id) {
584
622
  var _a;
623
+ var _b;
585
624
  const { cache } = require;
586
625
  // build reversed map of dependencies
587
626
  const requiredBy = { '.': ['.'] }; // don't touch main entry
588
627
  for (const k in cache)
589
628
  if (k !== id)
590
629
  for (const child of (0, misc_1.wantArray)((_a = cache[k]) === null || _a === void 0 ? void 0 : _a.children))
591
- (0, misc_1.getOrSet)(requiredBy, child.id, () => []).push(k);
630
+ (requiredBy[_b = child.id] || (requiredBy[_b] = [])).push(k);
592
631
  const deleted = [];
593
632
  (function deleteCache(id) {
594
633
  const mod = cache[id];
@@ -603,7 +642,7 @@ function deleteModule(id) {
603
642
  }
604
643
  (0, first_1.onProcessExit)(() => Promise.allSettled(mapPlugins(pl => pl.unload())));
605
644
  function parsePluginSource(id, source) {
606
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
645
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
607
646
  const pl = { id };
608
647
  pl.description = (0, misc_1.tryJson)((_a = /exports.description *= *(".*")/.exec(source)) === null || _a === void 0 ? void 0 : _a[1]);
609
648
  pl.repo = (0, misc_1.tryJson)((_b = /exports.repo *= *(.*);? *$/m.exec(source)) === null || _b === void 0 ? void 0 : _b[1]);
@@ -611,8 +650,9 @@ function parsePluginSource(id, source) {
611
650
  pl.apiRequired = (_f = (0, misc_1.tryJson)((_e = /exports.apiRequired *= *([ \d.,[\]]+)/.exec(source)) === null || _e === void 0 ? void 0 : _e[1])) !== null && _f !== void 0 ? _f : undefined;
612
651
  pl.isTheme = (_h = (0, misc_1.tryJson)((_g = /exports.isTheme *= *(true|false|"light"|"dark")/.exec(source)) === null || _g === void 0 ? void 0 : _g[1])) !== null && _h !== void 0 ? _h : (id.endsWith('-theme') || undefined);
613
652
  pl.preview = (_k = (0, misc_1.tryJson)((_j = /exports.preview *= *(.+)/.exec(source)) === null || _j === void 0 ? void 0 : _j[1])) !== null && _k !== void 0 ? _k : undefined;
614
- pl.depend = (_m = (0, misc_1.tryJson)((_l = /exports.depend *= *(\[.*])/m.exec(source)) === null || _l === void 0 ? void 0 : _l[1])) === null || _m === void 0 ? void 0 : _m.filter((x) => typeof x.repo === 'string' && x.version === undefined || typeof x.version === 'number'
653
+ pl.depend = (_m = (0, misc_1.tryJson)((_l = /exports.depend *= *(\[[\s\S]*?])/m.exec(source)) === null || _l === void 0 ? void 0 : _l[1])) === null || _m === void 0 ? void 0 : _m.filter((x) => typeof x.repo === 'string' && x.version === undefined || typeof x.version === 'number'
615
654
  || console.warn("plugin dependency discarded", x));
655
+ pl.changelog = (0, misc_1.tryJson)((_o = /exports.changelog *= *(\[[\s\S]*?])/m.exec(source)) === null || _o === void 0 ? void 0 : _o[1]);
616
656
  if (Array.isArray(pl.apiRequired) && (pl.apiRequired.length !== 2 || !pl.apiRequired.every(lodash_1.default.isFinite))) // validate [from,to] form
617
657
  pl.apiRequired = undefined;
618
658
  calculateBadApi(pl);
package/src/roots.js CHANGED
@@ -42,9 +42,7 @@ const rootsMiddleware = (ctx, next) => (() => {
42
42
  return true; // true will avoid calling next
43
43
  }
44
44
  if (!root || root === '/')
45
- return; // not transformation is required
46
- if (root === '' || root === '/')
47
- return;
45
+ return; // no transformation is required
48
46
  changeUriParams(v => (0, misc_1.join)(root, v));
49
47
  if (!params)
50
48
  ctx.path = (0, misc_1.join)(root, ctx.path);
@@ -20,6 +20,8 @@ const serveGuiFiles_1 = require("./serveGuiFiles");
20
20
  const koa_mount_1 = __importDefault(require("koa-mount"));
21
21
  const listen_1 = require("./listen");
22
22
  const misc_1 = require("./misc");
23
+ const promises_1 = require("fs/promises");
24
+ const comments_1 = require("./comments");
23
25
  const basicWeb_1 = require("./basicWeb");
24
26
  const icons_1 = require("./icons");
25
27
  const plugins_1 = require("./plugins");
@@ -28,7 +30,7 @@ const serveFrontendPrefixed = (0, koa_mount_1.default)(cross_const_1.FRONTEND_UR
28
30
  const serveAdminFiles = (0, serveGuiFiles_1.serveGuiFiles)(process.env.ADMIN_PROXY, cross_const_1.ADMIN_URI);
29
31
  const serveAdminPrefixed = (0, koa_mount_1.default)(cross_const_1.ADMIN_URI.slice(0, -1), serveAdminFiles);
30
32
  const serveGuiAndSharedFiles = async (ctx, next) => {
31
- var _a;
33
+ var _a, _b;
32
34
  const { path } = ctx;
33
35
  // dynamic import on frontend|admin (used for non-https login) while developing (vite4) is not producing a relative path
34
36
  if (const_1.DEV && path.startsWith('/node_modules/')) {
@@ -108,16 +110,22 @@ const serveGuiAndSharedFiles = async (ctx, next) => {
108
110
  return;
109
111
  }
110
112
  if (ctx.method === 'DELETE') {
111
- const res = await (0, misc_1.deleteNode)(ctx, node, ctx.path);
112
- if (typeof res === 'number')
113
- return ctx.status = res;
114
- if (res instanceof Error) {
115
- ctx.body = res.message || String(res);
113
+ const { source } = node;
114
+ if (!source)
115
+ return ctx.status = cross_const_1.HTTP_METHOD_NOT_ALLOWED;
116
+ if ((0, vfs_1.statusCodeForMissingPerm)(node, 'can_delete', ctx))
117
+ return;
118
+ try {
119
+ if ((_b = (await events_1.default.emitAsync('deleting', { node, ctx }))) === null || _b === void 0 ? void 0 : _b.isDefaultPrevented())
120
+ return ctx.status = cross_const_1.HTTP_FAILED_DEPENDENCY;
121
+ await (0, promises_1.rm)(source, { recursive: true });
122
+ void (0, comments_1.setCommentFor)(source, ''); // necessary only to clean a possible descript.ion or kvstorage
123
+ return ctx.status = cross_const_1.HTTP_OK;
124
+ }
125
+ catch (e) {
126
+ ctx.body = String(e);
116
127
  return ctx.status = cross_const_1.HTTP_SERVER_ERROR;
117
128
  }
118
- if (res)
119
- return ctx.status = cross_const_1.HTTP_OK;
120
- return;
121
129
  }
122
130
  const { get } = ctx.query;
123
131
  if (node.default && path.endsWith('/') && !get) { // final/ needed on browser to make resource urls correctly with html pages
@@ -164,7 +172,7 @@ async function sendFolderList(node, ctx) {
164
172
  const { URL } = ctx;
165
173
  const base = prepend === undefined && listen_1.baseUrl.get()
166
174
  || URL.protocol + '//' + URL.host + ctx.state.revProxyPath;
167
- prepend = base + ctx.originalUrl.split('?')[0];
175
+ prepend = base + (0, misc_1.pathEncode)(decodeURI(ctx.path)); // redo the encoding our way, keeping unicode chars unchanged
168
176
  }
169
177
  const walker = (0, vfs_1.walkNode)(node, { ctx, depth: depth === '*' ? Infinity : Number(depth) });
170
178
  ctx.body = (0, misc_1.asyncGeneratorToReadable)((0, misc_1.filterMapGenerator)(walker, async (el) => {
@@ -24,10 +24,8 @@ const size1024 = (0, config_1.defineConfig)(misc_1.CFG.size_1024, false, x => mi
24
24
  const splitUploads = (0, config_1.defineConfig)(misc_1.CFG.split_uploads, 0);
25
25
  exports.logGui = (0, config_1.defineConfig)(misc_1.CFG.log_gui, false);
26
26
  lodash_1.default.each(misc_1.FRONTEND_OPTIONS, (v, k) => (0, config_1.defineConfig)(k, v)); // define default values
27
- // in case of dev env we have our static files within the 'dist' folder'
28
- const DEV_STATIC = process.env.DEV ? 'dist/' : '';
29
27
  function serveStatic(uri) {
30
- const folder = uri.slice(2, -1); // we know folder is very similar to uri
28
+ const folder = (const_1.DEV ? 'dist/' : '') + uri.slice(2, -1); // we know folder is very similar to uri
31
29
  let cache = {};
32
30
  customHtml_1.customHtml.emitter.on('change', () => cache = {}); // reset cache at every change
33
31
  return async (ctx) => {
@@ -41,7 +39,7 @@ function serveStatic(uri) {
41
39
  if (ctx.method !== 'GET')
42
40
  return ctx.status = const_1.HTTP_METHOD_NOT_ALLOWED;
43
41
  const serveApp = shouldServeApp(ctx);
44
- const fullPath = (0, path_1.join)(__dirname, '..', DEV_STATIC, folder, serveApp ? '/index.html' : ctx.path);
42
+ const fullPath = (0, path_1.join)(__dirname, '..', folder, serveApp ? '/index.html' : ctx.path);
45
43
  const content = await (0, misc_1.parseFileContent)(fullPath, raw => serveApp || !raw.length ? raw : adjustBundlerLinks(ctx, uri, raw))
46
44
  .catch(() => null);
47
45
  if (content === null)
package/src/throttler.js CHANGED
@@ -26,6 +26,7 @@ maxKbpsPerIp.sub(v => {
26
26
  });
27
27
  const throttler = async (ctx, next) => {
28
28
  var _a;
29
+ var _b;
29
30
  await next();
30
31
  let { body } = ctx;
31
32
  const downloadTotal = ctx.response.length;
@@ -35,10 +36,10 @@ const throttler = async (ctx, next) => {
35
36
  return;
36
37
  // we wrap the stream also for unlimited connections to get speed and other features
37
38
  const noLimit = ((_a = ctx.state.account) === null || _a === void 0 ? void 0 : _a.ignore_limits) || (0, misc_1.isLocalHost)(ctx);
38
- const ipGroup = (0, misc_1.getOrSet)(ip2group, noLimit ? '' : ctx.ip, () => ({
39
+ const ipGroup = ip2group[_b = noLimit ? '' : ctx.ip] || (ip2group[_b] = {
39
40
  count: 0,
40
41
  group: new ThrottledStream_1.ThrottleGroup(noLimit ? Infinity : maxKbpsPerIp.get(), noLimit ? undefined : mainThrottleGroup),
41
- }));
42
+ });
42
43
  const conn = (0, connections_1.getConnection)(ctx);
43
44
  if (!conn)
44
45
  throw 'assert throttler connection';
package/src/update.js CHANGED
@@ -125,7 +125,7 @@ async function update(tagOrUrl = '') {
125
125
  const asset = update.assets.find((x) => x.name.includes(assetSearch) && x.name.endsWith('.zip'))
126
126
  || update.assets.find((x) => x.name.endsWith(legacyAssetSearch));
127
127
  if (!asset)
128
- throw "asset not found";
128
+ throw `asset not found: ${assetSearch}`;
129
129
  url = asset.browser_download_url;
130
130
  }
131
131
  if (url) {
package/src/upload.js CHANGED
@@ -105,7 +105,7 @@ function uploadWriter(base, baseUri, path, ctx) {
105
105
  if (typeof free !== 'number' || isNaN(free))
106
106
  throw '';
107
107
  if (fullSize > free - (min || 0))
108
- return fail(const_1.HTTP_PAYLOAD_TOO_LARGE);
108
+ return fail(const_1.HTTP_INSUFFICIENT_STORAGE);
109
109
  }
110
110
  catch (e) { // warn, but let it through
111
111
  console.warn("can't check disk size:", e.message || String(e));
package/src/vfs.js CHANGED
@@ -300,7 +300,7 @@ async function* walkNode(parent, { ctx, depth = Infinity, prefixPath = '', requi
300
300
  const { path } = entry;
301
301
  const isFolder = entry.isDirectory();
302
302
  const name = prefixPath + (((_a = parent.rename) === null || _a === void 0 ? void 0 : _a[path]) || path);
303
- if (comments_1.descriptIon.get() && (0, path_1.basename)(name) === comments_1.DESCRIPT_ION)
303
+ if ((0, comments_1.usingDescriptIon)() && (0, path_1.basename)(name) === comments_1.DESCRIPT_ION)
304
304
  return;
305
305
  if (taken === null || taken === void 0 ? void 0 : taken.has(normalizeFilename(name))) // taken by vfs node above
306
306
  return false; // false just in case it's a folder
package/README.md DELETED
@@ -1,177 +0,0 @@
1
- # HFS: HTTP File Server (version 3)
2
-
3
- ![logo and motto](hfs-logo-color-motto.svg)
4
-
5
- ## Introduction
6
-
7
- Access via web your files directly from your disk.
8
-
9
- - You be the server, share files **fresh from your disk**, with **unlimited** space and bandwidth.
10
- - **Fast!** Try zipping 100GB, download starts immediately!
11
- - **Intelligent**. HFS tries to detect problems and suggest solutions.
12
- - Share **even a single file** with our *virtual file system*, even with a different name, all without touching the real file. Present things the way you want!
13
- - **Watch** all activities in real-time.
14
- - **Control bandwidth**, decide how much to give.
15
- - **No intermediaries**, give a huge file to your friend without waiting for it to be uploaded on a server first.
16
-
17
- This is a full rewrite of [the Delphi version](https://github.com/rejetto/hfs2).
18
-
19
- ## How does it work
20
-
21
- - run HFS on your computer, administration page automatically shows up
22
- - select what files and folders you want to be accessible
23
- - access those files from a phone or another computer just using a browser
24
- - possibly create accounts and limit access to files
25
-
26
- ## Features
27
-
28
- - https
29
- - easy certificate generation
30
- - unicode
31
- - virtual file system
32
- - mobile friendly
33
- - search
34
- - accounts
35
- - resumable downloads & uploads
36
- - download folders as zip archive
37
- - delete, move and rename files
38
- - simple website serving
39
- - plug-ins
40
- - real-time monitoring of connections
41
- - [show some files](https://github.com/rejetto/hfs/discussions/270)
42
- - speed throttler
43
- - geographic firewall
44
- - admin web interface
45
- - multi-language front-end
46
- - virtual hosting
47
- - anti-brute-force (plug-in)
48
- - [reverse-proxy support](https://github.com/rejetto/hfs/wiki/Reverse-proxy)
49
- - comments in file descript.ion
50
- - integrated media player
51
- - [customizable with html, css and javascript](https://github.com/rejetto/hfs/wiki/Customization)
52
- - dynamic-dns updater
53
-
54
- ## Installation
55
-
56
- For service installation instructions, [see our wiki](https://github.com/rejetto/hfs/wiki/Service-installation).
57
-
58
- For Docker installation, [see dedicated repo](https://github.com/damienzonly/hfs-docker).
59
-
60
- NB: minimum Windows version required is 8.1 , Windows Server 2012 R2 (because of Node.js 18)
61
-
62
- 1. go to https://github.com/rejetto/hfs/releases
63
- 2. click on `Assets`
64
- 3. **download** the right version for your system, unzip and launch `hfs` file.
65
- - Mac: if you get *"cannot be opened because it is from an unidentified developer"*,
66
- you can hold `control` key while clicking, then click `open`.
67
- - If you cannot find your system in the list, see next section [Other systems](#other-systems).
68
- 4. the browser should automatically open on `localhost` address, so you can configure the rest in the Admin-panel.
69
- - if a browser cannot be opened on the computer where you are installing HFS,
70
- you should enter this command in the HFS console: `create-admin <PASSWORD>`
71
- - if you cannot access the console (like when you are running as a service),
72
- you can [edit the config file to add your admin account](config.md#accounts)
73
- - if you don't want to use an editor you can create the file with this command:
74
-
75
- `echo "create-admin: PASSWORD" > config.yaml`
76
-
77
- If you access *Admin-panel* via localhost, by default HFS **won't** require you to login.
78
- If you don't like this behavior, disable it in the Admin-panel or enter this console command `config localhost_admin false`.
79
-
80
- ### Other systems
81
-
82
- If your system is not Windows/Linux/Mac or you just don't want to run the binaries, you can try this alternative version:
83
-
84
- 1. [install node.js](https://nodejs.org) version 18.20
85
- 2. execute at command line `npx hfs@latest`
86
-
87
- The `@latest` part is optional, and ensures that you are always up to date.
88
-
89
- If this procedure fails, it may be that you are missing one of [these requirements](https://github.com/nodejs/node-gyp#installation).
90
-
91
- Configuration and other files will be stored in `%HOME%/.vfs`
92
-
93
- ## Console commands
94
-
95
- If you have full access to HFS' console, you can enter commands. Start with `help` to have a full list.
96
-
97
- ## Configuration
98
-
99
- For configuration please see [file config.md](config.md).
100
-
101
- ### Where is it stored
102
-
103
- Configuration is stored in the file `config.yaml`, exception made for custom HTML which is stored in `custom.html`.
104
-
105
- These files are kept in the Current Working Directory (cwd), which is by default the same folder of `hfs.exe`
106
- if you are using this kind of distribution on Windows, or `USER_FOLDER/.hfs` on other systems.
107
- You can decide a different folder passing `--cwd SOME_FOLDER` parameter at command line.
108
- Any relative path provided is relative to the *cwd*.
109
-
110
- [Check details about config file format](config.md).
111
-
112
- ## Internationalization
113
-
114
- It is possible to show the Front-end in other languages.
115
- Translation for some languages is already provided. If you find an error, consider reporting it
116
- or [editing the source file](https://github.com/rejetto/hfs/tree/main/src/langs).
117
-
118
- In the Languages section of the Admin-panel you can install additional language files.
119
-
120
- If your language is missing, please consider [translating yourself](https://github.com/rejetto/hfs/wiki/Translation).
121
-
122
- ## Hidden features
123
-
124
- - Appending `#LOGIN` to address will bring up the login dialog
125
- - Appending ?lang=CODE to address will force a specific language
126
- - Right-click on toggle-all checkbox will invert each checkbox state
127
- - Appending `?login=USER:PASSWORD` will automatically log in the browser
128
- - Appending `?overwrite` on uploads, will override the dont_overwrite_uploading configuration, provided you also have delete permission
129
- - Appending `?search=PATTERN` will trigger search at start
130
- - Appending `?onlyFiles` or `?onlyFolders` will limit type of results
131
- - Appending `?autoplay=shuffle` will trigger show & play; `?autoplay` will not shuffle, but also will not start until the list is complete
132
- - Right-click on "check for updates" will let you input a URL of a version to install
133
- - Shift+click on a file will show & play
134
- - Type the name of a file/folder to focus it, and ctrl+backspace to go to parent folder
135
- - `--consoleFile PATH` will output all stdout and stderr also to a file
136
- - env `DISABLE_UPDATE=1` (for containers)
137
-
138
- ## Contribute
139
-
140
- There are several ways to contribute
141
-
142
- - [Report bugs](https://github.com/rejetto/hfs/issues/new?labels=bug&template=bug_report.md)
143
-
144
- It's very important to report bugs, and if you are not so sure about it, don't worry, we'll discuss it.
145
- If you find important security problems, please [contact us privately](mailto:a@rejetto.com) so that we can publish a fix before
146
- the problem is disclosed, for the safety of other users.
147
-
148
- - Use beta versions, and give feedback.
149
-
150
- While betas have more problems, you'll get more features and give a huge help to the project.
151
-
152
- - [Translate to your language](https://github.com/rejetto/hfs/wiki/Translation).
153
-
154
- - [Suggest ideas](https://github.com/rejetto/hfs/discussions)
155
-
156
- While the project should not become too complex, yours may be an idea for a plugin.
157
-
158
- - Write guides or make videos for other users. [We got a wiki](https://github.com/rejetto/hfs/wiki)!
159
-
160
- - Submit your code
161
-
162
- If you'd like to make a change yourself in the code, please first open an "issue" or "discussion" about it,
163
- so we'll try to cooperate and understand what's the best path for it.
164
-
165
- - [Make a plugin](dev-plugins.md)
166
-
167
- A plugin can change the look (a theme), and/or introduce a new functionality.
168
-
169
- ## More
170
-
171
- - [APIs](https://github.com/rejetto/hfs/wiki/APIs)
172
-
173
- - [Build yourself](dev.md)
174
-
175
- - [License](https://github.com/rejetto/hfs/blob/master/LICENSE.txt)
176
-
177
- - Flag images are public-domain, downloaded from https://flagpedia.net