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