search: prioritize outer entries over deeper ones
Massimo Melina committed
Jan 18, 2025 at 15:36 UTC
2cd174eb1ff2f3b29aed2e84f1bf94150a13f37f
2 files changed
+46
-38
src/vfs.ts
+45
-37
@@ -275,15 +275,16 @@ export async function* walkNode(parent: VfsNode, {
275
onlyFiles = false,
276
}: { ctx?: Koa.Context,depth?: number, prefixPath?: string, requiredPerm?: undefined | keyof VfsPerms, onlyFolders?: boolean, onlyFiles?: boolean } = {}): AsyncIterableIterator<VfsNode> {
277
const { children, source } = parent
278
- const took = prefixPath ? undefined : new Set()
278
+ const taken = prefixPath ? undefined : new Set()
279
const maskApplier = parentMaskApplier(parent)
280
const parentsCache = new Map() // we use this only if depth > 0
281
+ const visitLater: any = []
282
if (children)
283
for (const child of children) {
284
if (await nodeIsDirectory(child) ? onlyFiles : onlyFolders) continue
285
const nodeName = getNodeName(child)
286
const name = prefixPath + nodeName
286
- took?.add(normalizeFilename(name))
287
+ taken?.add(normalizeFilename(name))
288
const item = { ...child, name }
289
if (!await canSee(item)) continue
290
if (item.source) // real items must be accessible
@@ -294,48 +295,55 @@ export async function* walkNode(parent: VfsNode, {
295
parentsCache.set(name, item)
296
inheritMasks(item, parent, nodeName)
297
if (!ctx || hasPermission(item, 'can_list', ctx)) // check perm before recursion
297
- yield* walkNode(item, { ctx, depth: depth - 1, prefixPath: name + '/', requiredPerm, onlyFolders })
298
+ visitLater.push([item, name]) // prioritize siblings
299
}
299
- if (!source)
300
- return
301
- if (requiredPerm && ctx // no permission, no reason to continue (at least for dynamic elements)
302
- && !hasPermission(parent, requiredPerm, ctx)
303
- && !masksCouldGivePermission(parent.masks, requiredPerm))
304
- return
305
-
300
try {
307
- let lastDir = prefixPath.slice(0, -1) || '.'
308
- parentsCache.set(lastDir, parent)
309
- for await (const entry of dirStream(source, { depth, onlyFolders, hidden: showHiddenFiles.get() })) {
310
- if (ctx?.isAborted()) break
311
- const {path} = entry
312
- const isFolder = entry.isDirectory()
313
- const name = prefixPath + (parent.rename?.[path] || path)
314
- if (took?.has(normalizeFilename(name))) continue
315
- if (depth) {
316
- const dir = dirname(name)
317
- if (dir !== lastDir)
318
- parent = parentsCache.get(lastDir = dir)
319
- }
301
321
- const item: VfsNode = {
322
- name,
323
- isFolder,
324
- source: join(source, path),
325
- rename: renameUnderPath(parent.rename, path),
302
+ if (!source)
303
+ return
304
+ if (requiredPerm && ctx // no permission, no reason to continue (at least for dynamic elements)
305
+ && !hasPermission(parent, requiredPerm, ctx)
306
+ && !masksCouldGivePermission(parent.masks, requiredPerm))
307
+ return
308
+
309
+ try {
310
+ let lastDir = prefixPath.slice(0, -1) || '.'
311
+ parentsCache.set(lastDir, parent)
312
+ for await (const entry of dirStream(source, { depth, onlyFolders, hidden: showHiddenFiles.get() })) {
313
+ if (ctx?.isAborted()) break
314
+ const {path} = entry
315
+ const isFolder = entry.isDirectory()
316
+ const name = prefixPath + (parent.rename?.[path] || path)
317
+ if (taken?.has(normalizeFilename(name))) continue
318
+ if (depth) {
319
+ const dir = dirname(name)
320
+ if (dir !== lastDir)
321
+ parent = parentsCache.get(lastDir = dir)
322
+ }
323
+
324
+ const item: VfsNode = {
325
+ name,
326
+ isFolder,
327
+ source: join(source, path),
328
+ rename: renameUnderPath(parent.rename, path),
329
+ }
330
+ if (isFolder) // store it even if we can't see it (masks), as its children can be produced by dirStream
331
+ parentsCache.set(name, item)
332
+ if (!(onlyFiles && isFolder) && await canSee(item))
333
+ yield item
334
+ entry.closingBranch?.then(p =>
335
+ parentsCache.delete(p || '.'))
336
}
327
- if (isFolder) // store it even if we can't see it (masks), as its children can be produced by dirStream
328
- parentsCache.set(name, item)
329
- if (!(onlyFiles && isFolder) && await canSee(item))
330
- yield item
331
- entry.closingBranch?.then(p =>
332
- parentsCache.delete(p || '.'))
337
}
338
+ catch(e) {
339
+ console.debug('walkNode', source, e) // ENOTDIR, or lacking permissions
340
+ }
341
+ parentsCache.clear() // hoping for faster GC
342
}
335
- catch(e) {
336
- console.debug('walkNode', source, e) // ENOTDIR, or lacking permissions
343
+ finally {
344
+ for (const [item, name] of visitLater)
345
+ yield* walkNode(item, { ctx, depth: depth - 1, prefixPath: name + '/', requiredPerm, onlyFolders })
346
}
338
- parentsCache.clear() // hoping for faster GC
347
348
// item will be changed, so be sure to pass a temp node
349
async function canSee(item: VfsNode) {
tests/test.ts
+1
-1
@@ -97,7 +97,7 @@ describe('basics', () => {
97
it('protectFromAbove.list', reqList('/protectFromAbove/child/', { inList:['alfa.txt'] }))
98
99
const zipSize = 13010
100
- const zipOfs = 5000
100
+ const zipOfs = 0x1359
101
it('zip.head', req('/f1/?get=zip', { empty:true, length:zipSize }, { method:'HEAD' }) )
102
it('zip.partial', req('/f1/?get=zip', { re:/^C3$/, length: 2 }, { headers: { Range: `bytes=${zipOfs}-${zipOfs+1}` } }) )
103
it('zip.partial.resume', req('/f1/?get=zip', { re:/^C3/, length:zipSize-zipOfs }, { headers: { Range: `bytes=${zipOfs}-` } }) )