plugins: event listDiskFolder #966

Massimo Melina committed Apr 26, 2025 at 13:06 UTC f354dd7cd6d22022124d4d4a61a8a1220d4e0f72
5 files changed +35 -14
dev-plugins.md
+11 -3
@@ -651,13 +651,21 @@ This section is still partially documented, and you may need to have a look at t
651 - `publicIpsChanged`
652 - parameters: { IPs, IP4, IP6, IPX }
653 - `newSocket`
654 - - parameters: { socket,ip }
654 + - parameters: { socket, ip }
655 - preventable
656 - return: you can return a string with a message that will be logged, and it will also cause disconnection
657 - `getList` called when get=list on legit requests to ?get=list
658 - parameters: { node, ctx }
659 - async supported
660 - stoppable
661 +- `listDiskFolder` called when a list is read from the disk; useful to implement a cache
662 + - parameters: { path, ctx? }
663 + - async supported
664 + - return: to prevent the default listing and provide such a list yourself, return an array or iterator;
665 + to let the default behavior while getting the content of the list, return a function, and it will be called for each
666 + entry, passed as first parameter (an object of standard class fs.Dirent), and when the list is over it will be called
667 + with a boolean, true if the list is completed and false if it was aborted
668 +
669
670 # Notifications (backend-to-frontend events)
671
@@ -952,8 +960,8 @@ If you want to override a text regardless of the language, use the special langu
960 - automatic unload of api.subscribeConfig
961 - api._
962 - config.type=showHtml
955 -- 12.2 (v0.57.0)
956 - - backend event: finalizingLogin, httpsServerOptions, clearTextLogin
963 +- 12.3 (v0.57.0)
964 + - backend event: finalizingLogin, httpsServerOptions, clearTextLogin, listDiskFolder
965 - frontend events: beforeLoginSubmit, loginUsernameField, loginPasswordField
966 - exports.changelog
967 - automatic unload of api.events listeners
src/api.vfs.ts
+1 -1
@@ -205,7 +205,7 @@ const apis: ApiHandlers = {
205 try {
206 const matching = makeMatcher(fileMask)
207 path = isWindowsDrive(path) ? path + '\\' : resolve(path || '/')
208 - await walkDir(path, {}, async entry => {
208 + await walkDir(path, { ctx }, async entry => {
209 if (ctx.isAborted())
210 return null
211 const {path:name} = entry
src/const.ts
+1 -1
@@ -9,7 +9,7 @@ import { formatTimestamp } from './cross'
9 import { argv } from './argv'
10 export * from './cross-const'
11
12 -export const API_VERSION = 12.2
12 +export const API_VERSION = 12.3
13 export const COMPATIBLE_API_VERSION = 1 // while changes in the api are not breaking, this number stays the same, otherwise it is made equal to API_VERSION
14
15 // you can add arguments with this file, currently used for the update process on mac/linux
src/vfs.ts
+2 -2
@@ -277,7 +277,7 @@ interface WalkNodeOptions {
277 onlyFolders?: boolean,
278 onlyFiles?: boolean
279 }
280 -// it's responsibility of the caller to verify you have list permission on parent, as callers have different needs.
280 +// it's the responsibility of the caller to verify you have list permission on parent, as callers have different needs.
281 export async function* walkNode(parent: VfsNode, {
282 ctx,
283 depth = Infinity,
@@ -325,7 +325,7 @@ export async function* walkNode(parent: VfsNode, {
325 try {
326 let lastDir = prefixPath.slice(0, -1) || '.'
327 parentsCache.set(lastDir, parent)
328 - await walkDir(source, { depth, hidden: showHiddenFiles.get() }, async entry => {
328 + await walkDir(source, { depth, ctx, hidden: showHiddenFiles.get() }, async entry => {
329 if (ctx?.isAborted()) {
330 stream.push(null)
331 return null
src/walkDir.ts
+20 -7
@@ -3,7 +3,10 @@ import { stat, opendir } from 'fs/promises'
3 import { IS_WINDOWS } from './const'
4 import { join } from 'path'
5 import { pendingPromise, Promisable } from './cross'
6 -import { Stats, Dirent } from 'node:fs'
6 +import { Stats, Dirent, Dir } from 'node:fs'
7 +import events from './events'
8 +import _ from 'lodash'
9 +import { Context } from 'koa'
10 import fswin from 'fswin'
11 import { isDirectory } from './util-files'
12
@@ -15,9 +18,10 @@ export interface DirStreamEntry extends Dirent {
18 const dirQ = makeQ(3)
19
20 // cb returns void = just go on, null = stop, false = go on but don't recur (in case of depth)
18 -export function walkDir(path: string, { depth = 0, hidden = true }: {
21 +export function walkDir(path: string, { depth = 0, hidden = true, ctx }: {
22 depth?: number,
20 - hidden?: boolean
23 + hidden?: boolean,
24 + ctx?: Context
25 }, cb: (e: DirStreamEntry) => Promisable<void | null | false>) {
26 let stopped = false
27 const closingQ: string[] = []
@@ -37,7 +41,12 @@ export function walkDir(path: string, { depth = 0, hidden = true }: {
41 const subDirsDone: Promise<any>[] = []
42 let n = 0
43 let last: DirStreamEntry | undefined
40 - if (IS_WINDOWS) { // use native apis to read 'hidden' attribute
44 +
45 + const res = (await events.emitAsync('listDiskFolder', { path: base, ctx }))?.[0] // consider only first result
46 + const pluginReceiver = _.isFunction(res) && res || null
47 + const pluginIterator = _.isFunction(res?.[Symbol.asyncIterator] || res?.[Symbol.iterator]) && res as Dir
48 +
49 + if (IS_WINDOWS && !pluginIterator) { // use native apis to read the 'hidden' attribute
50 const direntMethods = {
51 isDir: false,
52 isFile(){ return !this.isDir },
@@ -55,12 +64,14 @@ export function walkDir(path: string, { depth = 0, hidden = true }: {
64 stats: { size: f.SIZE, birthtime: f.CREATION_TIME, mtime: f.LAST_WRITE_TIME } as Stats
65 }))
66 }, true))
67 + pluginReceiver?.(!stopped)
68 + return
69 }
59 - else for await (let entry of await opendir(base)) {
70 + for await (let entry of (pluginIterator || await opendir(base))) {
71 if (stopped) break
61 - if (!hidden && entry.name[0] === '.')
72 + if (!hidden && entry.name[0] === '.' && !IS_WINDOWS)
73 continue
63 - const stats = entry.isSymbolicLink() && await stat(join(base, entry.name)).catch(() => null)
74 + const stats = entry.isSymbolicLink?.() && await stat(join(base, entry.name)).catch(() => null)
75 if (stats === null) continue
76 if (stats)
77 entry = new DirentFromStats(entry.name, stats)
@@ -69,9 +80,11 @@ export function walkDir(path: string, { depth = 0, hidden = true }: {
80 expanded.stats = stats
81 await work(expanded)
82 }
83 + pluginReceiver?.(!stopped)
84
85 async function work(entry: DirStreamEntry) {
86 entry.path = (relativePath && relativePath + '/') + entry.name
87 + pluginReceiver?.(entry)
88 if (last && closingQ.length) // pending entries
89 last.closingBranch = Promise.resolve(closingQ.shift()!)
90 last = entry