hfs 0.48.3 → 0.49.0-alpha2

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 (44) hide show
  1. package/README.md +4 -2
  2. package/admin/assets/index-6afd4b06.js +542 -0
  3. package/{frontend/assets/sha512-70e9a0d9.js → admin/assets/sha512-f5f12dc1.js} +1 -1
  4. package/admin/index.html +1 -1
  5. package/central.json +30 -8
  6. package/frontend/assets/index-3a379840.js +94 -0
  7. package/frontend/assets/index-db5f0e4b.css +1 -0
  8. package/{admin/assets/sha512-2627feb7.js → frontend/assets/sha512-3392c31f.js} +1 -1
  9. package/frontend/index.html +2 -2
  10. package/package.json +1 -2
  11. package/plugins/download-counter/plugin.js +14 -15
  12. package/src/adminApis.js +1 -0
  13. package/src/api.auth.js +6 -6
  14. package/src/api.file_list.js +15 -8
  15. package/src/api.net.js +97 -72
  16. package/src/api.plugins.js +15 -26
  17. package/src/api.vfs.js +9 -12
  18. package/src/block.js +14 -1
  19. package/src/commands.js +1 -1
  20. package/src/comments.js +49 -0
  21. package/src/config.js +1 -5
  22. package/src/const.js +3 -2
  23. package/src/cross.js +43 -2
  24. package/src/customHtml.js +1 -1
  25. package/src/debounceAsync.js +18 -1
  26. package/src/frontEndApis.js +28 -6
  27. package/src/github.js +24 -30
  28. package/src/langs/hfs-lang-it.json +5 -3
  29. package/src/langs/hfs-lang-ru.json +6 -4
  30. package/src/listen.js +25 -15
  31. package/src/middlewares.js +3 -1
  32. package/src/misc.js +6 -5
  33. package/src/perm.js +19 -12
  34. package/src/plugins.js +62 -38
  35. package/src/serveFile.js +2 -3
  36. package/src/upload.js +5 -1
  37. package/src/util-files.js +38 -2
  38. package/src/util-http.js +8 -6
  39. package/src/vfs.js +24 -32
  40. package/src/watchLoad.js +5 -12
  41. package/src/zip.js +4 -4
  42. package/admin/assets/index-4e55b514.js +0 -536
  43. package/frontend/assets/index-cadcb0e9.css +0 -1
  44. package/frontend/assets/index-e24651da.js +0 -94
package/src/block.js CHANGED
@@ -6,8 +6,12 @@ const config_1 = require("./config");
6
6
  const connections_1 = require("./connections");
7
7
  const misc_1 = require("./misc");
