hfs 0.53.0 → 0.54.0-alpha3

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 (65) hide show
  1. package/admin/assets/index-CcUio6cm.css +1 -0
  2. package/admin/assets/index-DCsMhd8s.js +807 -0
  3. package/admin/assets/{sha512-rpja4qx0.js → sha512-VvQJXb6I.js} +1 -1
  4. package/admin/index.html +4 -2
  5. package/frontend/assets/index-legacy-vQymU_EM.js +60 -0
  6. package/frontend/assets/{sha512-legacy-pz5im6D1.js → sha512-legacy-BD2cMJIT.js} +1 -1
  7. package/frontend/index.html +2 -2
  8. package/package.json +8 -6
  9. package/plugins/antibrute/plugin.js +1 -1
  10. package/plugins/download-counter/plugin.js +0 -15
  11. package/plugins/list-uploader/public/main.js +3 -2
  12. package/src/acme.js +2 -2
  13. package/src/adminApis.js +23 -3
  14. package/src/api.accounts.js +1 -1
  15. package/src/api.auth.js +6 -4
  16. package/src/api.get_file_list.js +18 -9
  17. package/src/api.monitor.js +1 -1
  18. package/src/api.net.js +4 -1
  19. package/src/api.plugins.js +2 -0
  20. package/src/api.vfs.js +6 -4
  21. package/src/apiMiddleware.js +4 -1
  22. package/src/auth.js +7 -7
  23. package/src/basicWeb.js +1 -1
  24. package/src/block.js +13 -1
  25. package/src/commands.js +8 -2
  26. package/src/const.js +13 -10
  27. package/src/cross-const.js +2 -1
  28. package/src/cross.js +25 -8
  29. package/src/debounceAsync.js +3 -5
  30. package/src/dirStream.js +138 -0
  31. package/src/events.js +18 -8
  32. package/src/frontEndApis.js +32 -15
  33. package/src/github.js +21 -3
  34. package/src/langs/embedded.js +4 -1
  35. package/src/langs/hfs-lang-fi.json +5 -3
  36. package/src/langs/hfs-lang-hu.json +27 -19
  37. package/src/langs/hfs-lang-it.json +2 -1
  38. package/src/langs/hfs-lang-th.json +166 -0
  39. package/src/langs/hfs-lang-tr.json +166 -0
  40. package/src/langs/hfs-lang-uk.json +170 -0
  41. package/src/langs/hfs-lang-vi.json +5 -5
  42. package/src/listen.js +12 -4
  43. package/src/log.js +1 -1
  44. package/src/makeQ.js +31 -0
  45. package/src/middlewares.js +11 -8
  46. package/src/misc.js +2 -1
  47. package/src/nat.js +4 -4
  48. package/src/perm.js +1 -1
  49. package/src/persistence.js +2 -1
  50. package/src/plugins.js +22 -3
  51. package/src/serveFile.js +16 -14
  52. package/src/serveGuiAndSharedFiles.js +9 -4
  53. package/src/serveGuiFiles.js +8 -6
  54. package/src/srp.js +2 -2
  55. package/src/update.js +48 -7
  56. package/src/upload.js +33 -18
  57. package/src/util-files.js +7 -29
  58. package/src/vfs.js +23 -11
  59. package/src/watchLoad.js +1 -1
  60. package/src/zip.js +4 -4
  61. package/README.md +0 -232
  62. package/admin/assets/index-Ly9xr1F6.js +0 -811
  63. package/admin/assets/index-gNE0rRfC.css +0 -1
  64. package/frontend/assets/index-legacy-5MEl9EM8.js +0 -60
  65. /package/frontend/assets/{polyfills-legacy-31b7nyOi.js → polyfills-legacy-DMrMt_pQ.js} +0 -0
@@ -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.sessionMiddleware = exports.paramsDecoder = exports.prepareState = exports.getProxyDetected = exports.someSecurity = exports.headRequests = exports.gzipper = exports.sessionDuration = void 0;
7
+ exports.sessionMiddleware = exports.paramsDecoder = exports.prepareState = exports.getProxyDetected = exports.someSecurity = exports.cloudflareDetected = exports.headRequests = exports.gzipper = exports.sessionDuration = void 0;
8
8
  const koa_compress_1 = __importDefault(require("koa-compress"));
9
9
  const const_1 = require("./const");
10
10
  const misc_1 = require("./misc");
@@ -19,8 +19,10 @@ const config_1 = require("./config");
19
19
  const koa_session_1 = __importDefault(require("koa-session"));
20
20
  const index_1 = require("./index");
21
21
  const events_1 = __importDefault(require("./events"));
22
+ const allowSessionIpChange = (0, config_1.defineConfig)('allow_session_ip_change', false);
22
23
  const forceHttps = (0, config_1.defineConfig)('force_https', true);
23
24
  const ignoreProxies = (0, config_1.defineConfig)('ignore_proxies', false);
25
+ const allowAuthorizationHeader = (0, config_1.defineConfig)('authorization_header', true);
24
26
  exports.sessionDuration = (0, config_1.defineConfig)('session_duration', Number(process.env.SESSION_DURATION) || misc_1.DAY / 1000, v => v * 1000);
