hfs 0.42.3 → 0.44.0

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 (56) hide show
  1. package/README.md +56 -17
  2. package/admin/assets/index-35f6e3dc.css +1 -0
  3. package/admin/assets/index-ef68a7ab.js +510 -0
  4. package/{frontend/assets/sha512-f6798733.js → admin/assets/sha512-d091720e.js} +1 -1
  5. package/admin/index.html +2 -2
  6. package/frontend/assets/index-a3b0d6ac.js +94 -0
  7. package/frontend/assets/index-fe0f3d77.css +1 -0
  8. package/{admin/assets/sha512-69b26793.js → frontend/assets/sha512-96fd938f.js} +1 -1
  9. package/frontend/fontello.css +9 -3
  10. package/frontend/fontello.woff2 +0 -0
  11. package/frontend/index.html +3 -2
  12. package/package.json +2 -2
  13. package/plugins/antibrute/plugin.js +1 -1
  14. package/plugins/download-counter/plugin.js +14 -5
  15. package/plugins/download-counter/public/main.js +12 -2
  16. package/plugins/vhosting/plugin.js +17 -11
  17. package/src/adminApis.js +4 -4
  18. package/src/api.auth.js +4 -1
  19. package/src/api.file_list.js +22 -10
  20. package/src/api.lang.js +9 -11
  21. package/src/api.vfs.js +43 -29
  22. package/src/apiMiddleware.js +3 -2
  23. package/src/block.js +6 -20
  24. package/src/config.js +6 -2
  25. package/src/const.js +5 -4
  26. package/src/customHtml.js +2 -2
  27. package/src/debounceAsync.js +3 -3
  28. package/src/frontEndApis.js +7 -32
  29. package/src/github.js +26 -8
  30. package/src/index.js +2 -1
  31. package/src/lang.js +67 -0
  32. package/src/langs/embedded.js +13 -0
  33. package/src/langs/hfs-lang-it.json +100 -0
  34. package/src/langs/hfs-lang-ko.json +103 -0
  35. package/src/langs/hfs-lang-ms.json +70 -0
  36. package/src/langs/hfs-lang-ru.json +106 -0
  37. package/src/langs/hfs-lang-sr.json +108 -0
  38. package/src/langs/hfs-lang-zh-tw.json +106 -0
  39. package/src/langs/hfs-lang-zh.json +98 -0
  40. package/src/listen.js +8 -3
  41. package/src/log.js +6 -2
  42. package/src/middlewares.js +24 -18
  43. package/src/misc.js +17 -9
  44. package/src/perm.js +1 -1
  45. package/src/plugins.js +6 -6
  46. package/src/serveFile.js +2 -2
  47. package/src/serveGuiFiles.js +23 -10
  48. package/src/update.js +1 -2
  49. package/src/upload.js +6 -6
  50. package/src/util-http.js +5 -3
  51. package/src/vfs.js +114 -74
  52. package/src/zip.js +4 -4
  53. package/admin/assets/index-08017e15.js +0 -511
  54. package/admin/assets/index-94bbe0be.css +0 -1
  55. package/frontend/assets/index-5f125477.js +0 -94
  56. package/frontend/assets/index-a09cacfd.css +0 -1
package/src/perm.js CHANGED
@@ -41,7 +41,7 @@ function saveSrpInfo(account, salt, verifier) {
41
41
  account.srp = String(salt) + '|' + String(verifier);
42
42
  }
43
43
  exports.saveSrpInfo = saveSrpInfo;
44
- exports.allowClearTextLogin = (0, config_1.defineConfig)('allow_clear_text_login');
44
+ exports.allowClearTextLogin = (0, config_1.defineConfig)('allow_clear_text_login', false);
45
45
  const srp6aNimbusRoutines = new tssrp6a_1.SRPRoutines(new tssrp6a_1.SRPParameters());