8
8
  const block = (0, config_1.defineConfig)('block', [], rules => {
9
+ const now = new Date();
9
10
  const ret = !Array.isArray(rules) ? []
10
- : (0, misc_1.onlyTruthy)(rules.map(rule => (0, misc_1.makeNetMatcher)(rule.ip, true)));
11
+ : (0, misc_1.onlyTruthy)(rules.map(rule => {
12
+ rule.expire && (rule.expire = new Date(rule.expire));
13
+ return !(rule.expire > now) && (0, misc_1.makeNetMatcher)(rule.ip, true);
14
+ }));
11
15
  // reapply new block to existing connections
12
16
  for (const { socket, ip } of (0, connections_1.getConnections)())
13
17
  applyBlock(socket, ip);
@@ -18,3 +22,12 @@ function applyBlock(socket, ip = (0, connections_1.normalizeIp)(socket.remoteAdd
18
22
  return socket.destroy();
19
23
  }
20
24
  exports.applyBlock = applyBlock;
25
+ setInterval(() => {
26
+ const now = new Date();
27
+ const next = block.get().filter(x => !x.expire || x.expire > now);
28
+ const n = block.get().length - next.length;
29
+ if (!n)
30
+ return;
31
+ console.log("blocking rules:", n, "expired");
32
+ block.set(next);
33
+ }, misc_1.MINUTE / 2);
package/src/commands.js CHANGED
@@ -106,7 +106,7 @@ const commands = {
106
106
  },
107
107
  update: {
108
108
  params: '',
109
- cb: update_1.update
109
+ cb: () => (0, update_1.update)()
110
110
  },
111
111
  'check-update': {
112
112
  params: '',
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.setCommentFor = exports.getCommentFor = exports.descriptIon = exports.DESCRIPT_ION = void 0;
7
+ const config_1 = require("./config");
8
+ const path_1 = require("path");
9
+ const cross_1 = require("./cross");
10
+ const util_files_1 = require("./util-files");
11
+ const promises_1 = require("fs/promises");
12
+ const misc_1 = require("./misc");
13
+ const lodash_1 = __importDefault(require("lodash"));
14
+ exports.DESCRIPT_ION = 'descript.ion';
15
+ exports.descriptIon = (0, config_1.defineConfig)('descript_ion', true);
16
+ async function getCommentFor(path) {
17
+ return !path || !exports.descriptIon.get() ? undefined
18
+ : readDescription((0, path_1.dirname)(path)).then(x => x.get((0, cross_1.basename)(path)), () => undefined);
19
+ }
20
+ exports.getCommentFor = getCommentFor;
21
+ exports.setCommentFor = (0, misc_1.singleFromBatch)(async (jobs) => {
22
+ const byFolder = lodash_1.default.groupBy(jobs, job => (0, path_1.dirname)(job[0]));
23
+ return Promise.allSettled(lodash_1.default.map(byFolder, async (jobs, folder) => {
24
+ const comments = await readDescription(folder).catch(() => new Map());
25
+ for (const [path, comment] of jobs) {
26
+ const file = path.slice(folder.length + 1);
27
+ if (!comment)
28
+ comments.delete(file);
29
+ else
30
+ comments.set(file, comment);
31
+ }
32
+ // encode comments in descript.ion format
33
+ let txt = '';
34
+ comments.forEach((c, f) => txt += (f.includes(' ') ? `"${f}"` : f) + ' ' + (c.includes('\n') ? c.replaceAll('\n', '\\n') + MULTILINE_SUFFIX : c) + '\n');
35
+ await (0, promises_1.writeFile)((0, path_1.join)(folder, exports.DESCRIPT_ION), txt);
36
+ }));
37
+ });
38
+ const MULTILINE_SUFFIX = '\x04\xc2';
39
+ function readDescription(path) {
40
+ return (0, util_files_1.parseFile)((0, path_1.join)(path, exports.DESCRIPT_ION), txt => new Map(txt.split('\n').map(line => {
41
+ const quoted = line[0] === '"' ? 1 : 0;
42
+ const i = quoted ? line.indexOf('"', 2) + 1 : line.indexOf(' ');
43
+ const fn = line.slice(quoted, i - quoted);
44
+ let comment = line.slice(i + 1);
45
+ if (comment.endsWith(MULTILINE_SUFFIX))
46
+ comment = comment.slice(0, -2).replaceAll('\\n', '\n');
47
+ return [fn, comment];
48
+ })));
49
+ }
package/src/config.js CHANGED
@@ -89,11 +89,7 @@ function defineConfig(k, defaultValue, compiler) {
89
89
  else
90
90
  setConfig1(k, v);
91
91
  },
92
- compiled: () => {
93
- if (!compiler)
94
- throw "missing compiler";
95
- return compiled;
96
- }
92
+ compiled: () => compiled !== null && compiled !== void 0 ? compiled : (0, misc_1.throw_)("missing compiler"),
97
93
  };
98
94
  if (compiler)
99
95
  ret.sub((...args) => compiled = compiler(...args));
package/src/const.js CHANGED
@@ -27,7 +27,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
27
27
  return (mod && mod.__esModule) ? mod : { "default": mod };
28
28
  };
29
29
  Object.defineProperty(exports, "__esModule", { value: true });
30
- exports.APP_PATH = exports.IS_BINARY = exports.IS_MAC = exports.IS_WINDOWS = exports.HTTP_SERVICE_UNAVAILABLE = exports.HTTP_SERVER_ERROR = exports.HTTP_FAILED_DEPENDENCY = exports.HTTP_FOOL = exports.HTTP_RANGE_NOT_SATISFIABLE = exports.HTTP_PAYLOAD_TOO_LARGE = exports.HTTP_CONFLICT = exports.HTTP_NOT_ACCEPTABLE = exports.HTTP_METHOD_NOT_ALLOWED = exports.HTTP_NOT_FOUND = exports.HTTP_FORBIDDEN = exports.HTTP_UNAUTHORIZED = exports.HTTP_BAD_REQUEST = exports.HTTP_NOT_MODIFIED = exports.HTTP_TEMPORARY_REDIRECT = exports.HTTP_PARTIAL_CONTENT = exports.HTTP_NO_CONTENT = exports.HTTP_OK = exports.PLUGINS_PUB_URI = exports.API_URI = exports.ADMIN_URI = exports.FRONTEND_URI = exports.SPECIAL_URI = exports.HFS_REPO_BRANCH = exports.HFS_REPO = exports.COMPATIBLE_API_VERSION = exports.API_VERSION = exports.RUNNING_BETA = exports.VERSION = exports.BUILD_TIMESTAMP = exports.HFS_STARTED = exports.ORIGINAL_CWD = exports.DEV = exports.argv = void 0;
30
+ exports.APP_PATH = exports.IS_BINARY = exports.IS_MAC = exports.IS_WINDOWS = exports.HTTP_SERVICE_UNAVAILABLE = exports.HTTP_SERVER_ERROR = exports.HTTP_FAILED_DEPENDENCY = exports.HTTP_FOOL = exports.HTTP_RANGE_NOT_SATISFIABLE = exports.HTTP_PAYLOAD_TOO_LARGE = exports.HTTP_CONFLICT = exports.HTTP_NOT_ACCEPTABLE = exports.HTTP_METHOD_NOT_ALLOWED = exports.HTTP_NOT_FOUND = exports.HTTP_FORBIDDEN = exports.HTTP_UNAUTHORIZED = exports.HTTP_BAD_REQUEST = exports.HTTP_NOT_MODIFIED = exports.HTTP_TEMPORARY_REDIRECT = exports.HTTP_MOVED_PERMANENTLY = exports.HTTP_PARTIAL_CONTENT = exports.HTTP_NO_CONTENT = exports.HTTP_OK = exports.PLUGINS_PUB_URI = exports.API_URI = exports.ADMIN_URI = exports.FRONTEND_URI = exports.SPECIAL_URI = exports.HFS_REPO_BRANCH = exports.HFS_REPO = exports.COMPATIBLE_API_VERSION = exports.API_VERSION = exports.RUNNING_BETA = exports.VERSION = exports.BUILD_TIMESTAMP = exports.HFS_STARTED = exports.ORIGINAL_CWD = exports.DEV = exports.argv = void 0;
31
31
  const minimist_1 = __importDefault(require("minimist"));
32
32
  const fs = __importStar(require("fs"));
33
33
  const os_1 = require("os");
@@ -38,7 +38,7 @@ exports.DEV = process.env.DEV || exports.argv.dev ? 'DEV' : '';
38
38
  exports.ORIGINAL_CWD = process.cwd();
39
39
  exports.HFS_STARTED = new Date();
40
40
  const PKG_PATH = (0, path_1.join)(__dirname, '..', 'package.json');
41
- exports.BUILD_TIMESTAMP = "2023-09-21T14:29:22.907Z";
41
+ exports.BUILD_TIMESTAMP = "2023-10-04T19:41:32.601Z";
42
42
  const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf8'));
43
43
  exports.VERSION = pkg.version;
44
44
  exports.RUNNING_BETA = exports.VERSION.includes('-');
@@ -54,6 +54,7 @@ exports.PLUGINS_PUB_URI = exports.SPECIAL_URI + 'plugins/';
54
54
  exports.HTTP_OK = 200;
55
55
  exports.HTTP_NO_CONTENT = 204;
56
56
  exports.HTTP_PARTIAL_CONTENT = 206;
57
+ exports.HTTP_MOVED_PERMANENTLY = 301;
57
58
  exports.HTTP_TEMPORARY_REDIRECT = 302;
58
59
  exports.HTTP_NOT_MODIFIED = 304;
59
60
  exports.HTTP_BAD_REQUEST = 400;
package/src/cross.js CHANGED
@@ -3,7 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.xlate = exports.isEqualLax = exports.isWindowsDrive = exports.isIP = exports.isPrimitive = exports.formatTimestamp = exports.repeat = exports.asyncGeneratorToArray = exports.filterMapGenerator = exports.throw_ = exports.hasProp = exports.typedEntries = exports.typedKeys = exports.objRenameKey = exports.randomId = exports.getOrSet = exports.waitFor = exports.newObj = exports.removeStarting = exports.findDefined = exports.isOrderedEqual = exports.swap = exports.tryJson = exports.basename = exports.pendingPromise = exports._log = exports.wantArray = exports.formatPerc = exports.with_ = exports.try_ = exports.setHidden = exports.onlyTruthy = exports.truthy = exports.enforceFinal = exports.objSameKeys = exports.haveTimeout = exports.wait = exports.prefix = exports.formatBytes = exports.MAX_TILES_SIZE = exports.DAY = exports.HOUR = exports.MINUTE = exports.WIKI_URL = exports.REPO_URL = void 0;
6
+ exports.isWindowsDrive = exports.isIP = exports.isPrimitive = exports.formatTimestamp = exports.repeat = exports.asyncGeneratorToArray = exports.filterMapGenerator = exports.throw_ = exports.hasProp = exports.typedEntries = exports.typedKeys = exports.objRenameKey = exports.randomId = exports.getOrSet = exports.waitFor = exports.newObj = exports.removeStarting = exports.findDefined = exports.isOrderedEqual = exports.swap = exports.tryJson = exports.basename = exports.pendingPromise = exports._dbg = exports._log = exports.wantArray = exports.formatPerc = exports.with_ = exports.try_ = exports.setHidden = exports.onlyTruthy = exports.truthy = exports.enforceFinal = exports.objSameKeys = exports.haveTimeout = exports.wait = exports.prefix = exports.formatBytes = exports.isWhoObject = exports.PERM_KEYS = exports.defaultPerms = exports.WHO_ANY_ACCOUNT = exports.WHO_NO_ONE = exports.WHO_ANYONE = exports.MAX_TILES_SIZE = exports.DAY = exports.HOUR = exports.MINUTE = exports.WIKI_URL = exports.REPO_URL = void 0;
7
+ exports.promiseBestEffort = exports.escapeHTML = exports.ipForUrl = exports.ipLocalHost = exports.xlate = exports.isEqualLax = void 0;
7
8
  // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
8
9
  // all content here is shared between client and server
9
10
  const lodash_1 = __importDefault(require("lodash"));
@@ -13,6 +14,22 @@ exports.MINUTE = 60000;
13
14
  exports.HOUR = 60 * exports.MINUTE;
14
15
  exports.DAY = 24 * exports.HOUR;
15
16
  exports.MAX_TILES_SIZE = 10;
17
+ exports.WHO_ANYONE = true;
18
+ exports.WHO_NO_ONE = false;
19
+ exports.WHO_ANY_ACCOUNT = '*';
20
+ exports.defaultPerms = {
21
+ can_see: 'can_read',
22
+ can_read: exports.WHO_ANYONE,
23
+ can_list: 'can_read',
24
+ can_upload: exports.WHO_NO_ONE,
25
+ can_delete: exports.WHO_NO_ONE,
26
+ can_archive: 'can_read'
27
+ };
28
+ exports.PERM_KEYS = typedKeys(exports.defaultPerms);
29
+ function isWhoObject(v) {
30
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
31
+ }
32
+ exports.isWhoObject = isWhoObject;
16
33
  const MULTIPLIERS = ['', 'K', 'M', 'G', 'T'];
17
34
  function formatBytes(n, { post = 'B', k = 1024, digits = NaN } = {}) {
18
35
  if (isNaN(Number(n)) || n < 0)
@@ -86,6 +103,11 @@ function _log(...args) {
86
103
  return args[args.length - 1];
87
104
  }
88
105
  exports._log = _log;
106
+ function _dbg(x) {
107
+ debugger;
108
+ return x;
109
+ }
110
+ exports._dbg = _dbg;
89
111
  function pendingPromise() {
90
112
  let takeOut;
91
113
  const ret = new Promise((resolve, reject) => takeOut = { resolve, reject });
@@ -158,6 +180,7 @@ function newObj(src, returnNewValue, recur = false) {
158
180
  return Object.fromEntries(onlyTruthy(pairs));
159
181
  }
160
182
  exports.newObj = newObj;
183
+ // returns undefined if timeout is reached
161
184
  async function waitFor(cb, { interval = 200, timeout = Infinity } = {}) {
162
185
  const started = Date.now();
163
186
  while (1) {
@@ -225,7 +248,7 @@ async function asyncGeneratorToArray(generator) {
225
248
  }
226
249
  exports.asyncGeneratorToArray = asyncGeneratorToArray;
227
250
  function repeat(every, cb) {
228
- Promise.allSettled([cb()]).then(() => setTimeout(() => repeat(every, cb), every));
251
+ return Promise.allSettled([cb()]).then(() => setTimeout(() => repeat(every, cb), every));
229
252
  }
230
253
  exports.repeat = repeat;
231
254
  function formatTimestamp(x) {
@@ -255,3 +278,21 @@ function xlate(input, table) {
255
278
  return (_a = table[input]) !== null && _a !== void 0 ? _a : input;
256
279
  }
257
280
  exports.xlate = xlate;
281
+ function ipLocalHost(ip) {
282
+ return ip === '::1' || ip.endsWith('127.0.0.1');
283
+ }
284
+ exports.ipLocalHost = ipLocalHost;
285
+ function ipForUrl(ip) {
286
+ return ip.includes(':') ? '[' + ip + ']' : ip;
287
+ }
288
+ exports.ipForUrl = ipForUrl;
289
+ function escapeHTML(text) {
290
+ return text.replace(/[\u0000-\u002F\u003A-\u0040\u005B-\u0060\u007B-\u00FF]/g, c => '&#' + ('000' + c.charCodeAt(0)).slice(-4) + ';');
291
+ }
292
+ exports.escapeHTML = escapeHTML;
293
+ // wait for all, but returns only those that resolved
294
+ async function promiseBestEffort(promises) {
295
+ const res = await Promise.allSettled(promises);
296
+ return res.filter(x => x.status === 'fulfilled').map((x) => x.value);
297
+ }
298
+ exports.promiseBestEffort = promiseBestEffort;
package/src/customHtml.js CHANGED
@@ -35,7 +35,7 @@ function watchLoadCustomHtml(folder = '') {
35
35
  exports.watchLoadCustomHtml = watchLoadCustomHtml;
36
36
  function getSection(name) {
37
37
  return (exports.customHtmlState.sections.get(name) || '')
38
- + (0, plugins_1.mapPlugins)(pl => pl.getData().customHtml.get(name)).join('\n');
38
+ + (0, plugins_1.mapPlugins)(pl => { var _a; return (_a = pl.getData().customHtml) === null || _a === void 0 ? void 0 : _a.get(name); }).join('\n');
39
39
  }
40
40
  exports.getSection = getSection;
41
41
  async function saveCustomHtml(sections) {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.singleFromBatch = exports.debounceAsync = void 0;
4
5
  // like lodash.debounce, but also avoids async invocations to overlap
5
6
  function debounceAsync(callback, wait = 100, options = {}) {
6
7
  const { leading = false, maxWait = Infinity, cancelable = false, retain = 0, retainFailure } = options;
@@ -14,7 +15,9 @@ function debounceAsync(callback, wait = 100, options = {}) {
14
15
  let lastSince = 0;
15
16
  const interceptingWrapper = (...args) => runningDebouncer = debouncer(...args);
16
17
  return Object.assign(interceptingWrapper, {
18
+ clearRetain: () => last = undefined,
17
19
  flush: () => runningCallback !== null && runningCallback !== void 0 ? runningCallback : exec(),
20
+ isWorking: () => runningCallback,
18
21
  ...cancelable && {
19
22
  cancel() {
20
23
  waitingSince = 0;
@@ -58,4 +61,18 @@ function debounceAsync(callback, wait = 100, options = {}) {
58
61
  }
59
62
  }
60
63
  }
61
- exports.default = debounceAsync;
64
+ exports.debounceAsync = debounceAsync;
65
+ // given a function that works on a batch of requests, returns the function that works on a single request
66
+ function singleFromBatch(batchWorker) {
67
+ let batch = [];
68
+ const debounced = debounceAsync(async () => {
69
+ const ret = batchWorker(batch);
70
+ batch = [];
71
+ return ret;
72
+ });
73
+ return (...args) => {
74
+ batch.push(args);
75
+ return debounced();
76
+ };
77
+ }
78
+ exports.singleFromBatch = singleFromBatch;
@@ -39,6 +39,7 @@ const promises_1 = require("fs/promises");
39
39
  const path_1 = require("path");
40
40
  const upload_1 = require("./upload");
41
41
  const misc_1 = require("./misc");
42
+ const comments_1 = require("./comments");
42
43
  exports.frontEndApis = {
43
44
  get_file_list: api_file_list_1.get_file_list,
44
45
  ...api_auth,
@@ -95,6 +96,7 @@ exports.frontEndApis = {
95
96
  throw new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED);
96
97
  try {
97
98
  await (0, promises_1.rm)(node.source, { recursive: true });
99
+ (0, comments_1.setCommentFor)(node.source, '').then();
98
100
  return {};
99
101
  }
100
102
  catch (e) {
@@ -111,18 +113,38 @@ exports.frontEndApis = {
111
113
  if (!(0, vfs_1.hasPermission)(node, 'can_delete', ctx))
112
114
  throw new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED);
113
115
  try {
114
- if (node.name)
116
+ if (node.name) // virtual name = virtual rename
115
117
  node.name = dest;
116
- else if (node.source)
117
- await (0, promises_1.rename)(node.source, (0, path_1.join)((0, path_1.dirname)(node.source), dest));
118
- else
119
- throw new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR);
118
+ else {
119
+ if (!node.source)
120
+ throw new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY);
121
+ const destSource = (0, path_1.join)((0, path_1.dirname)(node.source), dest);
122
+ await (0, promises_1.rename)(node.source, destSource);
123
+ (0, comments_1.getCommentFor)(node.source).then(c => {
124
+ if (!c)
125
+ return;
126
+ (0, comments_1.setCommentFor)(node.source, '').then();
127
+ (0, comments_1.setCommentFor)(destSource, c).then();
128
+ });
129
+ }
120
130
  return {};
121
131
  }
122
132
  catch (e) {
123
133
  throw new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR, e);
124
134
  }
125
- }
135
+ },
136
+ async comment({ uri, comment }, ctx) {
137
+ (0, misc_1.apiAssertTypes)({ string: { uri, comment } });
138
+ const node = await (0, vfs_1.urlToNode)(uri, ctx);
139
+ if (!node)
140
+ throw new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND);
141
+ if (!(0, vfs_1.hasPermission)(node, 'can_upload', ctx))
142
+ throw new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED);
143
+ if (!node.source)
144
+ throw new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY);
145
+ await (0, comments_1.setCommentFor)(node.source, comment);
146
+ return {};
147
+ },
126
148
  };
127
149
  function notifyClient(ctx, name, data) {
128
150
  const { notificationChannel } = ctx.query;
package/src/github.js CHANGED
@@ -26,35 +26,37 @@ function downloadProgress(id, status) {
26
26
  // determine default branch, possibly without consuming api quota
27
27
  async function getGithubDefaultBranch(repo) {
28
28
  var _a;
29
- const res = await (0, misc_1.httpString)(`https://github.com/${repo}/archive/refs/heads/main.zip`, { method: 'HEAD' });
30
- return res.ok ? 'main'
31
- : (_a = (await getRepoInfo(repo))) === null || _a === void 0 ? void 0 : _a.default_branch;
29
+ const test = await (0, misc_1.httpString)(`https://github.com/${repo}/archive/refs/heads/main.zip`, { method: 'HEAD' }).then(() => 1, () => 0);
30
+ return test ? 'main' : (_a = (await getRepoInfo(repo))) === null || _a === void 0 ? void 0 : _a.default_branch;
32
31
  }
33
32
  async function downloadPlugin(repo, { branch = '', overwrite = false } = {}) {
34
- var _a, _b, _c;
33
+ var _a, _b, _c, _d;
35
34
  if (typeof repo !== 'string')
36
35
  repo = repo.main;
37
36
  if (downloading[repo])
38
- return new apiMiddleware_1.ApiError(const_1.HTTP_CONFLICT, "already downloading");
37
+ throw new apiMiddleware_1.ApiError(const_1.HTTP_CONFLICT, "already downloading");
38
+ const projectInfo = await (0, exports.getProjectInfo)();
39
+ if ((_a = projectInfo === null || projectInfo === void 0 ? void 0 : projectInfo.plugins_blacklist) === null || _a === void 0 ? void 0 : _a.includes(repo))
40
+ throw new apiMiddleware_1.ApiError(const_1.HTTP_FORBIDDEN, "blacklisted");
39
41
  console.log('downloading plugin', repo);
40
42
  downloadProgress(repo, true);
41
43
  try {
42
44
  if (repo.includes('//')) { // custom repo
43
45
  const pl = (0, plugins_1.findPluginByRepo)(repo);
44
46
  if (!pl)
45
- return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, "bad repo");
46
- const customRepo = (((_b = (_a = pl).getData) === null || _b === void 0 ? void 0 : _b.call(_a)) || pl).repo;
47
+ throw new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, "bad repo");
48
+ const customRepo = (((_c = (_b = pl).getData) === null || _c === void 0 ? void 0 : _c.call(_b)) || pl).repo;
47
49
  let url = customRepo === null || customRepo === void 0 ? void 0 : customRepo.zip;
48
50
  if (!url)
49
- return new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR, "bad plugin");
51
+ throw new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR, "bad plugin");
50
52
  if (!url.includes('//'))
51
53
  url = customRepo.web + url;
52
- return await go(url, pl === null || pl === void 0 ? void 0 : pl.id, (_c = customRepo.zipRoot) !== null && _c !== void 0 ? _c : DIST_ROOT);
54
+ return await go(url, pl === null || pl === void 0 ? void 0 : pl.id, (_d = customRepo.zipRoot) !== null && _d !== void 0 ? _d : DIST_ROOT);
53
55
  }
54
56
  branch || (branch = await getGithubDefaultBranch(repo));
55
57
  const short = repo.split('/')[1]; // second part, repo without the owner
56
58
  if (!short)
57
- return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, "bad repo");
59
+ throw new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, "bad repo");
58
60
  const folder = overwrite ? lodash_1.default.findKey(getFolder2repo(), x => x === repo) // use existing folder
59
61
  : getFolder2repo().hasOwnProperty(short) ? repo.replace('/', '-') // longer form only if another plugin is using short form, to avoid overwriting
60
62
  : short;
@@ -96,8 +98,7 @@ function getRepoInfo(id) {
96
98
  }
97
99
  exports.getRepoInfo = getRepoInfo;
98
100
  function readGithubFile(uri) {
99
- return (0, misc_1.httpString)('https://raw.githubusercontent.com/' + uri)
100
- .then(res => res.body);
101
+ return (0, misc_1.httpString)('https://raw.githubusercontent.com/' + uri);
101
102
  }
102
103
  exports.readGithubFile = readGithubFile;
103
104
  async function readOnlinePlugin(repo, branch = '') {
@@ -111,10 +112,7 @@ async function readOnlinePlugin(repo, branch = '') {
111
112
  throw Error("missing repo.main");
112
113
  if (!main.includes('//'))
113
114
  main = pl.repo.web + main;
114
- const res = await (0, misc_1.httpString)(main);
115
- if (!res.ok)
116
- throw Error("bad repo.main");
117
- return (0, plugins_1.parsePluginSource)(main, res.body); // use 'repo' as 'id' client-side
115
+ return (0, plugins_1.parsePluginSource)(main, await (0, misc_1.httpString)(main)); // use 'repo' as 'id' client-side
118
116
  }
119
117
  branch || (branch = await getGithubDefaultBranch(repo));
120
118
  const res = await readGithubFile(`${repo}/${branch}/${DIST_ROOT}/plugin.js`);
@@ -132,22 +130,16 @@ function getFolder2repo() {
132
130
  }
133
131
  exports.getFolder2repo = getFolder2repo;
134
132
  async function apiGithub(uri) {
135
- try {
136
- const res = await (0, misc_1.httpString)('https://api.github.com/' + uri, {
137
- headers: {
138
- 'User-Agent': 'HFS',
139
- Accept: 'application/vnd.github.v3+json',
140
- }
141
- });
142
- if (!res.ok)
143
- throw res.statusCode;
144
- return JSON.parse(res.body);
145
- }
146
- catch (e) {
133
+ return (0, misc_1.httpString)('https://api.github.com/' + uri, {
134
+ headers: {
135
+ 'User-Agent': 'HFS',
136
+ Accept: 'application/vnd.github.v3+json',
137
+ }
138
+ }).then(JSON.parse, e => {
147
139
  // https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#rate-limiting
148
140
  throw e.message === '403' ? Error('github_quota')
149
141
  : e;
150
- }
142
+ });
151
143
  }
152
144
  async function searchPlugins(text = '') {
153
145
  const projectInfo = await (0, exports.getProjectInfo)();
@@ -188,5 +180,7 @@ exports.searchPlugins = searchPlugins;
188
180
  // centralized hosted information, to be used as little as possible
189
181
  const FN = 'central.json';
190
182
  let builtIn = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '..', FN), 'utf8'));
191
- exports.getProjectInfo = (0, misc_1.debounceAsync)(() => readGithubFile(`${const_1.HFS_REPO}/${const_1.HFS_REPO_BRANCH}/${FN}`).then(JSON.parse, () => builtIn), // fall back to latest
183
+ exports.getProjectInfo = (0, misc_1.debounceAsync)(() => readGithubFile(`${const_1.HFS_REPO}/${const_1.HFS_REPO_BRANCH}/${FN}`)
184
+ .then(JSON.parse, () => null)
185
+ .then(x => Object.assign(Object.create(builtIn), const_1.DEV ? null : x)), // fall back to built-in
192
186
  0, { retain: misc_1.DAY, retainFailure: 60000 });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "author": "Massimo Melina",
3
3
  "version": 1.8,
4
- "hfs_version": "0.47.1",
4
+ "hfs_version": "0.49.0",
5
5
  "translate": {
6
6
  "Select": "Seleziona",
7
7
  "n_files": "{n} file",
@@ -121,8 +121,10 @@
121
121
  "showHelp_F_body": "pieno schermo",
122
122
  "Destination": "Destinazione",
123
123
  "in_queue": "{n} in coda",
124
- "": "PLUGINS SECTION",
125
- "": "PLUGIN thumbnails",
124
+ "enter_comment": "Comment for this file",
125
+ "Comment": "Comment",
126
+ "_PLUGINS SECTION": "",
127
+ "_PLUGIN thumbnails": "",
126
128
  "Enable tiles mode": "Modalità griglia",
127
129
  "thumbnails_switchBack": "Puoi tornare alla lista com'era prima entrando in Opzioni"
128
130
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "author": "Mefistofell, SanokKule",
3
- "version": 2.1,
4
- "hfs_version": "0.48.0",
3
+ "version": 2.2,
4
+ "hfs_version": "0.49.0",
5
5
  "translate": {
6
6
  "Select": "Выбор",
7
7
  "n_files": "{n,plural, one{1 файл} few{# файла} other{# файлов}}",
@@ -129,8 +129,10 @@
129
129
  "showHelp_F_body": "полноэкранный режим",
130
130
  "Destination": "Назначение",
131
131
  "in_queue": "{n} в очереди",
132
- "": "PLUGINS SECTION",
133
- "": "PLUGIN thumbnails",
132
+ "enter_comment": "Комментарий для файла",
133
+ "Comment": "Комментировать",
134
+ "_PLUGINS SECTION": "",
135
+ "_PLUGIN thumbnails": "",
134
136
  "Enable tiles mode": "Просмотр в режиме плиток",
135
137
  "thumbnails_switchBack": "Изменить вид можно в настройках"
136
138
  }
package/src/listen.js CHANGED
@@ -43,6 +43,8 @@ const adminApis_1 = require("./adminApis");
43
43
  const lodash_1 = __importDefault(require("lodash"));
44
44
  const crypto_1 = require("crypto");
45
45
  const api_net_1 = require("./api.net");
46
+ const events_1 = __importDefault(require("./events"));
47
+ const net_1 = require("net");
46
48
  let httpSrv;
47
49
  let httpsSrv;
48
50
  const openBrowserAtStart = (0, config_1.defineConfig)('open_browser_at_start', !const_1.DEV);
@@ -52,13 +54,11 @@ function getHttpsWorkingPort() {
52
54
  }
53
55
  exports.getHttpsWorkingPort = getHttpsWorkingPort;
54
56
  const commonOptions = { requestTimeout: 0 };
55
- exports.portCfg = (0, config_1.defineConfig)('port', 80);
56
- exports.portCfg.sub(async (port) => {
57
- while (!index_1.app)
58
- await (0, misc_1.wait)(100);
57
+ const considerHttp = (0, misc_1.debounceAsync)(async () => {
58
+ await (0, misc_1.waitFor)(() => index_1.app);
59
59
  stopServer(httpSrv).then();
60
60
  httpSrv = Object.assign(http.createServer(commonOptions, index_1.app.callback()), { name: 'http' });
61
- port = await startServer(httpSrv, { port });
61
+ const port = await startServer(httpSrv, { port: exports.portCfg.get(), host: listenInterface.get() });
62
62
  if (!port)
63
63
  return;
64
64
  httpSrv.on('connection', connections_1.newConnection);
@@ -66,6 +66,10 @@ exports.portCfg.sub(async (port) => {
66
66
  if (openBrowserAtStart.get() && !const_1.argv.updated)
67
67
  openAdmin();
68
68
  });
69
+ exports.portCfg = (0, config_1.defineConfig)('port', 80);
70
+ const listenInterface = (0, config_1.defineConfig)('listen_interface', '');
71
+ exports.portCfg.sub(considerHttp);
72
+ listenInterface.sub(considerHttp);
69
73
  function openAdmin() {
70
74
  for (const srv of [httpSrv, httpsSrv]) {
71
75
  const a = srv === null || srv === void 0 ? void 0 : srv.address();
@@ -84,6 +88,8 @@ function openAdmin() {
84
88
  }
85
89
  exports.openAdmin = openAdmin;
86
90
  function getCertObject() {
91
+ if (!httpsOptions.cert)
92
+ return;
87
93
  const o = new crypto_1.X509Certificate(httpsOptions.cert);
88
94
  const some = lodash_1.default.pick(o, ['subject', 'issuer', 'validFrom', 'validTo']);
89
95
  return (0, misc_1.objSameKeys)(some, v => (v === null || v === void 0 ? void 0 : v.includes('=')) ? Object.fromEntries(v.split('\n').map(x => x.split('='))) : v);
@@ -99,6 +105,8 @@ const considerHttps = (0, misc_1.debounceAsync)(async () => {
99
105
  httpsSrv = Object.assign(https.createServer(port === PORT_DISABLED ? {} : { ...commonOptions, key: httpsOptions.private_key, cert: httpsOptions.cert }, index_1.app.callback()), { name: 'https' });
100
106
  if (port >= 0) {
101
107
  const cert = getCertObject();
108
+ if (!cert)
109
+ return;
102
110
  const cn = (_a = cert.subject) === null || _a === void 0 ? void 0 : _a.CN;
103
111
  if (cn)
104
112
  console.log("certificate loaded for", cn);
@@ -120,11 +128,12 @@ const considerHttps = (0, misc_1.debounceAsync)(async () => {
120
128
  console.log("failed to create https server: check your private key and certificate", String(e));
121
129
  return;
122
130
  }
123
- port = await startServer(httpsSrv, { port });
131
+ port = await startServer(httpsSrv, { port, host: listenInterface.get() });
124
132
  if (!port)
125
133
  return;
126
134
  httpsSrv.on('connection', connections_1.newConnection);
127
135
  printUrls(httpsSrv.name);
136
+ events_1.default.emit('https ready');
128
137
  });
129
138
  exports.cert = (0, config_1.defineConfig)('cert', '');
130
139
  exports.privateKey = (0, config_1.defineConfig)('private_key', '');
@@ -150,6 +159,7 @@ for (const cfg of httpsNeeds) {
150
159
  const PORT_DISABLED = -1;
151
160
  exports.httpsPortCfg = (0, config_1.defineConfig)('https_port', PORT_DISABLED);
152
161
  exports.httpsPortCfg.sub(considerHttps);
162
+ listenInterface.sub(considerHttps);
153
163
  function startServer(srv, { port, host }) {
154
164
  return new Promise(async (resolve) => {
155
165
  if (!srv)
@@ -194,6 +204,8 @@ function startServer(srv, { port, host }) {
194
204
  srv.error = String(e);
195
205
  srv.busy = undefined;
196
206
  const { code } = e;
207
+ if (code === 'EACCES' && port < 1024)
208
+ srv.error = `lacking permission on port ${port}, try with permission (${const_1.IS_WINDOWS ? 'administrator' : 'sudo'}) or port > 1024`;
197
209
  if (code === 'EADDRINUSE') {
198
210
  srv.busy = (0, find_process_1.default)('port', port).then(res => { var _a; return ((_a = res === null || res === void 0 ? void 0 : res[0]) === null || _a === void 0 ? void 0 : _a.name) || ''; }, () => '');
199
211
  srv.error = `port ${port} busy: ${await srv.busy || "unknown process"}`;
@@ -242,36 +254,34 @@ async function getServerStatus() {
242
254
  }
243
255
  exports.getServerStatus = getServerStatus;
244
256
  const ignore = /^(lo|.*loopback.*|virtualbox.*|.*\(wsl\).*|llw\d|awdl\d|utun\d|anpi\d)$/i; // avoid giving too much information
245
- async function getIps() {
257
+ async function getIps(external = true) {
246
258
  const ips = (0, misc_1.onlyTruthy)(Object.entries((0, os_1.networkInterfaces)()).map(([name, nets]) => nets && !ignore.test(name)
247
259
  && v4first((0, misc_1.onlyTruthy)(nets.map(net => !net.internal && net.address)))[0] // for each interface we consider only 1 address
248
260
  )).flat();
249
- const e = await api_net_1.externalIp;
261
+ const e = external && api_net_1.externalIp;
250
262
  if (e && !ips.includes(e))
251
263
  ips.unshift(e);
252
264
  return v4first(ips)
253
265
  .filter((x, i, a) => a.length > 1 || !x.startsWith('169.254')); // 169.254 = dhcp failure on the interface, but keep it if it's our only one
254
266
  function v4first(a) {
255
- return lodash_1.default.sortBy(a, x => x.includes(':'));
267
+ return lodash_1.default.sortBy(a, net_1.isIPv6); // works because `false` comes first
256
268
  }
257
269
  }
258
270
  exports.getIps = getIps;
259
271
  async function getUrls() {
260
- const ips = (await getIps()).map(ip => ip.includes(':') ? '[' + ip + ']' : ip);
272
+ const on = listenInterface.get();
273
+ const ips = on ? [on] : await getIps();
261
274
  return Object.fromEntries((0, misc_1.onlyTruthy)([httpSrv, httpsSrv].map(srv => {
262
275
  var _a;
263
276
  if (!(srv === null || srv === void 0 ? void 0 : srv.listening))
264
277
  return false;
265
278
  const port = (_a = srv === null || srv === void 0 ? void 0 : srv.address()) === null || _a === void 0 ? void 0 : _a.port;
266
279
  const appendPort = port === (srv.name === 'https' ? 443 : 80) ? '' : ':' + port;
267
- const urls = ips.map(ip => `${srv.name}://${ip}${appendPort}`);
280
+ const urls = ips.map(ip => `${srv.name}://${(0, misc_1.ipForUrl)(ip)}${appendPort}`);
268
281
  return urls.length && [srv.name, urls];
269
282
  })));
270
283
  }
271
284
  exports.getUrls = getUrls;
272
285
  function printUrls(srvName) {
273
- getUrls().then(urls => {
274
- for (const url of urls[srvName])
275
- console.log('serving on', url);
276
- });
286
+ getUrls().then(urls => lodash_1.default.each(urls[srvName], url => console.log('serving on', url)));
277
287
  }
@@ -224,7 +224,9 @@ const prepareState = async (ctx, next) => {
224
224
  if (ctx.session)
225
225
  ctx.session.maxAge = exports.sessionDuration.compiled();
226
226
  // calculate these once and for all
227
- ctx.state.account = (_a = await getHttpAccount(ctx)) !== null && _a !== void 0 ? _a : (0, perm_1.getAccount)((_b = ctx.session) === null || _b === void 0 ? void 0 : _b.username, false);
227
+ const a = ctx.state.account = (_a = await getHttpAccount(ctx)) !== null && _a !== void 0 ? _a : (0, perm_1.getAccount)((_b = ctx.session) === null || _b === void 0 ? void 0 : _b.username, false);
228
+ if (a && !(0, perm_1.accountCanLogin)(a))
229
+ ctx.state.account = undefined;
228
230
  const conn = ctx.state.connection = (0, connections_1.socket2connection)(ctx.socket);
229
231
  ctx.state.revProxyPath = ctx.get('x-forwarded-prefix');
230
232
  if (conn)