25
27
  exports.gzipper = (0, koa_compress_1.default)({
26
28
  threshold: 2048,
@@ -49,9 +51,8 @@ exports.headRequests = headRequests;
49
51
  let proxyDetected;
50
52
  const someSecurity = (ctx, next) => {
51
53
  ctx.request.ip = (0, connections_1.normalizeIp)(ctx.ip);
52
- // don't allow sessions to change ip
53
54
  const ss = ctx.session;
54
- if (ss === null || ss === void 0 ? void 0 : ss.username)
55
+ if ((ss === null || ss === void 0 ? void 0 : ss.username) && (!allowSessionIpChange.get() || !ctx.secure && allowSessionIpChange.get() === 'https'))
55
56
  if (!ss.ip)
56
57
  ss.ip = ctx.ip;
57
58
  else if (ss.ip !== ctx.ip) {
@@ -69,6 +70,8 @@ const someSecurity = (ctx, next) => {
69
70
  proxyDetected = ctx;
70
71
  ctx.state.whenProxyDetected = new Date();
71
72
  }
73
+ if (ctx.get('cf-ray'))
74
+ exports.cloudflareDetected = new Date();
72
75
  }
73
76
  catch (_a) {
74
77
  return ctx.status = const_1.HTTP_FOOL;
@@ -112,24 +115,24 @@ const prepareState = async (ctx, next) => {
112
115
  return;
113
116
  const [u, p] = (0, misc_1.splitAt)(':', String(login));
114
117
  ctx.redirect(ctx.originalUrl.slice(0, -ctx.querystring.length - 1)); // redirect to hide credentials
115
- return doLogin(u, p);
118
+ return doLogin(u, p, 'url');
116
119
  }
117
120
  function getHttpAccount() {
118
121
  var _a;
119
- const b64 = (_a = ctx.get('authorization')) === null || _a === void 0 ? void 0 : _a.split(' ')[1];
122
+ const b64 = allowAuthorizationHeader.get() && ((_a = ctx.get('authorization')) === null || _a === void 0 ? void 0 : _a.split(' ')[1]);
120
123
  if (!b64)
121
124
  return;
122
125
  try {
123
126
  const [u, p] = atob(b64).split(':');
124
- return doLogin(u, p || '');
127
+ return doLogin(u, p || '', 'header');
125
128
  }
126
129
  catch (_b) { }
127
130
  }
128
- async function doLogin(u, p) {
131
+ async function doLogin(u, p, via) {
129
132
  var _a;
130
133
  if (!u || u === ((_a = ctx.session) === null || _a === void 0 ? void 0 : _a.username))
131
134
  return; // providing credentials, but not needed
132
- await events_1.default.emitAsync('attemptingLogin', ctx);
135
+ await events_1.default.emitAsync('attemptingLogin', { ctx, username: u, via });
133
136
  const a = await (0, auth_1.srpCheck)(u, p);
134
137
  if (a) {
135
138
  await (0, auth_1.setLoggedIn)(ctx, a.username);
package/src/misc.js CHANGED
@@ -168,13 +168,14 @@ function createStreamLimiter(limit) {
168
168
  }
169
169
  exports.createStreamLimiter = createStreamLimiter;
170
170
  async function deleteNode(ctx, node, uri) {
171
+ var _a;
171
172
  const { source } = node;
172
173
  if (!source)
173
174
  return const_1.HTTP_METHOD_NOT_ALLOWED;
174
175
  if ((0, vfs_1.statusCodeForMissingPerm)(node, 'can_delete', ctx))
175
176
  return ctx.status;
176
177
  try {
177
- if (await events_1.default.emitAsync('deleting', { node, ctx }).isDefaultPrevented())
178
+ if ((_a = (await events_1.default.emitAsync('deleting', { node, ctx }))) === null || _a === void 0 ? void 0 : _a.isDefaultPrevented())
178
179
  return null; // stop
179
180
  ctx.logExtra(null, { target: decodeURI(uri) });
180
181
  await (0, promises_1.rm)(source, { recursive: true });
package/src/nat.js CHANGED
@@ -32,7 +32,7 @@ exports.defaultBaseUrl = (0, valtio_1.proxy)({
32
32
  exports.upnpClient = new nat_upnp_rejetto_1.Client({ timeout: 4000 });
33
33
  const originalMethod = exports.upnpClient.getGateway;
34
34
  // other client methods call getGateway too, so this will ensure they reuse this same result
35
- exports.upnpClient.getGateway = (0, debounceAsync_1.debounceAsync)(() => originalMethod.apply(exports.upnpClient), 0, { retain: cross_1.HOUR, retainFailure: 30000 });
35
+ exports.upnpClient.getGateway = (0, debounceAsync_1.debounceAsync)(() => originalMethod.apply(exports.upnpClient), { retain: cross_1.HOUR, retainFailure: 30000 });
36
36
  exports.upnpClient.getGateway().then(res => {
37
37
  console.debug('upnp', res.gateway.description);
38
38
  }, e => console.debug('upnp failed:', e.message || String(e)));
@@ -64,16 +64,16 @@ exports.getPublicIps = (0, debounceAsync_1.debounceAsync)(async () => {
64
64
  return validIps;
65
65
  }))));
66
66
  return exports.defaultBaseUrl.publicIps = lodash_1.default.uniq(ips.flat());
67
- }, 0, { retain: 10 * cross_1.MINUTE });
67
+ }, { retain: 10 * cross_1.MINUTE });
68
68
  exports.getNatInfo = (0, debounceAsync_1.debounceAsync)(async () => {
69
69
  var _a, _b, _c, _d;
70
+ const gatewayIpPromise = findGateway().catch(() => undefined);
70
71
  const res = await (0, cross_1.haveTimeout)(10000, exports.upnpClient.getGateway()).catch(() => null);
71
72
  const status = await (0, listen_1.getServerStatus)();
72
73
  const mappings = res && await (0, cross_1.haveTimeout)(5000, exports.upnpClient.getMappings()).catch(() => null);
73
74
  console.debug('mappings found', mappings === null || mappings === void 0 ? void 0 : mappings.map(x => x.description));
74
- const gatewayIp = res && (0, cross_1.try_)(() => new URL(res.gateway.description).hostname, () => { var _a; return console.debug('unexpected upnp gw', (_a = res.gateway) === null || _a === void 0 ? void 0 : _a.description); })
75
- || await findGateway().catch(() => undefined);
76
75
  const localIps = await (0, listen_1.getIps)(false);
76
+ const gatewayIp = await gatewayIpPromise;
77
77
  const localIp = (res === null || res === void 0 ? void 0 : res.address) || gatewayIp ? lodash_1.default.maxBy(localIps, x => (0, cross_1.inCommon)(x, gatewayIp)) : localIps[0];
78
78
  const internalPort = ((_a = status === null || status === void 0 ? void 0 : status.https) === null || _a === void 0 ? void 0 : _a.listening) && status.https.port || ((_b = status === null || status === void 0 ? void 0 : status.http) === null || _b === void 0 ? void 0 : _b.listening) && status.http.port || undefined;
79
79
  const mapped = lodash_1.default.find(mappings, x => x.private.host === localIp && x.private.port === internalPort);
package/src/perm.js CHANGED
@@ -47,7 +47,7 @@ createAdminConfig.sub(v => {
47
47
  });
48
48
  async function createAdmin(password, username = 'admin') {
49
49
  const acc = await addAccount(username, { admin: true, password }, true);
50
- console.log(acc ? "account admin created" : "something went wrong");
50
+ console.log(acc ? "account admin set" : "something went wrong");
51
51
  }
52
52
  exports.createAdmin = createAdmin;
53
53
  const srp6aNimbusRoutines = new tssrp6a_1.SRPRoutines(new tssrp6a_1.SRPParameters());
@@ -8,7 +8,8 @@ exports.storedMap = new kvstorage_1.KvStorage({
8
8
  defaultPutDelay: 5000,
9
9
  maxPutDelay: misc_1.MINUTE,
10
10
  maxPutDelayCreate: 1000,
11
- rewriteLater: true
11
+ rewriteLater: true,
12
+ bucketThreshold: 10000,
12
13
  });
13
14
  exports.storedMap.open('data.kv');
14
15
  (0, first_1.onProcessExit)(() => exports.storedMap.flush());
package/src/plugins.js CHANGED
@@ -27,13 +27,14 @@ 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.getMissingDependencies = exports.parsePluginSource = exports.rescan = exports.pluginsConfig = exports.enablePlugins = exports.pluginsWatcher = exports.getAvailablePlugins = exports.mapPlugins = exports.SERVER_CODE_ID = exports.Plugin = exports.pluginsMiddleware = exports.getPluginConfigFields = exports.findPluginByRepo = exports.getPluginInfo = exports.setPluginConfig = exports.startPlugin = exports.stopPlugin = exports.enablePlugin = exports.isPluginEnabled = exports.isPluginRunning = exports.STORAGE_FOLDER = exports.DISABLING_SUFFIX = exports.PATH = void 0;
30
+ exports.getMissingDependencies = exports.parsePluginSource = exports.rescan = exports.pluginsConfig = exports.enablePlugins = exports.pluginsWatcher = exports.getAvailablePlugins = exports.firstPlugin = exports.mapPlugins = exports.SERVER_CODE_ID = exports.Plugin = exports.pluginsMiddleware = exports.getPluginConfigFields = exports.findPluginByRepo = exports.getPluginInfo = exports.setPluginConfig = exports.startPlugin = exports.stopPlugin = exports.enablePlugin = exports.isPluginEnabled = exports.isPluginRunning = exports.STORAGE_FOLDER = exports.DISABLING_SUFFIX = exports.PATH = void 0;
31
31
  const fast_glob_1 = __importDefault(require("fast-glob"));
32
32
  const watchLoad_1 = require("./watchLoad");
33
33
  const lodash_1 = __importDefault(require("lodash"));
34
34
  const const_1 = require("./const");
35
35
  const Const = __importStar(require("./const"));
36
36
  const misc_1 = require("./misc");
37
+ const misc = __importStar(require("./misc"));
37
38
  const config_1 = require("./config");
38
39
  const serveFile_1 = require("./serveFile");
39
40
  const events_1 = __importDefault(require("./events"));
@@ -46,6 +47,7 @@ const kvstorage_1 = require("@rejetto/kvstorage");
46
47
  const first_1 = require("./first");
47
48
  const frontEndApis_1 = require("./frontEndApis");
48
49
  const index_1 = require("./index");
50
+ const block_1 = require("./block");
49
51
  exports.PATH = 'plugins';
50
52
  exports.DISABLING_SUFFIX = '-disabled';
51
53
  exports.STORAGE_FOLDER = 'storage';
@@ -134,6 +136,8 @@ async function initPlugin(pl, morePassedToInit) {
134
136
  getHfsConfig: config_1.getConfig,
135
137
  customApiCall,
136
138
  notifyClient: frontEndApis_1.notifyClient,
139
+ addBlock: block_1.addBlock,
140
+ misc,
137
141
  ...morePassedToInit
138
142
  })));
139
143
  }
@@ -159,7 +163,7 @@ const pluginsMiddleware = async (ctx, next) => {
159
163
  lastStatus = ctx.status;
160
164
  lastBody = ctx.body;
161
165
  }
162
- if (res === true && !ctx.isStopped) { // legacy pre-0.53
166
+ if (res === true && !ctx.isStopped) { //legacy pre-0.53
163
167
  ctx.stop();
164
168
  warnOnce(`plugin ${id} is using deprecated API (return true on middleware) and may not work with future versions (check for an update to "${id}")`);
165
169
  }
@@ -306,12 +310,27 @@ function mapPlugins(cb, includeServerCode = true) {
306
310
  }).filter(x => x !== undefined);
307
311
  }
308
312
  exports.mapPlugins = mapPlugins;
313
+ function firstPlugin(cb, includeServerCode = true) {
314
+ for (const [plName, pl] of Object.entries(plugins)) {
315
+ if (!includeServerCode && plName === exports.SERVER_CODE_ID)
316
+ continue;
317
+ try {
318
+ const ret = cb(pl, plName);
319
+ if (ret !== undefined)
320
+ return ret;
321
+ }
322
+ catch (e) {
323
+ console.log('plugin error', plName, String(e));
324
+ }
325
+ }
326
+ }
327
+ exports.firstPlugin = firstPlugin;
309
328
  let availablePlugins = {};
310
329
  function getAvailablePlugins() {
311
330
  return Object.values(availablePlugins);
312
331
  }
313
332
  exports.getAvailablePlugins = getAvailablePlugins;
314
- const rescanAsap = (0, misc_1.debounceAsync)(rescan, 1000);
333
+ const rescanAsap = (0, misc_1.debounceAsync)(rescan, { wait: 1000 });
315
334
  if (!(0, fs_1.existsSync)(exports.PATH))
316
335
  try {
317
336
  (0, fs_1.mkdirSync)(exports.PATH);
package/src/serveFile.js CHANGED
@@ -19,9 +19,9 @@ const auth_1 = require("./auth");
19
19
  const errorPages_1 = require("./errorPages");
20
20
  const stream_1 = require("stream");
21
21
  const allowedReferer = (0, config_1.defineConfig)('allowed_referer', '');
22
- const limitDownloads = downloadLimiter((0, config_1.defineConfig)(misc_1.CFG.max_downloads, 0), () => true);
23
- const limitDownloadsPerIp = downloadLimiter((0, config_1.defineConfig)(misc_1.CFG.max_downloads_per_ip, 0), ctx => ctx.ip);
24
- const limitDownloadsPerAccount = downloadLimiter((0, config_1.defineConfig)(misc_1.CFG.max_downloads_per_account, 0), ctx => (0, auth_1.getCurrentUsername)(ctx) || undefined);
22
+ const maxDownloads = downloadLimiter((0, config_1.defineConfig)(misc_1.CFG.max_downloads, 0), () => true);
23
+ const maxDownloadsPerIp = downloadLimiter((0, config_1.defineConfig)(misc_1.CFG.max_downloads_per_ip, 0), ctx => ctx.ip);
24
+ const maxDownloadsPerAccount = downloadLimiter((0, config_1.defineConfig)(misc_1.CFG.max_downloads_per_account, 0), ctx => (0, auth_1.getCurrentUsername)(ctx) || undefined);
25
25
  async function serveFileNode(ctx, node) {
26
26
  var _a, _b;
27
27
  const { source, mime } = node;
@@ -30,7 +30,7 @@ async function serveFileNode(ctx, node) {
30
30
  : lodash_1.default.find(mime, (val, mask) => (0, misc_1.matches)(name, mask));
31
31
  if (allowedReferer.get()) {
32
32
  const ref = (_a = /\/\/([^:/]+)/.exec(ctx.get('referer'))) === null || _a === void 0 ? void 0 : _a[1]; // extract host from url
33
- if (ref && ref !== host() // automatic accept if referer is basically the hosting domain
33
+ if (ref && ref !== host() // automatically accept if referer is basically the hosting domain
34
34
  && !(0, misc_1.matches)(ref, allowedReferer.get()))
35
35
  return ctx.status = const_1.HTTP_FORBIDDEN;
36
36
  }
@@ -41,10 +41,10 @@ async function serveFileNode(ctx, node) {
41
41
  else if (((_b = ctx.get('referer')) === null || _b === void 0 ? void 0 : _b.endsWith('/')) && (0, misc_1.with_)(ctx.get('accept'), x => x && !x.includes('text')))
42
42
  ctx.state.considerAsGui = true;
43
43
  await serveFile(ctx, source || '', mimeString);
44
- if (await limitDownloadsPerAccount(ctx) === undefined) // returning false will not execute other limits
45
- await limitDownloads(ctx) || await limitDownloadsPerIp(ctx);
44
+ if (await maxDownloadsPerAccount(ctx) === undefined) // returning false will not execute other limits
45
+ await maxDownloads(ctx) || await maxDownloadsPerIp(ctx);
46
46
  function host() {
47
- const s = ctx.get('host');
47
+ const s = ctx.host;
48
48
  return s[0] === '[' ? s.slice(1, s.indexOf(']')) : s === null || s === void 0 ? void 0 : s.split(':')[0];
49
49
  }
50
50
  }
@@ -54,6 +54,8 @@ const mimeCfg = (0, config_1.defineConfig)('mime', { '*': const_1.MIME_AUTO }, o
54
54
  const values = Object.values(obj);
55
55
  return (name) => values[matchers.findIndex(matcher => matcher(name))];
56
56
  });
57
+ // after this number of seconds, the browser should check the server to see if there's a newer version of the file
58
+ const cacheControlDiskFiles = (0, config_1.defineConfig)('cache_control_disk_files', 5);
57
59
  async function serveFile(ctx, source, mime, content) {
58
60
  if (!source)
59
61
  return;
@@ -75,19 +77,20 @@ async function serveFile(ctx, source, mime, content) {
75
77
  if (!stats.isFile())
76
78
  return ctx.status = const_1.HTTP_METHOD_NOT_ALLOWED;
77
79
  ctx.set('Last-Modified', stats.mtime.toUTCString());
78
- ctx.fileSource = // legacy pre-0.51
79
- ctx.state.fileSource = source;
80
- ctx.fileStats = // legacy pre-0.51
81
- ctx.state.fileStats = stats;
80
+ ctx.state.fileSource = source;
81
+ ctx.state.fileStats = stats;
82
82
  ctx.status = const_1.HTTP_OK;
83
83
  if (ctx.fresh)
84
84
  return ctx.status = const_1.HTTP_NOT_MODIFIED;
85
85
  if (content !== undefined)
86
86
  return ctx.body = content;
87
+ const cc = cacheControlDiskFiles.get();
88
+ if (lodash_1.default.isNumber(cc))
89
+ ctx.set('Cache-Control', `max-age=${cc}`);
87
90
  const { size } = stats;
88
91
  const range = applyRange(ctx, size);
89
92
  ctx.body = (0, fs_1.createReadStream)(source, range);
90
- if (ctx.vfsNode)
93
+ if (ctx.state.vfsNode)
91
94
  monitorAsDownload(ctx, size, range === null || range === void 0 ? void 0 : range.start);
92
95
  }
93
96
  catch (e) {
@@ -101,7 +104,6 @@ function monitorAsDownload(ctx, size, offset) {
101
104
  const conn = (0, connections_1.getConnection)(ctx);
102
105
  ctx.body.on('end', () => (0, connections_1.updateConnection)(conn, {}, { opProgress: 1 }));
103
106
  (0, connections_1.updateConnection)(conn, {}, {
104
- op: 'download',
105
107
  opProgress: 0,
106
108
  opTotal: size,
107
109
  opOffset: size && offset && (offset / size),
@@ -145,7 +147,7 @@ function downloadLimiter(configMax, cbKey) {
145
147
  const map = new Map();
146
148
  return (ctx) => {
147
149
  if (!ctx.body || ctx.state.considerAsGui)
148
- return; // no file sent, cache hit
150
+ return; // !body = no file sent, cache hit
149
151
  const k = cbKey(ctx);
150
152
  if (k === undefined)
151
153
  return; // undefined = skip limit
@@ -26,7 +26,6 @@ const serveFrontendPrefixed = (0, koa_mount_1.default)(cross_const_1.FRONTEND_UR
26
26
  const serveAdminFiles = (0, serveGuiFiles_1.serveGuiFiles)(process.env.ADMIN_PROXY, cross_const_1.ADMIN_URI);
27
27
  const serveAdminPrefixed = (0, koa_mount_1.default)(cross_const_1.ADMIN_URI.slice(0, -1), serveAdminFiles);
28
28
  const serveGuiAndSharedFiles = async (ctx, next) => {
29
- var _a;
30
29
  const { path } = ctx;
31
30
  // dynamic import on frontend|admin (used for non-https login) while developing (vite4) is not producing a relative path
32
31
  if (const_1.DEV && path.startsWith('/node_modules/')) {
@@ -100,15 +99,21 @@ const serveGuiAndSharedFiles = async (ctx, next) => {
100
99
  return;
101
100
  }
102
101
  const { get } = ctx.query;
103
- if (node.default && path.endsWith('/') && !get) // final/ needed on browser to make resource urls correctly with html pages
104
- node = (_a = await (0, vfs_1.urlToNode)(node.default, ctx, node)) !== null && _a !== void 0 ? _a : node;
102
+ if (node.default && path.endsWith('/') && !get) { // final/ needed on browser to make resource urls correctly with html pages
103
+ const found = await (0, vfs_1.urlToNode)(node.default, ctx, node);
104
+ if (found && /\.html?/i.test(node.default))
105
+ ctx.state.considerAsGui = true;
106
+ node = found !== null && found !== void 0 ? found : node;
107
+ }
108
+ if (get === 'icon')
109
+ return (0, serveFile_1.serveFile)(ctx, node.icon || '|'); // pipe to cause not-found
105
110
  if (!await (0, vfs_1.nodeIsDirectory)(node))
106
111
  return node.url ? ctx.redirect(node.url)
107
112
  : !node.source ? (0, errorPages_1.sendErrorPage)(ctx, cross_const_1.HTTP_METHOD_NOT_ALLOWED) // !dir && !source is not supported at this moment
108
113
  : !(0, vfs_1.statusCodeForMissingPerm)(node, 'can_read', ctx) ? (0, serveFile_1.serveFileNode)(ctx, node) // all good
109
114
  : ctx.status !== cross_const_1.HTTP_UNAUTHORIZED ? null // all errors don't need extra handling, except unauthorized
110
115
  : (0, basicWeb_1.detectBasicAgent)(ctx) ? (ctx.set('WWW-Authenticate', 'Basic'), (0, errorPages_1.sendErrorPage)(ctx))
111
- : (ctx.state.serveApp = true) && serveFrontendFiles(ctx, next); // this is necessary to support standard urls with credentials, as chrome125 will send provided credentials only after attempt a GET without them, and after this error
116
+ : ctx.query.dl === undefined && (ctx.state.serveApp = true) && serveFrontendFiles(ctx, next);
112
117
  if (!path.endsWith('/'))
113
118
  return ctx.redirect(ctx.state.revProxyPath + ctx.originalUrl.replace(/(\?|$)/, '/$1')); // keep query-string, if any
114
119
  if ((0, vfs_1.statusCodeForMissingPerm)(node, 'can_list', ctx)) {
@@ -43,6 +43,7 @@ const lodash_1 = __importDefault(require("lodash"));
43
43
  const config_1 = require("./config");
44
44
  const lang_1 = require("./lang");
45
45
  const upload_1 = require("./upload");
46
+ const splitUploads = (0, config_1.defineConfig)(misc_1.CFG.split_uploads, 0);
46
47
  exports.logGui = (0, config_1.defineConfig)(misc_1.CFG.log_gui, false);
47
48
  lodash_1.default.each(misc_1.FRONTEND_OPTIONS, (v, k) => (0, config_1.defineConfig)(k, v)); // define default values
48
49
  // in case of dev env we have our static files within the 'dist' folder'
@@ -85,7 +86,7 @@ function adjustBundlerLinks(ctx, uri, data) {
85
86
  const getFaviconTimestamp = (0, misc_1.debounceAsync)(async () => {
86
87
  const f = adminApis_1.favicon.get();
87
88
  return !f ? 0 : promises_1.default.stat(f).then(x => (x === null || x === void 0 ? void 0 : x.mtimeMs) || 0, () => 0);
88
- }, 0, { retain: 5000 });
89
+ }, { retain: 5000 });
89
90
  async function treatIndex(ctx, filesUri, body) {
90
91
  const session = await (0, api_auth_1.refresh_session)({}, ctx);
91
92
  ctx.set('etag', '');
@@ -114,10 +115,6 @@ async function treatIndex(ctx, filesUri, body) {
114
115
  const isOpen = !isClose;
115
116
  if (isHead && isOpen)
116
117
  return all + `
117
- ${isFrontend && `
118
- <title>${adminApis_1.title.get()}</title>
119
- <link rel="shortcut icon" href="/favicon.ico?${timestamp}" />
120
- ` + (0, customHtml_1.getSection)('htmlHead')}
121
118
  <script>
122
119
  HFS = ${JSON.stringify({
123
120
  VERSION: const_1.VERSION,
@@ -128,13 +125,18 @@ async function treatIndex(ctx, filesUri, body) {
128
125
  loadScripts: Object.fromEntries((0, plugins_1.mapPlugins)((p, id) => { var _a; return [id, (_a = p.frontend_js) === null || _a === void 0 ? void 0 : _a.map(f => f.includes('//') ? f : pub + id + '/' + f)]; })),
129
126
  prefixUrl: ctx.state.revProxyPath,
130
127
  dontOverwriteUploading: upload_1.dontOverwriteUploading.get(),
128
+ splitUploads: splitUploads.get(),
131
129
  forceTheme: (0, plugins_1.mapPlugins)(p => lodash_1.default.isString(p.isTheme) ? p.isTheme : undefined).find(Boolean),
132
130
  customHtml: lodash_1.default.omit((0, customHtml_1.getAllSections)(), ['top', 'bottom', 'htmlHead', 'style']),
133
131
  ...(0, misc_1.newObj)(misc_1.FRONTEND_OPTIONS, (v, k) => (0, config_1.getConfig)(k)),
134
132
  lang
135
133
  }, null, 4).replace(/<(\/script)/g, '<"+"$1') /*avoid breaking our script container*/}
136
- document.documentElement.setAttribute('ver', '${const_1.VERSION.split('-')[0] /*for style selectors*/}')
134
+ document.documentElement.setAttribute('ver', HFS.VERSION.split('-')[0])
137
135
  </script>
136
+ ${isFrontend && `
137
+ <title>${adminApis_1.title.get()}</title>
138
+ <link rel="shortcut icon" href="/favicon.ico?${timestamp}" />
139
+ ${(0, customHtml_1.getSection)('htmlHead')}`}
138
140
  `;
139
141
  if (isBody && isOpen)
140
142
  return `${all}
package/src/srp.js CHANGED
@@ -15,8 +15,8 @@ async function srpClientSequence(username, password, apiCall) {
15
15
  exports.srpClientSequence = srpClientSequence;
16
16
  async function srpClientPart(username, password, salt, pubKey) {
17
17
  const srp6aNimbusRoutines = new tssrp6a_1.SRPRoutines(new tssrp6a_1.SRPParameters());
18
- const srp = new tssrp6a_1.SRPClientSession(srp6aNimbusRoutines);
19
- const res = await srp.step1(username, password);
18
+ const srpClient = new tssrp6a_1.SRPClientSession(srp6aNimbusRoutines);
19
+ const res = await srpClient.step1(username, password);
20
20
  return await res.step2(BigInt(salt), BigInt(pubKey));
21
21
  }
22
22
  exports.srpClientPart = srpClientPart;
package/src/update.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.update = exports.updateSupported = exports.localUpdateAvailable = exports.getUpdates = void 0;
7
+ exports.update = exports.updateSupported = exports.localUpdateAvailable = exports.getUpdates = exports.autoCheckUpdateResult = void 0;
8
8
  const github_1 = require("./github");
9
9
  const const_1 = require("./const");
10
10
  const path_1 = require("path");
@@ -17,15 +17,48 @@ const open_1 = __importDefault(require("open"));
17
17
  const config_1 = require("./config");
18
18
  const util_os_1 = require("./util-os");
19
19
  const first_1 = require("./first");
20
+ const persistence_1 = require("./persistence");
21
+ const lodash_1 = __importDefault(require("lodash"));
20
22
  const updateToBeta = (0, config_1.defineConfig)('update_to_beta', false);
23
+ const autoCheckUpdate = (0, config_1.defineConfig)('auto_check_update', true);
24
+ const lastCheckUpdate = persistence_1.storedMap.singleSync('lastCheckUpdate', 0);
25
+ const AUTO_CHECK_EVERY = misc_1.DAY;
26
+ exports.autoCheckUpdateResult = persistence_1.storedMap.singleSync('autoCheckUpdateResult', undefined);
27
+ exports.autoCheckUpdateResult.ready().then(() => {
28
+ exports.autoCheckUpdateResult.set(v => {
29
+ if (!v)
30
+ return; // refresh isNewer, as currentVersion may have changed
31
+ v.isNewer = config_1.currentVersion.olderThan(v.tag_name);
32
+ return v;
33
+ });
34
+ });
35
+ setInterval((0, misc_1.debounceAsync)(async () => {
36
+ if (!autoCheckUpdate.get())
37
+ return;
38
+ if (Date.now() < lastCheckUpdate.get() + AUTO_CHECK_EVERY)
39
+ return;
40
+ console.log("checking for updates");
41
+ try {
42
+ const u = (await getUpdates(true))[0];
43
+ if (u)
44
+ console.log("new version available", u.name);
45
+ exports.autoCheckUpdateResult.set(u);
46
+ lastCheckUpdate.set(Date.now());
47
+ }
48
+ catch (_a) { }
49
+ }), misc_1.HOUR);
50
+ const ReleaseKeys = ['prerelease', 'tag_name', 'name', 'body', 'assets', 'isNewer'];
51
+ const ReleaseAssetKeys = ['name', 'browser_download_url'];
21
52
  async function getUpdates(strict = false) {
53
+ (0, github_1.getProjectInfo)(); // check for alerts
22
54
  const stable = await (0, github_1.getRepoInfo)(const_1.HFS_REPO + '/releases/latest');
23
55
  const verStable = ver(stable);
24
56
  const ret = await getBetas();
25
57
  stable.isNewer = config_1.currentVersion.olderThan(stable.tag_name);
26
58
  if (stable.isNewer || const_1.RUNNING_BETA)
27
59
  ret.push(stable);
28
- return ret.filter(x => !strict || x.isNewer);
60
+ // prune a bit, as it will be serialized, but it has a lot of unused data
61
+ return ret.filter(x => !strict || x.isNewer).map(x => Object.assign(lodash_1.default.pick(x, ReleaseKeys), { assets: x.assets.map(a => lodash_1.default.pick(a, ReleaseAssetKeys)) }));
29
62
  function ver(x) {
30
63
  return (0, config_1.versionToScalar)(x.name);
31
64
  }
@@ -41,10 +74,10 @@ async function getUpdates(strict = false) {
41
74
  break;
42
75
  const curV = config_1.currentVersion.getScalar();
43
76
  for (const x of res) {
44
- if (!x.prerelease)
45
- continue; // prerelease are all the end
77
+ if (!x.prerelease || x.name.endsWith('-ignore'))
78
+ continue;
46
79
  const v = ver(x);
47
- if (v <= verStable) // prerelease-s are locally ordered, so as soon as we reach verStable we are done
80
+ if (v < verStable) // we don't consider betas before stable
48
81
  return ret;
49
82
  if (v === curV)
50
83
  continue; // skip current
@@ -68,8 +101,9 @@ exports.updateSupported = updateSupported;
68
101
  async function update(tagOrUrl = '') {
69
102
  if (!await updateSupported())
70
103
  throw "only binary versions supports automatic update for now";
104
+ let doingLocal = '';
71
105
  let updateSource = tagOrUrl.includes('://') ? await (0, misc_1.httpStream)(tagOrUrl)
72
- : await localUpdateAvailable() && (0, fs_1.createReadStream)(LOCAL_UPDATE);
106
+ : await localUpdateAvailable() && (0, fs_1.createReadStream)(doingLocal = LOCAL_UPDATE);
73
107
  if (!updateSource) {
74
108
  if (/^\d/.test(tagOrUrl)) // work even if the tag is passed without the initial 'v' (useful for console commands)
75
109
  tagOrUrl = 'v' + tagOrUrl;
@@ -115,7 +149,14 @@ async function update(tagOrUrl = '') {
115
149
  catch (_a) { }
116
150
  (0, fs_1.renameSync)(bin, oldBin);
117
151
  console.log("launching new version in background", newBinFile);
118
- launch(newBin, ['--updating', binFile], { sync: true }); // sync necessary to work on mac by double-click
152
+ if (doingLocal)
153
+ try {
154
+ (0, fs_1.renameSync)(doingLocal, 'old-' + doingLocal);
155
+ }
156
+ catch (e) {
157
+ console.warn(e);
158
+ }
159
+ launch(newBin, ['--updating', binFile], { sync: true }); // sync necessary to work on Mac by double-click
119
160
  });
120
161
  console.log("quitting");
121
162
  setTimeout(() => process.exit()); // give time to return (and caller to complete, eg: rest api to reply)
package/src/upload.js CHANGED
@@ -61,9 +61,9 @@ function uploadWriter(base, path, ctx) {
61
61
  else
62
62
  try {
63
63
  // refer to the source of the closest node that actually belongs to the vfs, so that cache is more effective
64
- let closestVfsNode = base;
65
- while (closestVfsNode && !closestVfsNode.original)
66
- closestVfsNode = closestVfsNode.parent;
64
+ let closestVfsNode = base; // if base=root, there's no parent and no original
65
+ while ((closestVfsNode === null || closestVfsNode === void 0 ? void 0 : closestVfsNode.parent) && !closestVfsNode.original)
66
+ closestVfsNode = closestVfsNode.parent; // if it's not original, it surely has a parent
67
67
  const statDir = closestVfsNode.source;
68
68
  if (!Object.hasOwn(diskSpaceCache, statDir)) {
69
69
  const c = diskSpaceCache[statDir] = (0, util_os_1.getDiskSpaceSync)(statDir);
@@ -86,14 +86,15 @@ function uploadWriter(base, path, ctx) {
86
86
  if (ctx.query.existing === 'skip' && fs_1.default.existsSync(fullPath))
87
87
  return fail(const_1.HTTP_CONFLICT, 'exists');
88
88
  openFiles.add(fullPath);
89
+ let overwriteRequestedButForbidden = false;
89
90
  try {
90
91
  // if upload creates a folder, then add meta to it too
91
- if (fs_1.default.mkdirSync(dir, { recursive: true }))
92
+ if (!dir.endsWith(':\\') && fs_1.default.mkdirSync(dir, { recursive: true }))
92
93
  setUploadMeta(dir, ctx);
93
94
  // use temporary name while uploading
94
95
  const keepName = (0, path_1.basename)(fullPath).slice(-200);
95
96
  let tempName = (0, path_1.join)(dir, 'hfs$upload-' + keepName);
96
- const resumable = fs_1.default.existsSync(tempName) && !openFiles.has(tempName) && tempName;
97
+ const resumable = fs_1.default.existsSync(tempName) && !openFiles.has(tempName) && tempName; // resumable is temp-file-1
97
98
  if (resumable)
98
99
  tempName = (0, path_1.join)(dir, 'hfs$upload2-' + keepName);
99
100
  // checks for resume feature
@@ -101,15 +102,19 @@ function uploadWriter(base, path, ctx) {
101
102
  const size = resumable && (0, misc_1.try_)(() => fs_1.default.statSync(resumable).size);
102
103
  if (size === undefined) // stat failed
103
104
  return fail(const_1.HTTP_SERVER_ERROR);
104
- if (resume > size)
105
+ if (lodash_1.default.isNumber(size) && resume > size)
105
106
  return fail(const_1.HTTP_RANGE_NOT_SATISFIABLE);
106
107
  // warn frontend about resume possibility
108
+ let resumableLost = false;
107
109
  if (!resume && resumable) {
108
110
  const timeout = 30;
109
111
  (0, frontEndApis_1.notifyClient)(ctx, 'upload.resumable', { [path]: size, expires: Date.now() + timeout * 1000 });
110
- delayedDelete(resumable, timeout, () => fs_1.default.rename(tempName, resumable, err => {
111
- if (!err)
112
- tempName = resumable;
112
+ delayedDelete(resumable, timeout, () => // if user resumes, this upload is interrupted, and next upload will cancel this delayedDelete
113
+ fs_1.default.rename(tempName, resumable, err => {
114
+ if (err)
115
+ return;
116
+ tempName = resumable;
117
+ resumableLost = true;
113
118
  }));
114
119
  }
115
120
  // append if resuming
@@ -136,14 +141,21 @@ function uploadWriter(base, path, ctx) {
136
141
  const lockMiddleware = (0, misc_1.pendingPromise)(); // outside we need to know when all operations stopped
137
142
  writeStream.once('close', async () => {
138
143
  try {
144
+ await new Promise(res => fileStream.close(res)); // this only seem to be necessary on Windows
139
145
  if (ctx.req.aborted) {
140
- if (resumable) // we don't want to be left with 2 temp files
141
- return delayedDelete(tempName, 0);
146
+ if (resumable && !resumableLost && !resuming) // we don't want to be left with 2 temp files
147
+ return (0, promises_1.rm)(tempName);
142
148
  const sec = exports.deleteUnfinishedUploadsAfter.get();
143
149
  return lodash_1.default.isNumber(sec) && delayedDelete(tempName, sec);
144
150
  }
151
+ if (ctx.query.partial)
152
+ return; // this upload is partial, and we are supposed to leave the upload as unfinished, with the temp name
145
153
  let dest = fullPath;
146
154
  if (exports.dontOverwriteUploading.get() && !await overwriteAnyway() && fs_1.default.existsSync(dest)) {
155
+ if (overwriteRequestedButForbidden) {
156
+ await (0, promises_1.rm)(tempName);
157
+ return fail();
158
+ }
147
159
  const ext = (0, path_1.extname)(dest);
148
160
  const base = dest.slice(0, -ext.length || Infinity);
149
161
  let i = 1;
@@ -153,12 +165,13 @@ function uploadWriter(base, path, ctx) {
153
165
  }
154
166
  try {
155
167
  await (0, promises_1.rename)(tempName, dest);
168
+ cancelDeletion(tempName); // not necessary, as deletion's failure is silent, but still
156
169
  ctx.state.uploadDestinationPath = dest;
157
170
  setUploadMeta(dest, ctx);
158
171
  if (ctx.query.comment)
159
172
  void (0, comments_1.setCommentFor)(dest, String(ctx.query.comment));
160
- if (resumable)
161
- delayedDelete(resumable, 0);
173
+ if (resumable && !resuming) // this happens if user decided to not resume and the new upload finished before delayedDelete
174
+ (0, promises_1.rm)(resumable).catch(console.warn);
162
175
  events_1.default.emit('uploadFinished', obj);
163
176
  if (resEvent)
164
177
  for (const cb of resEvent)
@@ -182,7 +195,7 @@ function uploadWriter(base, path, ctx) {
182
195
  let lastGot = 0;
183
196
  let lastGotTime = 0;
184
197
  const opTotal = reqSize + resume;
185
- Object.assign(ctx.state, { op: 'upload', opTotal, opOffset: resume / opTotal, opProgress: 0 });
198
+ Object.assign(ctx.state, { opTotal, opOffset: resume / opTotal, opProgress: 0 });
186
199
  const conn = (0, connections_1.updateConnectionForCtx)(ctx);
187
200
  if (!conn)
188
201
  return;
@@ -202,11 +215,13 @@ function uploadWriter(base, path, ctx) {
202
215
  throw e;
203
216
  }
204
217
  async function overwriteAnyway() {
205
- if (ctx.query.overwrite === undefined // legacy pre-0.52
206
- && ctx.query.existing !== 'overwrite')
207
- return;
218
+ if (ctx.query.existing !== 'overwrite')
219
+ return false;
208
220
  const n = await (0, vfs_1.getNodeByName)(path, base);
209
- return n && (0, vfs_1.hasPermission)(n, 'can_delete', ctx);
221
+ if (n && !(0, vfs_1.statusCodeForMissingPerm)(n, 'can_delete', ctx))
222
+ return true;
223
+ overwriteRequestedButForbidden = true;
224
+ return false;
210
225
  }
211
226
  function delayedDelete(path, secs, cb) {
212
227
  clearTimeout(waitingToBeDeleted[path]);