optimization: a long list of folders can be 2x faster, especially on network volumes (operations on a single entries are parallelized)

Massimo Melina committed May 9, 2026 at 15:33 UTC d97cb8f1d134ab2366897bedadad1056cc1aef3a
3 files changed +21 -12
src/api.get_file_list.ts
+10 -6
@@ -117,10 +117,14 @@ export const get_file_list: ApiHandler = async ({ uri='/', offset, limit, c, onl
117 const name = getNodeName(node)
118 const isFolder = nodeIsFolder(node)
119 try {
120 - const st = source ? await (node.stats || statWithTimeout(source).catch(e => {
121 - if (!isFolder || !node.children?.length) // folders with virtual children, keep them
122 - throw e
123 - })) : undefined
120 + const [web, comment, st] = await Promise.all([
121 + hasDefaultFile(node, ctx).then(x => x ? true : undefined),
122 + node.comment ?? getCommentFor(source),
123 + source ? (node.stats || statWithTimeout(source).catch(e => {
124 + if (!isFolder || !node.children?.length) // folders with virtual children, keep them
125 + throw e
126 + })) : undefined
127 + ])
128 // permissions of entries are sent as a difference with permissions of parent
129 const pl = node.can_list === WHO_NO_ONE ? 'l'
130 : !hasPermission(node, 'can_list', ctx) ? 'L'
@@ -142,9 +146,9 @@ export const get_file_list: ApiHandler = async ({ uri='/', offset, limit, c, onl
146 url,
147 target: node.target,
148 order: node.order,
145 - comment: node.comment ?? await getCommentFor(source),
149 + comment,
150 icon: getNodeIcon(node),
147 - web: await hasDefaultFile(node, ctx) ? true : undefined,
151 + web,
152 }
153 }
154 catch {
src/vfs.ts
+3 -4
@@ -4,7 +4,7 @@ import fs from 'fs/promises'
4 import { basename, dirname, join, resolve } from 'path'
5 import {
6 makeMatcher, setHidden, onlyTruthy, isValidFileName, throw_, VfsPerms, Who, debounceAsync,
7 - isWhoObject, WHO_ANY_ACCOUNT, defaultPerms, PERM_KEYS, removeStarting, HTTP_SERVER_ERROR, try_, matches,
7 + isWhoObject, WHO_ANY_ACCOUNT, defaultPerms, PERM_KEYS, HTTP_SERVER_ERROR, try_, matches, Promisable,
8 statWithTimeout, safeDecodeURIComponent, getUncHost,
9 } from './misc'
10 import Koa from 'koa'
@@ -44,7 +44,7 @@ export interface VfsNode extends VfsNodeStored { // include fields that are only
44 original?: VfsNode // if this is a temp node but reflecting an existing node
45 parent?: VfsNode // available when original is available (therefore, only for isTemp)
46 isFolder?: boolean // use nodeIsFolder() instead of relying on this field
47 - stats?: Promise<Stats>
47 + stats?: Promisable<Stats>
48 }
49
50 export function permsFromParent(parent: VfsNode, child: VfsNode) {
@@ -381,8 +381,7 @@ export async function* walkNode(parent: VfsNode, {
381 const name = prefixPath + (renamed || path)
382 if (taken?.has(normalizeFilename(name))) // taken by vfs node above
383 return false // false just in case it's a folder
384 -
385 - const item: VfsNode = { name, isFolder, source: join(source, path), parent }
384 + const item: VfsNode = { name, isFolder, source: join(source, path), parent, stats: entry.stats }
385 // masks containing '/' must be matched against the relative path while keeping walkDir recursion enabled
386 await pathMaskApplier(item, renamed || path)
387 if (await cantSee(item)) // can't see: don't produce and don't recur
src/walkDir.ts
+8 -2
@@ -62,7 +62,13 @@ export function walkDir(path: string, { depth = 0, hidden = true, parallelizeRec
62 work(Object.assign(Object.create(direntMethods), {
63 isDir: f.IS_DIRECTORY,
64 name: f.LONG_NAME,
65 - stats: { size: f.SIZE, birthtime: f.CREATION_TIME, mtime: f.LAST_WRITE_TIME } as Stats
65 + stats: {
66 + size: f.SIZE,
67 + birthtime: f.CREATION_TIME, birthtimeMs: f.CREATION_TIME.getTime(),
68 + mtime: f.LAST_WRITE_TIME, mtimeMs: f.LAST_WRITE_TIME.getTime(),
69 + isFile: () => !f.IS_DIRECTORY,
70 + isDirectory: () => f.IS_DIRECTORY,
71 + } as Stats
72 }))
73 }, true))
74 }
@@ -139,4 +145,4 @@ for (const key of Reflect.ownKeys(Dirent.prototype)) {
145 DirentFromStats.prototype[name] = function () {
146 return this[kStats][name]();
147 };
142 -}
\ No newline at end of file
148 +}