46
46
  async function updateAccount(account, changer) {
47
47
  const was = JSON.stringify(account);
package/src/plugins.js CHANGED
@@ -231,7 +231,8 @@ async function rescan() {
231
231
  if (found.includes(id)) // not twice
232
232
  continue;
233
233
  found.push(id);
234
- loadPlugin(id, f);
234
+ if (!plugins[id]) // already loaded
235
+ loadPlugin(id, f);
235
236
  }
236
237
  for (const [id, p] of Object.entries(foundDisabled)) {
237
238
  const a = availablePlugins[id];
@@ -322,17 +323,16 @@ function deleteModule(id) {
322
323
  for (const child of (0, misc_1.wantArray)((_a = cache[k]) === null || _a === void 0 ? void 0 : _a.children))
323
324
  (0, misc_1.getOrSet)(requiredBy, child.id, () => []).push(k);
324
325
  const deleted = [];
325
- recur(id);
326
- function recur(id) {
327
- let mod = cache[id];
326
+ (function deleteCache(id) {
327
+ const mod = cache[id];
328
328
  if (!mod)
329
329
  return;
330
330
  delete cache[id];
331
331
  deleted.push(id);
332
332
  for (const child of mod.children)
333
333
  if (!lodash_1.default.difference(requiredBy[child.id], deleted).length)
334
- recur(child.id);
335
- }
334
+ deleteCache(child.id);
335
+ })(id);
336
336
  }
337
337
  (0, misc_1.onProcessExit)(() => Promise.allSettled(mapPlugins(pl => pl.unload())));
338
338
  function parsePluginSource(id, source) {
package/src/serveFile.js CHANGED
@@ -33,6 +33,8 @@ function serveFileNode(ctx, node) {
33
33
  }
34
34
  }
35
35
  ctx.vfsNode = node; // useful to tell service files from files shared by the user
36
+ if ('dl' in ctx.query) // please, download
37
+ ctx.attachment(name);
36
38
  return serveFile(ctx, source || '', mimeString);
37
39
  }
38
40
  exports.serveFileNode = serveFileNode;
@@ -41,8 +43,6 @@ async function serveFile(ctx, source, mime, content) {
41
43
  if (!source)
42
44
  return;
43
45
  const fn = path_1.default.basename(source);
44
- if ('dl' in ctx.params) // please, download
45
- ctx.attachment(fn);
46
46
  mime = mime !== null && mime !== void 0 ? mime : lodash_1.default.find(mimeCfg.get(), (v, k) => (0, misc_1.matches)(fn, k));
47
47
  if (mime === vfs_1.MIME_AUTO)
48
48
  mime = mime_types_1.default.lookup(source) || '';
@@ -41,6 +41,7 @@ const valtio_1 = require("valtio");
41
41
  const customHtml_1 = require("./customHtml");
42
42
  const lodash_1 = __importDefault(require("lodash"));
43
43
  const config_1 = require("./config");
44
+ const lang_1 = require("./lang");
44
45
  // in case of dev env we have our static files within the 'dist' folder'
45
46
  const DEV_STATIC = process.env.DEV ? 'dist/' : '';
46
47
  function serveStatic(uri) {
@@ -68,8 +69,6 @@ function serveStatic(uri) {
68
69
  return (0, serveFile_1.serveFile)(ctx, fullPath, 'auto', content);
69
70
  // we don't cache the index as it's small and may prevent plugins change to apply
70
71
  ctx.body = await treatIndex(ctx, uri, String(content));
71
- ctx.type = 'html';
72
- ctx.set('Cache-Control', 'no-store, no-cache, must-revalidate');
73
72
  };
74
73
  }
75
74
  function shouldServeApp(ctx) {
@@ -84,10 +83,10 @@ function adjustBundlerLinks(ctx, uri, data) {
84
83
  async function treatIndex(ctx, filesUri, body) {
85
84
  const session = await (0, api_auth_1.refresh_session)({}, ctx);
86
85
  ctx.set('etag', '');
86
+ ctx.set('Cache-Control', 'no-store, no-cache, must-revalidate');
87
+ ctx.type = 'html';
87
88
  const isFrontend = filesUri === const_1.FRONTEND_URI;
88
89
  const pub = ctx.state.revProxyPath + const_1.PLUGINS_PUB_URI;
89
- const css = (0, plugins_1.mapPlugins)((plug, k) => { var _a; return (_a = (isFrontend ? plug.frontend_css : null)) === null || _a === void 0 ? void 0 : _a.map(f => pub + k + '/' + f); }).flat().filter(Boolean);
90
- const js = (0, plugins_1.mapPlugins)((plug, k) => { var _a; return (_a = (isFrontend ? plug.frontend_js : null)) === null || _a === void 0 ? void 0 : _a.map(f => pub + k + '/' + f); }).flat().filter(Boolean);
91
90
  // expose plugins' configs that are declared with 'frontend' attribute
92
91
  const plugins = Object.fromEntries((0, misc_1.onlyTruthy)((0, plugins_1.mapPlugins)((pl, name) => {
93
92
  var _a, _b;
@@ -99,9 +98,10 @@ async function treatIndex(ctx, filesUri, body) {
99
98
  configs = ((_b = (_a = (0, plugins_1.getPluginInfo)(name)).onFrontendConfig) === null || _b === void 0 ? void 0 : _b.call(_a, configs)) || configs;
100
99
  return !lodash_1.default.isEmpty(configs) && [name, configs];
101
100
  })));
101
+ const lang = await (0, lang_1.getLangData)(ctx);
102
102
  let ret = body
103
103
  .replace(/((?:src|href) *= *['"])\/?(?![a-z]+:\/\/)/g, '$1' + ctx.state.revProxyPath + filesUri)
104
- .replace('</head>', () => `
104
+ .replace('<body>', () => `<body>
105
105
  ${!isFrontend ? '' : `
106
106
  <title>${adminApis_1.title.get()}</title>
107
107
  <link rel="icon" href="${adminApis_1.favicon.get() ? '/favicon.ico' : 'data:;'}" />
@@ -115,18 +115,31 @@ async function treatIndex(ctx, filesUri, body) {
115
115
  plugins,
116
116
  prefixUrl: ctx.state.revProxyPath,
117
117
  customHtml: lodash_1.default.omit(Object.fromEntries(customHtml_1.customHtmlState.sections), ['top', 'bottom']),
118
- fileMenuOnLink: fileMenuOnLink.get()
119
- }, null, 4)}
118
+ fileMenuOnLink: fileMenuOnLink.get(),
119
+ lang
120
+ }, null, 4)
121
+ .replace(/<(\/script)/g, '<"+"$1') /*avoid breaking our script container*/}
120
122
  document.documentElement.setAttribute('ver', '${const_1.VERSION.split('-')[0] /*for style selectors*/}')
123
+ HFS.getPluginKey = () => document.currentScript?.getAttribute('plugin')
124
+ || console.error("this function must be called at the very top of your file")
125
+ HFS.getPluginConfig = () => HFS.plugins[HFS.getPluginKey()]
121
126
  </script>
122
127
  <style>
123
128
  :root {
124
129
  ${lodash_1.default.map(plugins, (configs, pluginName) => lodash_1.default.map(configs, (v, k) => `--${pluginName}-${k}: ${serializeCss(v)};`).join('\n')).join('')}
125
130
  }
126
131
  </style>
127
- ${css.map(uri => `<link rel='stylesheet' type='text/css' href='${uri}'/>`).join('\n')}
128
- ${js.map(uri => `<script defer src='${uri}'></script>`).join('\n')}
129
- </head>`);
132
+ ${!isFrontend ? '' : (0, plugins_1.mapPlugins)((plug, k) => {
133
+ var _a;
134
+ return (_a = plug.frontend_css) === null || _a === void 0 ? void 0 : _a.map(f => `<link rel='stylesheet' type='text/css' href='${pub + k + '/' + f}' plugin=${JSON.stringify(k)}/>`);
135
+ })
136
+ .flat().filter(Boolean).join('\n')}
137
+ ${!isFrontend ? '' : (0, plugins_1.mapPlugins)((plug, k) => {
138
+ var _a;
139
+ return (_a = plug.frontend_js) === null || _a === void 0 ? void 0 : _a.map(f => `<script defer plugin=${JSON.stringify(k)} src='${pub + k + '/' + f}'></script>`);
140
+ })
141
+ .flat().filter(Boolean).join('\n')}
142
+ `);
130
143
  if (isFrontend)
131
144
  ret = ret
132
145
  .replace('<body>', '<body>' + (0, customHtml_1.getSection)('top'))
package/src/update.js CHANGED
@@ -10,9 +10,8 @@ const misc_1 = require("./misc");
10
10
  const fs_1 = require("fs");
11
11
  const plugins_1 = require("./plugins");
12
12
  const promises_1 = require("fs/promises");
13
- const HFS_REPO = 'rejetto/hfs';
14
13
  async function getUpdate() {
15
- const [latest] = await (0, github_1.getRepoInfo)(HFS_REPO + '/releases?per_page=1');
14
+ const [latest] = await (0, github_1.getRepoInfo)(const_1.HFS_REPO + '/releases?per_page=1');
16
15
  if (latest.name === const_1.VERSION)
17
16
  throw "you already have the latest version: " + const_1.VERSION;
18
17
  return latest;
package/src/upload.js CHANGED
@@ -15,14 +15,13 @@ const util_os_1 = require("./util-os");
15
15
  const connections_1 = require("./connections");
16
16
  const throttler_1 = require("./throttler");
17
17
  const lodash_1 = __importDefault(require("lodash"));
18
- exports.deleteUnfinishedUploadsAfter = (0, config_1.defineConfig)('delete_unfinished_uploads_after');
18
+ exports.deleteUnfinishedUploadsAfter = (0, config_1.defineConfig)('delete_unfinished_uploads_after', 86400);
19
19
  exports.minAvailableMb = (0, config_1.defineConfig)('min_available_mb', 100);
20
20
  const dontOverwriteUploading = (0, config_1.defineConfig)('dont_overwrite_uploading', false);
21
21
  const waitingToBeDeleted = {};
22
22
  function uploadWriter(base, path, ctx) {
23
- const res = (0, vfs_1.statusCodeForMissingPerm)(base, 'can_upload', ctx);
24
- if (res)
25
- return fail(res);
23
+ if ((0, vfs_1.statusCodeForMissingPerm)(base, 'can_upload', ctx))
24
+ return fail();
26
25
  const fullPath = (0, path_1.join)(base.source, path);
27
26
  const dir = (0, path_1.dirname)(fullPath);
28
27
  const min = exports.minAvailableMb.get() * (1 << 20);
@@ -36,7 +35,7 @@ function uploadWriter(base, path, ctx) {
36
35
  return fail(const_1.HTTP_PAYLOAD_TOO_LARGE);
37
36
  }
38
37
  catch (e) {
39
- console.warn("can't check disk size", e.message || String(e));
38
+ console.warn("can't check disk size:", e.message || String(e));
40
39
  }
41
40
  fs_1.default.mkdirSync(dir, { recursive: true });
42
41
  const keepName = (0, path_1.basename)(fullPath).slice(-200);
@@ -122,7 +121,8 @@ function uploadWriter(base, path, ctx) {
122
121
  delete waitingToBeDeleted[path];
123
122
  }
124
123
  function fail(status) {
125
- ctx.status = status;
124
+ if (status)
125
+ ctx.status = status;
126
126
  (0, frontEndApis_1.notifyClient)(ctx, 'upload.status', { [path]: ctx.status }); // allow browsers to detect failure while still sending body
127
127
  }
128
128
  }
package/src/util-http.js CHANGED
@@ -20,13 +20,15 @@ function httpsString(url, options = {}) {
20
20
  exports.httpsString = httpsString;
21
21
  function httpsStream(url, options = {}) {
22
22
  return new Promise((resolve, reject) => {
23
- node_https_1.default.request(url, options, res => {
23
+ const req = node_https_1.default.request(url, options, res => {
24
24
  if (!res.statusCode || res.statusCode >= 400)
25
- throw res;
25
+ return reject(new Error(String(res.statusCode), { cause: res }));
26
26
  if (res.statusCode === const_1.HTTP_TEMPORARY_REDIRECT && res.headers.location)
27
27
  return resolve(httpsStream(res.headers.location, options));
28
28
  resolve(res);
29
- }).on('error', reject).end();
29
+ }).on('error', e => {
30
+ reject(req.res || e);
31
+ }).end();
30
32
  });
31
33
  }
32
34
  exports.httpsStream = httpsStream;
package/src/vfs.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.walkNode = exports.statusCodeForMissingPerm = exports.hasPermission = exports.nodeIsDirectory = exports.getNodeName = exports.saveVfs = exports.vfs = exports.urlToNode = exports.isSameFilenameAs = exports.MIME_AUTO = exports.defaultPerms = void 0;
7
+ exports.parentMaskApplier = exports.masksCouldGivePermission = exports.walkNode = exports.statusCodeForMissingPerm = exports.hasPermission = exports.nodeIsDirectory = exports.getNodeName = exports.saveVfs = exports.vfs = exports.urlToNode = exports.applyParentToChild = exports.isSameFilenameAs = exports.MIME_AUTO = exports.PERM_KEYS = exports.defaultPerms = exports.WHO_ANY_ACCOUNT = exports.WHO_NO_ONE = exports.WHO_ANYONE = void 0;
8
8
  const promises_1 = __importDefault(require("fs/promises"));
9
9
  const path_1 = require("path");
10
10
  const misc_1 = require("./misc");
@@ -13,21 +13,25 @@ const config_1 = require("./config");
13
13
  const const_1 = require("./const");
14
14
  const events_1 = __importDefault(require("./events"));
15
15
  const perm_1 = require("./perm");
16
- const WHO_ANYONE = true;
17
- const WHO_NO_ONE = false;
18
- const WHO_ANY_ACCOUNT = '*';
16
+ exports.WHO_ANYONE = true;
17
+ exports.WHO_NO_ONE = false;
18
+ exports.WHO_ANY_ACCOUNT = '*';
19
19
  exports.defaultPerms = {
20
- can_see: WHO_ANYONE,
21
- can_read: WHO_ANYONE,
22
- can_list: WHO_ANYONE,
23
- can_upload: WHO_NO_ONE,
24
- can_delete: WHO_NO_ONE,
20
+ can_see: 'can_read',
21
+ can_read: exports.WHO_ANYONE,
22
+ can_list: 'can_read',
23
+ can_upload: exports.WHO_NO_ONE,
24
+ can_delete: exports.WHO_NO_ONE,
25
25
  };
26
+ exports.PERM_KEYS = (0, misc_1.typedKeys)(exports.defaultPerms);
26
27
  exports.MIME_AUTO = 'auto';
27
28
  function inheritFromParent(parent, child) {
28
29
  var _a, _b, _c;
29
- for (const k of (0, misc_1.typedKeys)(exports.defaultPerms))
30
- (_a = child[k]) !== null && _a !== void 0 ? _a : (child[k] = parent[k]);
30
+ for (const k of (0, misc_1.typedKeys)(exports.defaultPerms)) {
31
+ const v = parent[k];
32
+ if (v !== undefined) // small optimization: don't expand the object
33
+ (_a = child[k]) !== null && _a !== void 0 ? _a : (child[k] = v);
34
+ }
31
35
  if (typeof parent.mime === 'object' && typeof child.mime === 'object')
32
36
  lodash_1.default.defaults(child.mime, parent.mime);
33
37
  else
@@ -40,6 +44,20 @@ function isSameFilenameAs(name) {
40
44
  return (other) => lc === (typeof other === 'string' ? other : getNodeName(other)).toLowerCase();
41
45
  }
42
46
  exports.isSameFilenameAs = isSameFilenameAs;
47
+ function applyParentToChild(child, parent, name) {
48
+ const ret = {
49
+ ...child,
50
+ original: child,
51
+ isTemp: true,
52
+ parent,
53
+ };
54
+ name || (name = child ? getNodeName(child) : '');
55
+ inheritMasks(ret, parent, name);
56
+ parentMaskApplier(parent)(ret, name);
57
+ inheritFromParent(parent, ret);
58
+ return ret;
59
+ }
60
+ exports.applyParentToChild = applyParentToChild;
43
61
  async function urlToNode(url, ctx, parent = exports.vfs, getRest) {
44
62
  var _a;
45
63
  let initialSlashes = 0;
@@ -57,19 +75,11 @@ async function urlToNode(url, ctx, parent = exports.vfs, getRest) {
57
75
  }
58
76
  // does the tree node have a child that goes by this name?
59
77
  const child = (_a = parent.children) === null || _a === void 0 ? void 0 : _a.find(isSameFilenameAs(name));
60
- const ret = {
61
- ...child,
62
- original: child,
63
- isTemp: true,
64
- };
65
- inheritMasks(ret, parent, name);
66
- applyMasks(ret, parent, name);
67
- inheritFromParent(parent, ret);
78
+ if (!child && !parent.source)
79
+ return; // on tree or on disk
80
+ const ret = applyParentToChild(child, parent, name);
68
81
  if (child) // yes
69
82
  return urlToNode(rest, ctx, ret, getRest);
70
- // not in the tree, we can see consider continuing on the disk
71
- if (!parent.source)
72
- return; // but then we need the current node to be linked to the disk, otherwise, we give up
73
83
  let onDisk = name;
74
84
  if (parent.rename) { // reverse the mapping
75
85
  for (const [from, to] of Object.entries(parent.rename))
@@ -124,27 +134,47 @@ async function nodeIsDirectory(node) {
124
134
  }
125
135
  exports.nodeIsDirectory = nodeIsDirectory;
126
136
  function hasPermission(node, perm, ctx) {
127
- var _a;
128
- return (node.source || perm !== 'can_upload') // Upload possible only if we know where to store. First check node.source because is supposedly faster.
129
- && matchWho((_a = node[perm]) !== null && _a !== void 0 ? _a : exports.defaultPerms[perm], ctx);
137
+ return !statusCodeForMissingPerm(node, perm, ctx, false);
130
138
  }
131
139
  exports.hasPermission = hasPermission;
132
- function statusCodeForMissingPerm(node, perm, ctx) {
133
- if (hasPermission(node, perm, ctx))
134
- return false;
135
- return ctx.status = node[perm] === false ? const_1.HTTP_FORBIDDEN : const_1.HTTP_UNAUTHORIZED;
140
+ function statusCodeForMissingPerm(node, perm, ctx, assign = true) {
141
+ const ret = getCode();
142
+ if (ret && assign)
143
+ ctx.status = ret;
144
+ return ret;
145
+ function getCode() {
146
+ var _a;
147
+ if (!node.source && perm === 'can_upload') // Upload possible only if we know where to store. First check node.source because is supposedly faster.
148
+ return const_1.HTTP_FORBIDDEN;
149
+ // calculate value of permission resolving references to other permissions, avoiding infinite loop
150
+ let who;
151
+ let max = exports.PERM_KEYS.length;
152
+ do {
153
+ who = (_a = node[perm]) !== null && _a !== void 0 ? _a : exports.defaultPerms[perm];
154
+ if (!max-- || typeof who !== 'string' || who === exports.WHO_ANY_ACCOUNT)
155
+ break;
156
+ perm = who;
157
+ } while (1);
158
+ if (Array.isArray(who)) {
159
+ const arr = who; // shut up ts
160
+ // check if I or any ancestor match `who`, but cache ancestors' usernames inside context state
161
+ const some = (0, misc_1.getOrSet)(ctx.state, 'usernames', () => (0, perm_1.getCurrentUsernameExpanded)(ctx))
162
+ .some((u) => arr.includes(u));
163
+ return some ? 0 : const_1.HTTP_UNAUTHORIZED;
164
+ }
165
+ return typeof who === 'boolean' ? (who ? 0 : const_1.HTTP_FORBIDDEN)
166
+ : who === exports.WHO_ANY_ACCOUNT ? (ctx.state.account ? 0 : const_1.HTTP_UNAUTHORIZED)
167
+ : (() => { throw Error('invalid permission: ' + who); })();
168
+ }
136
169
  }
137
170
  exports.statusCodeForMissingPerm = statusCodeForMissingPerm;
138
171
  // it's responsibility of the caller to verify you have list permission on parent, as callers have different needs.
139
172
  // Too many parameters: consider object, but benchmark against degraded recursion on huge folders.
140
173
  async function* walkNode(parent, ctx, depth = 0, prefixPath = '', requiredPerm) {
141
174
  var _a;
142
- if (requiredPerm && ctx
143
- && !hasPermission(parent, requiredPerm, ctx)
144
- && !masksCouldGivePermission(parent.masks))
145
- return; // no permission, no reason to continue
146
175
  const { children, source } = parent;
147
176
  const took = prefixPath ? undefined : new Set();
177
+ const maskApplier = parentMaskApplier(parent);
148
178
  if (children)
149
179
  for (const child of children) {
150
180
  const nodeName = getNodeName(child);
@@ -162,6 +192,10 @@ async function* walkNode(parent, ctx, depth = 0, prefixPath = '', requiredPerm)
162
192
  }
163
193
  if (!source)
164
194
  return;
195
+ if (requiredPerm && ctx // no permission, no reason to continue (at least for dynamic elements)
196
+ && !hasPermission(parent, requiredPerm, ctx)
197
+ && !masksCouldGivePermission(parent.masks, requiredPerm))
198
+ return;
165
199
  try {
166
200
  let lastDir = prefixPath.slice(0, -1) || '.';
167
201
  const map = new Map();
@@ -196,46 +230,58 @@ async function* walkNode(parent, ctx, depth = 0, prefixPath = '', requiredPerm)
196
230
  // item will be changed, so be sure to pass a temp node
197
231
  function canSee(item) {
198
232
  // we basename for depth>0 where we already have the rest of the path in the parent's url, and would be duplicated
199
- applyMasks(item, parent, (0, path_1.basename)(getNodeName(item)));
233
+ maskApplier(item, (0, path_1.basename)(getNodeName(item)));
200
234
  inheritFromParent(parent, item);
201
235
  if (ctx && !hasPermission(item, 'can_see', ctx))
202
236
  return;
203
237
  item.isTemp = true;
204
238
  return item;
205
239
  }
206
- function masksCouldGivePermission(masks) {
207
- if (!masks)
208
- return false;
209
- for (const [, props] of Object.entries(masks)) {
210
- const v = props[requiredPerm];
211
- if (v && (!ctx || matchWho(v, ctx))) // without ctx we can't say, so it could
212
- return true;
213
- if (masksCouldGivePermission(props.masks))
214
- return true;
215
- }
216
- return false;
217
- }
218
240
  }
219
241
  exports.walkNode = walkNode;
220
- function applyMasks(item, parent, virtualBasename) {
221
- const { masks } = parent;
222
- if (!masks)
223
- return;
224
- for (const [k, v] of Object.entries(masks))
225
- if (k.startsWith('**/') && (0, misc_1.matches)(virtualBasename, k.slice(3))
226
- || !k.includes('/') && (0, misc_1.matches)(virtualBasename, k))
227
- lodash_1.default.defaults(item, v);
242
+ function masksCouldGivePermission(masks, perm) {
243
+ return masks !== undefined && Object.values(masks).some(props => props[perm] || masksCouldGivePermission(props.masks, perm));
244
+ }
245
+ exports.masksCouldGivePermission = masksCouldGivePermission;
246
+ function parentMaskApplier(parent) {
247
+ const matchers = Object.entries(parent.masks || {}).map(([k, v]) => {
248
+ k = k.startsWith('**/') ? k.slice(3) : !k.includes('/') ? k : '';
249
+ if (!k)
250
+ return;
251
+ const m = (0, misc_1.makeMatcher)(k);
252
+ return [m, v];
253
+ });
254
+ return (item, virtualBasename) => {
255
+ if (virtualBasename === undefined)
256
+ virtualBasename = getNodeName(item);
257
+ for (const entry of matchers) {
258
+ if (!entry)
259
+ continue;
260
+ const [matcher, mods] = entry;
261
+ if (!matcher(virtualBasename))
262
+ continue;
263
+ if (item.masks)
264
+ item.masks = lodash_1.default.merge(lodash_1.default.cloneDeep(mods.masks), item.masks); // item.masks must take precedence
265
+ lodash_1.default.defaults(item, mods);
266
+ }
267
+ };
228
268
  }
269
+ exports.parentMaskApplier = parentMaskApplier;
229
270
  function inheritMasks(item, parent, virtualBasename) {
230
271
  const { masks } = parent;
231
272
  if (!masks)
232
273
  return;
233
274
  const o = {};
234
- for (const [k, v] of Object.entries(masks))
235
- if (k.startsWith('**/'))
236
- o[k.slice(3)] = v;
237
- else if (k.startsWith(virtualBasename + '/'))
238
- o[k.slice(virtualBasename.length + 1)] = v;
275
+ for (const [k, v] of Object.entries(masks)) {
276
+ const neg = k[0] === '!' && k[1] !== '(' ? '!' : '';
277
+ const withoutNeg = neg ? k.slice(1) : k;
278
+ if (withoutNeg.startsWith('**'))
279
+ o[k] = v;
280
+ else if (withoutNeg.startsWith('*/'))
281
+ o[neg + withoutNeg.slice(2)] = v;
282
+ else if (withoutNeg.startsWith(virtualBasename + '/'))
283
+ o[neg + withoutNeg.slice(virtualBasename.length + 1)] = v;
284
+ }
239
285
  if (Object.keys(o).length)
240
286
  item.masks = lodash_1.default.defaults(item.masks, o);
241
287
  }
@@ -247,24 +293,18 @@ function renameUnderPath(rename, path) {
247
293
  delete rename[''];
248
294
  return lodash_1.default.isEmpty(rename) ? undefined : rename;
249
295
  }
250
- function matchWho(who, ctx) {
251
- return who === WHO_ANYONE
252
- || who === WHO_ANY_ACCOUNT && Boolean(ctx.state.account)
253
- || Array.isArray(who) // check if I or any ancestor match `who`, but cache ancestors' usernames inside context state
254
- && (0, misc_1.getOrSet)(ctx.state, 'usernames', () => (0, perm_1.getCurrentUsernameExpanded)(ctx)).some((u) => who.includes(u));
255
- }
256
296
  events_1.default.on('accountRenamed', (from, to) => {
257
- recur(exports.vfs);
258
- saveVfs();
259
- function recur(n) {
297
+ ;
298
+ (function renameInNode(n) {
260
299
  var _a;
261
- for (const k of (0, misc_1.typedKeys)(exports.defaultPerms))
262
- replace(n[k]);
300
+ for (const k of exports.PERM_KEYS)
301
+ renameInPerm(n[k]);
263
302
  if (n.masks)
264
- Object.values(n.masks).forEach(recur);
265
- (_a = n.children) === null || _a === void 0 ? void 0 : _a.forEach(recur);
266
- }
267
- function replace(a) {
303
+ Object.values(n.masks).forEach(renameInNode);
304
+ (_a = n.children) === null || _a === void 0 ? void 0 : _a.forEach(renameInNode);
305
+ })(exports.vfs);
306
+ saveVfs();
307
+ function renameInPerm(a) {
268
308
  if (!Array.isArray(a))
269
309
  return;
270
310
  for (let i = 0; i < a.length; i++)
package/src/zip.js CHANGED
@@ -26,16 +26,16 @@ async function zipStreamFromFolder(node, ctx) {
26
26
  const filter = (0, misc_1.pattern2filter)(String(ctx.query.search || ''));
27
27
  const walker = !list ? (0, vfs_1.walkNode)(node, ctx, Infinity, '', 'can_read')
28
28
  : (async function* () {
29
- for await (const el of list) {
30
- const subNode = await (0, vfs_1.urlToNode)(el, ctx, node);
29
+ for await (const uri of list) {
30
+ const subNode = await (0, vfs_1.urlToNode)(uri, ctx, node);
31
31
  if (!subNode)
32
32
  continue;
33
33
  if (await (0, vfs_1.nodeIsDirectory)(subNode)) { // a directory needs to walked
34
34
  if ((0, vfs_1.hasPermission)(subNode, 'can_list', ctx))
35
- yield* (0, vfs_1.walkNode)(subNode, ctx, Infinity, el + '/', 'can_read');
35
+ yield* (0, vfs_1.walkNode)(subNode, ctx, Infinity, uri + '/', 'can_read');
36
36
  continue;
37
37
  }
38
- let folder = (0, path_1.dirname)(el);
38
+ let folder = (0, path_1.dirname)(decodeURIComponent(uri)); // decodeURI() won't account for %23=#
39
39
  folder = folder === '.' ? '' : folder + '/';
40
40
  yield { ...subNode, name: folder + (0, vfs_1.getNodeName)(subNode) }; // reflect relative path in archive, otherwise way may have name-clashes
41
41
  }