hfs 0.40.1 → 0.41.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.
@@ -1,4 +1,4 @@
1
- import{c as SF}from"./index-87c2ebd1.js";function OF(iF,hF){for(var eF=0;eF<hF.length;eF++){const tF=hF[eF];if(typeof tF!="string"&&!Array.isArray(tF)){for(const w in tF)if(w!=="default"&&!(w in iF)){const lF=Object.getOwnPropertyDescriptor(tF,w);lF&&Object.defineProperty(iF,w,lF.get?lF:{enumerable:!0,get:()=>tF[w]})}}}return Object.freeze(Object.defineProperty(iF,Symbol.toStringTag,{value:"Module"}))}var EF={},UF={get exports(){return EF},set exports(iF){EF=iF}};/*
1
+ import{c as SF}from"./index-7ceb2f4c.js";function OF(iF,hF){for(var eF=0;eF<hF.length;eF++){const tF=hF[eF];if(typeof tF!="string"&&!Array.isArray(tF)){for(const w in tF)if(w!=="default"&&!(w in iF)){const lF=Object.getOwnPropertyDescriptor(tF,w);lF&&Object.defineProperty(iF,w,lF.get?lF:{enumerable:!0,get:()=>tF[w]})}}}return Object.freeze(Object.defineProperty(iF,Symbol.toStringTag,{value:"Module"}))}var EF={},UF={get exports(){return EF},set exports(iF){EF=iF}};/*
2
2
  * [js-sha512]{@link https://github.com/emn178/js-sha512}
3
3
  *
4
4
  * @version 0.8.0
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0" />
6
6
  <link href="/fontello.css" rel="stylesheet" />
7
7
 
8
- <script type="module" crossorigin src="/assets/index-9f5a7cc5.js"></script>
9
- <link rel="stylesheet" href="/assets/index-ffbcd4c9.css">
8
+ <script type="module" crossorigin src="/assets/index-7ceb2f4c.js"></script>
9
+ <link rel="stylesheet" href="/assets/index-25b49014.css">
10
10
  </head>
11
11
  <body>
12
12
  <noscript>You need to enable JavaScript to run this app.</noscript>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hfs",
3
- "version": "0.40.1",
3
+ "version": "0.41.0",
4
4
  "description": "HTTP File Server",
5
5
  "keywords": [
6
6
  "file server",
@@ -77,6 +77,7 @@
77
77
  "open": "^8.4.0",
78
78
  "tssrp6a": "^3.0.0",
79
79
  "unzip-stream": "^0.3.1",
80
+ "valtio": "^1.10.3",
80
81
  "yaml": "^2.0.0-10"
81
82
  },
82
83
  "devDependencies": {
@@ -17,8 +17,9 @@ const file_list = async ({ path, offset, limit, search, omit, sse }, ctx) => {
17
17
  const list = new apiMiddleware_1.SendListReadable();
18
18
  if (!node)
19
19
  return fail(const_1.HTTP_NOT_FOUND);
20
- if (!(0, vfs_1.hasPermission)(node, 'can_read', ctx))
21
- return fail((0, vfs_1.cantReadStatusCode)(node));
20
+ const res = (0, vfs_1.statusCodeForMissingPerm)(node, 'can_list', ctx);
21
+ if (res)
22
+ return fail(res);
22
23
  if ((0, misc_1.dirTraversal)(search))
23
24
  return fail(const_1.HTTP_FOOL);
24
25
  if (node.default)
@@ -50,8 +51,7 @@ const file_list = async ({ path, offset, limit, search, omit, sse }, ctx) => {
50
51
  function fail(code) {
51
52
  if (!sse)
52
53
  return new apiMiddleware_1.ApiError(code);
53
- list.error(code);
54
- list.close();
54
+ list.error(code, true);
55
55
  return list;
56
56
  }
57
57
  async function* produceEntries() {
package/src/api.lang.js CHANGED
@@ -13,7 +13,7 @@ const misc_1 = require("./misc");
13
13
  const PREFIX = 'hfs-lang-';
14
14
  const SUFFIX = '.json';
15
15
  const apis = {
16
- get_langs() {
16
+ list_langs() {
17
17
  return new apiMiddleware_1.SendListReadable({
18
18
  doAtStart: async (list) => {
19
19
  for await (let name of fast_glob_1.default.stream(code2file('*'))) {
package/src/api.vfs.js CHANGED
@@ -151,21 +151,21 @@ const apis = {
151
151
  }
152
152
  try {
153
153
  path = (0, misc_1.isWindowsDrive)(path) ? path + '\\' : (0, path_1.resolve)(path || '/');
154
- for await (const name of (0, misc_1.dirStream)(path)) {
154
+ for await (const [name, isDir] of (0, misc_1.dirStream)(path)) {
155
155
  if (ctx.req.aborted)
156
156
  return;
157
157
  try {
158
- const stats = await (0, promises_1.stat)((0, path_1.join)(path, name));
159
- if (stats.isFile())
158
+ if (!isDir)
160
159
  if (!files || fileMask && !(0, micromatch_1.isMatch)(name, fileMask))
161
160
  continue;
161
+ const stats = await (0, promises_1.stat)((0, path_1.join)(path, name));
162
162
  yield {
163
163
  add: {
164
164
  n: name,
165
165
  s: stats.size,
166
166
  c: stats.ctime,
167
167
  m: stats.mtime,
168
- k: stats.isDirectory() ? 'd' : undefined,
168
+ k: isDir ? 'd' : undefined,
169
169
  }
170
170
  };
171
171
  }
package/src/const.js CHANGED
@@ -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-03-14T17:37:51.823Z";
41
+ exports.BUILD_TIMESTAMP = "2023-03-18T22:44:28.592Z";
42
42
  const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf8'));
43
43
  exports.VERSION = pkg.version;
44
44
  exports.DAY = 86400000;
@@ -89,23 +89,25 @@ exports.frontEndApis = {
89
89
  },
90
90
  async load_lang({ lang, embedded }) {
91
91
  const ret = {};
92
- const langs = (0, misc_1.wantArray)(lang);
93
- for (let k of langs) {
94
- k = k.toLowerCase();
95
- if (k === embedded)
92
+ const langs = (0, misc_1.wantArray)(lang).map(x => x.toLowerCase());
93
+ let i = 0;
94
+ while (i < langs.length) {
95
+ let x = langs[i];
96
+ if (x === embedded)
96
97
  break;
97
98
  try {
98
- ret[k] = JSON.parse(await (0, promises_1.readFile)(`hfs-lang-${k}.json`, 'utf8'));
99
+ ret[x] = JSON.parse(await (0, promises_1.readFile)(`hfs-lang-${x}.json`, 'utf8'));
99
100
  }
100
101
  catch (_a) {
101
- while (1) {
102
- k = k.substring(0, k.lastIndexOf('-'));
103
- if (!k)
104
- break;
105
- if (!langs.includes(k))
106
- langs.push(k);
102
+ do {
103
+ x = x.substring(0, x.lastIndexOf('-'));
104
+ } while (x && langs.includes(x));
105
+ if (x) {
106
+ langs[i] = x; // overwrite and retry
107
+ continue;
107
108
  }
108
109
  }
110
+ i++;
109
111
  }
110
112
  return ret;
111
113
  }
@@ -98,7 +98,9 @@ const serveGuiAndSharedFiles = async (ctx, next) => {
98
98
  }
99
99
  if (ctx.originalUrl === '/favicon.ico' && adminApis_1.favicon.get()) // originalUrl to not be subject to changes (vhosting plugin)
100
100
  return (0, serveFile_1.serveFile)(ctx, adminApis_1.favicon.get());
101
- const node = await (0, vfs_1.urlToNode)(path, ctx);
101
+ let node = await (0, vfs_1.urlToNode)(path, ctx);
102
+ if ((node === null || node === void 0 ? void 0 : node.default) && (path.endsWith('/') || !node.default.match(/\.html?$/i))) // final/ needed on browser to make resource urls correctly
103
+ node = await (0, vfs_1.urlToNode)(node.default, ctx, node);
102
104
  if (!node)
103
105
  return ctx.status = const_1.HTTP_NOT_FOUND;
104
106
  if (ctx.method === 'POST') { // curl -F upload=@file url/
@@ -112,15 +114,13 @@ const serveGuiAndSharedFiles = async (ctx, next) => {
112
114
  await (0, stream_1.once)(form, 'end').catch(() => { });
113
115
  return;
114
116
  }
115
- const canRead = (0, vfs_1.hasPermission)(node, 'can_read', ctx);
116
- const isFolder = await (0, vfs_1.nodeIsDirectory)(node);
117
- if (isFolder && !path.endsWith('/'))
118
- return ctx.redirect(ctx.state.revProxyPath + ctx.originalUrl + '/');
119
- if (canRead && !isFolder)
120
- return node.source ? (0, serveFile_1.serveFileNode)(ctx, node)
121
- : next();
122
- if (!canRead) {
123
- ctx.status = (0, vfs_1.cantReadStatusCode)(node);
117
+ if (!await (0, vfs_1.nodeIsDirectory)(node))
118
+ return !node.source && await next()
119
+ || (0, vfs_1.statusCodeForMissingPerm)(node, 'can_read', ctx)
120
+ || (0, serveFile_1.serveFileNode)(ctx, node);
121
+ if (!path.endsWith('/'))
122
+ return ctx.redirect(ctx.state.revProxyPath + ctx.originalUrl.replace(/(\?|$)/, '/$1')); // keep query-string, if any
123
+ if ((0, vfs_1.statusCodeForMissingPerm)(node, 'can_list', ctx)) {
124
124
  if (ctx.status === const_1.HTTP_FORBIDDEN)
125
125
  return;
126
126
  const browserDetected = ctx.get('Upgrade-Insecure-Requests') || ctx.get('Sec-Fetch-Mode'); // ugh, heuristics
@@ -130,16 +130,8 @@ const serveGuiAndSharedFiles = async (ctx, next) => {
130
130
  return serveFrontendFiles(ctx, next);
131
131
  }
132
132
  ctx.set({ server: 'HFS ' + const_1.BUILD_TIMESTAMP });
133
- const { get } = ctx.query;
134
- if (get === 'zip')
135
- return await (0, zip_1.zipStreamFromFolder)(node, ctx);
136
- if (node.default) {
137
- const def = await (0, vfs_1.urlToNode)(path + node.default, ctx);
138
- return !def ? next()
139
- : (0, vfs_1.hasPermission)(def, 'can_read', ctx) ? (0, serveFile_1.serveFileNode)(ctx, def)
140
- : ctx.status = (0, vfs_1.cantReadStatusCode)(def);
141
- }
142
- return serveFrontendFiles(ctx, next);
133
+ return ctx.query.get === 'zip' ? (0, zip_1.zipStreamFromFolder)(node, ctx)
134
+ : serveFrontendFiles(ctx, next);
143
135
  };
144
136
  exports.serveGuiAndSharedFiles = serveGuiAndSharedFiles;
145
137
  let proxyDetected = false;
package/src/misc.js CHANGED
@@ -47,17 +47,21 @@ function setHidden(dest, src) {
47
47
  })));
48
48
  }
49
49
  exports.setHidden = setHidden;
50
- function newObj(src, newValue) {
50
+ function newObj(src, returnNewValue, recur = false) {
51
51
  if (!src)
52
52
  return {};
53
53
  const pairs = Object.entries(src).map(([k, v]) => {
54
54
  if (typeof k === 'symbol')
55
55
  return;
56
56
  let _k = k;
57
- const newV = newValue(v, k, (newK) => {
57
+ const curDepth = typeof recur === 'number' ? recur : 0;
58
+ let newV = returnNewValue(v, k, (newK) => {
58
59
  _k = newK;
59
60
  return true; // for convenient expression concatenation
60
- });
61
+ }, curDepth);
62
+ if ((recur !== false || returnNewValue.length === 4) // if callback is using depth parameter, then it wants recursion
63
+ && lodash_1.default.isPlainObject(newV)) // is it recurrable?
64
+ newV = newObj(newV, returnNewValue, curDepth + 1);
61
65
  return _k !== undefined && [_k, newV];
62
66
  });
63
67
  return Object.fromEntries(onlyTruthy(pairs));
package/src/serveFile.js CHANGED
@@ -41,7 +41,7 @@ async function serveFile(ctx, source, mime, content) {
41
41
  if (!source)
42
42
  return;
43
43
  const fn = path_1.default.basename(source);
44
- if (ctx.params.dl !== undefined) // please, download
44
+ if ('dl' in ctx.params) // please, download
45
45
  ctx.attachment(fn);
46
46
  mime = mime !== null && mime !== void 0 ? mime : lodash_1.default.find(mimeCfg.get(), (v, k) => k > '' && (0, micromatch_1.isMatch)(fn, k)); // isMatch throws on an empty string
47
47
  if (mime === vfs_1.MIME_AUTO)
@@ -46,7 +46,7 @@ function serveStatic(uri) {
46
46
  const folder = uri.slice(2, -1); // we know folder is very similar to uri
47
47
  let cache = {};
48
48
  (0, valtio_1.subscribe)(customHtml_1.customHtmlState, () => cache = {}); // reset cache at every change
49
- return async (ctx, next) => {
49
+ return async (ctx) => {
50
50
  if (ctx.method === 'OPTIONS') {
51
51
  ctx.status = const_1.HTTP_NO_CONTENT;
52
52
  ctx.set({ Allow: 'OPTIONS, GET' });
package/src/upload.js CHANGED
@@ -20,8 +20,9 @@ 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
- if (!(0, vfs_1.hasPermission)(base, 'can_upload', ctx))
24
- return fail(base.can_upload === false ? const_1.HTTP_FORBIDDEN : const_1.HTTP_UNAUTHORIZED);
23
+ const res = (0, vfs_1.statusCodeForMissingPerm)(base, 'can_upload', ctx);
24
+ if (res)
25
+ return fail(res);
25
26
  const fullPath = (0, path_1.join)(base.source, path);
26
27
  const dir = (0, path_1.dirname)(fullPath);
27
28
  const min = exports.minAvailableMb.get() * (1 << 20);
package/src/util-files.js CHANGED
@@ -106,18 +106,20 @@ async function* dirStream(path, deep) {
106
106
  const skip = await getItemsToSkip(path);
107
107
  for await (const entry of dirStream) {
108
108
  let { path, dirent } = entry;
109
- if (!dirent.isDirectory() && !dirent.isFile())
109
+ const isDir = dirent.isDirectory();
110
+ if (!isDir && !dirent.isFile())
110
111
  continue;
111
112
  path = String(path);
112
113
  if (!(skip === null || skip === void 0 ? void 0 : skip.includes(path)))
113
- yield path;
114
+ yield [path, isDir];
114
115
  }
115
116
  async function getItemsToSkip(path) {
116
117
  if (!const_1.IS_WINDOWS)
117
118
  return;
118
- const out = await (0, util_os_1.runCmd)('dir', ['/ah', '/b', path.replace(/\//g, '\\')])
119
+ const winPath = path.replace(/\//g, '\\');
120
+ const out = await (0, util_os_1.runCmd)('dir', ['/ah', '/b', deep ? '/s' : '', winPath])
119
121
  .catch(() => ''); // error in case of no matching file
120
- return out.split('\r\n').slice(0, -1);
122
+ return out.split('\r\n').slice(0, -1).map(x => !deep ? x : x.slice(winPath.length + 1).replace(/\\/g, '/'));
121
123
  }
122
124
  }
123
125
  exports.dirStream = dirStream;
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.cantReadStatusCode = exports.walkNode = exports.hasPermission = exports.nodeIsDirectory = exports.getNodeName = exports.saveVfs = exports.vfs = exports.urlToNode = exports.isSameFilenameAs = exports.MIME_AUTO = exports.defaultPerms = void 0;
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;
8
8
  const promises_1 = __importDefault(require("fs/promises"));
9
9
  const path_1 = require("path");
10
10
  const micromatch_1 = require("micromatch");
@@ -20,6 +20,7 @@ const WHO_ANY_ACCOUNT = '*';
20
20
  exports.defaultPerms = {
21
21
  can_see: WHO_ANYONE,
22
22
  can_read: WHO_ANYONE,
23
+ can_list: WHO_ANYONE,
23
24
  can_upload: WHO_NO_ONE,
24
25
  can_delete: WHO_NO_ONE,
25
26
  };
@@ -129,53 +130,91 @@ function hasPermission(node, perm, ctx) {
129
130
  && matchWho((_a = node[perm]) !== null && _a !== void 0 ? _a : exports.defaultPerms[perm], ctx);
130
131
  }
131
132
  exports.hasPermission = hasPermission;
132
- async function* walkNode(parent, ctx, depth = 0, prefixPath = '') {
133
+ function statusCodeForMissingPerm(node, perm, ctx) {
134
+ if (hasPermission(node, perm, ctx))
135
+ return false;
136
+ return ctx.status = node[perm] === false ? const_1.HTTP_FORBIDDEN : const_1.HTTP_UNAUTHORIZED;
137
+ }
138
+ exports.statusCodeForMissingPerm = statusCodeForMissingPerm;
139
+ // it's responsibility of the caller to verify you have list permission on parent, as callers have different needs.
140
+ // Too many parameters: consider object, but benchmark against degraded recursion on huge folders.
141
+ async function* walkNode(parent, ctx, depth = 0, prefixPath = '', requiredPerm) {
133
142
  var _a;
143
+ if (requiredPerm && ctx
144
+ && !hasPermission(parent, requiredPerm, ctx)
145
+ && !masksCouldGivePermission(parent.masks))
146
+ return; // no permission, no reason to continue
134
147
  const { children, source } = parent;
135
148
  const took = prefixPath ? undefined : new Set();
136
149
  if (children)
137
150
  for (const child of children) {
138
- const name = prefixPath + getNodeName(child);
151
+ const nodeName = getNodeName(child);
152
+ const name = prefixPath + nodeName;
139
153
  took === null || took === void 0 ? void 0 : took.add(name);
140
- yield* workItem({
141
- ...child,
142
- name,
143
- }, depth > 0 && await nodeIsDirectory(child).catch(() => false));
154
+ const item = { ...child, name };
155
+ if (!canSee(item))
156
+ continue;
157
+ yield item;
158
+ if (!depth || !await nodeIsDirectory(child).catch(() => false))
159
+ continue;
160
+ inheritMasks(item, parent, nodeName);
161
+ if (!ctx || hasPermission(item, 'can_list', ctx)) // check perm before recursion
162
+ yield* walkNode(item, ctx, depth - 1, name + '/');
144
163
  }
145
164
  if (!source)
146
165
  return;
147
166
  try {
148
- for await (const path of (0, misc_1.dirStream)(source, depth)) {
167
+ let lastDir = prefixPath.slice(0, -1) || '.';
168
+ const map = new Map();
169
+ map.set(lastDir, parent);
170
+ // it's important to keep using dirStream in deep-mode, as it is manyfold faster (it parallelizes)
171
+ for await (const [path, isDir] of (0, misc_1.dirStream)(source, depth)) {
149
172
  if (ctx === null || ctx === void 0 ? void 0 : ctx.req.aborted)
150
173
  return;
151
174
  const name = prefixPath + (((_a = parent.rename) === null || _a === void 0 ? void 0 : _a[path]) || path);
152
175
  if (took === null || took === void 0 ? void 0 : took.has(name))
153
176
  continue;
154
- yield* workItem({
177
+ if (depth) {
178
+ const dir = (0, path_1.dirname)(name);
179
+ if (dir !== lastDir)
180
+ parent = map.get(lastDir = dir);
181
+ }
182
+ const item = {
155
183
  name,
156
184
  source: (0, path_1.join)(source, path),
157
185
  rename: renameUnderPath(parent.rename, path),
158
- });
186
+ };
187
+ if (!canSee(item))
188
+ continue;
189
+ if (isDir)
190
+ map.set(name, item);
191
+ yield item;
159
192
  }
160
193
  }
161
194
  catch (e) {
162
195
  console.debug('glob', source, e); // ENOTDIR, or lacking permissions
163
196
  }
164
197
  // item will be changed, so be sure to pass a temp node
165
- async function* workItem(item, recur = false) {
166
- const name = getNodeName(item);
198
+ function canSee(item) {
167
199
  // we basename for depth>0 where we already have the rest of the path in the parent's url, and would be duplicated
168
- const virtualBasename = (0, path_1.basename)(name);
169
- item.isTemp = true;
170
- applyMasks(item, parent, virtualBasename);
200
+ applyMasks(item, parent, (0, path_1.basename)(getNodeName(item)));
171
201
  inheritFromParent(parent, item);
172
202
  if (ctx && !hasPermission(item, 'can_see', ctx))
173
203
  return;
174
- yield item;
175
- if (!recur)
176
- return;
177
- inheritMasks(item, parent, virtualBasename);
178
- yield* walkNode(item, ctx, depth - 1, name + '/');
204
+ item.isTemp = true;
205
+ return item;
206
+ }
207
+ function masksCouldGivePermission(masks) {
208
+ if (!masks)
209
+ return false;
210
+ for (const [, props] of Object.entries(masks)) {
211
+ const v = props[requiredPerm];
212
+ if (v && (!ctx || matchWho(v, ctx))) // without ctx we can't say, so it could
213
+ return true;
214
+ if (masksCouldGivePermission(props.masks))
215
+ return true;
216
+ }
217
+ return false;
179
218
  }
180
219
  }
181
220
  exports.walkNode = walkNode;
@@ -212,13 +251,9 @@ function renameUnderPath(rename, path) {
212
251
  function matchWho(who, ctx) {
213
252
  return who === WHO_ANYONE
214
253
  || who === WHO_ANY_ACCOUNT && Boolean(ctx.state.account)
215
- || Array.isArray(who) && (() => // check if I or any ancestor match `who`, but cache ancestors' usernames inside context state
216
- (0, misc_1.getOrSet)(ctx.state, 'usernames', () => (0, perm_1.getCurrentUsernameExpanded)(ctx)).some((u) => who.includes(u)))();
217
- }
218
- function cantReadStatusCode(node) {
219
- return node.can_read === false ? const_1.HTTP_FORBIDDEN : const_1.HTTP_UNAUTHORIZED;
254
+ || Array.isArray(who) // check if I or any ancestor match `who`, but cache ancestors' usernames inside context state
255
+ && (0, misc_1.getOrSet)(ctx.state, 'usernames', () => (0, perm_1.getCurrentUsernameExpanded)(ctx)).some((u) => who.includes(u));
220
256
  }
221
- exports.cantReadStatusCode = cantReadStatusCode;
222
257
  events_1.default.on('accountRenamed', (from, to) => {
223
258
  recur(exports.vfs);
224
259
  saveVfs();
package/src/zip.js CHANGED
@@ -14,6 +14,7 @@ const config_1 = require("./config");
14
14
  const path_1 = require("path");
15
15
  const serveFile_1 = require("./serveFile");
16
16
  const const_1 = require("./const");
17
+ // expects 'node' to have had permissions checked by caller
17
18
  async function zipStreamFromFolder(node, ctx) {
18
19
  var _a;
19
20
  ctx.status = const_1.HTTP_OK;
@@ -23,14 +24,15 @@ async function zipStreamFromFolder(node, ctx) {
23
24
  const name = (list === null || list === void 0 ? void 0 : list.length) === 1 ? (0, path_1.basename)(list[0]) : (0, vfs_1.getNodeName)(node);
24
25
  ctx.attachment(((0, misc_1.isWindowsDrive)(name) ? name[0] : (name || 'archive')) + '.zip');
25
26
  const filter = (0, misc_1.pattern2filter)(String(ctx.query.search || ''));
26
- const walker = !list ? (0, vfs_1.walkNode)(node, ctx, Infinity)
27
+ const walker = !list ? (0, vfs_1.walkNode)(node, ctx, Infinity, '', 'can_read')
27
28
  : (async function* () {
28
29
  for await (const el of list) {
29
30
  const subNode = await (0, vfs_1.urlToNode)(el, ctx, node);
30
- if (!subNode || !(0, vfs_1.hasPermission)(subNode, 'can_read', ctx))
31
+ if (!subNode)
31
32
  continue;
32
33
  if (await (0, vfs_1.nodeIsDirectory)(subNode)) { // a directory needs to walked
33
- yield* (0, vfs_1.walkNode)(subNode, ctx, Infinity, el + '/');
34
+ if ((0, vfs_1.hasPermission)(subNode, 'can_list', ctx))
35
+ yield* (0, vfs_1.walkNode)(subNode, ctx, Infinity, el + '/', 'can_read');
34
36
  continue;
35
37
  }
36
38
  let folder = (0, path_1.dirname)(el);
@@ -39,6 +41,8 @@ async function zipStreamFromFolder(node, ctx) {
39
41
  }
40
42
  })();
41
43
  const mappedWalker = (0, misc_1.filterMapGenerator)(walker, async (el) => {
44
+ if (!(0, vfs_1.hasPermission)(el, 'can_read', ctx))
45
+ return; // the fact you see it doesn't mean you can read it
42
46
  const { source } = el;
43
47
  const name = (0, vfs_1.getNodeName)(el);
44
48
  if (!source || ctx.req.aborted || !filter(name))