plugins: new backend event "dirEntry"
Massimo Melina committed
Mar 5, 2026 at 19:35 UTC
1a3e6ea8dee2e7ccbe156a6f8024ea9750a001e2
4 files changed
+47
-16
dev-plugins.md
+26
-14
@@ -189,20 +189,9 @@ exports.depend = [{ repo: "x", version: 1 }] // non-JSON object key
189
190
- `unload: function` called when unloading a plugin. This is a good place for example to clearInterval().
191
- `onDirEntry: ({ entry: DirEntryBackend, listUri: string, ctx, node: VfsNode }) => Promisable<void | false>`
192
- by providing this callback you can manipulate the record that is sent to the frontend (`entry`),
193
- or you can return false to exclude this entry from the results.
194
- `DirEntryBackend` fields:
195
- - `n: string` name of the entry. (May include relative path when searching in sub-folders.)
196
- - `s?: number` size of the entry, in bytes. It may be missing, for example for folders.
197
- - `m?: Date` modified-time.
198
- - `c?: Date` creation-time.
199
- - `p?: string` permissions.
200
- - `comment?: string` comment for the entry.
201
- - `web?: boolean` true for web links.
202
- - `url?: string` target url for links.
203
- - `target?: string` target for links.
204
- - `icon?: string | true` icon override or true for "specific for this file".
205
- - `order?: number` custom sort order.
192
+ legacy callback for compatibility. For new plugins prefer the backend event `dirEntry`, so all plugin hooks follow the same event-based DX.
193
+ You can manipulate the record that will be sent to the frontend (`entry`), or return false to exclude this entry from the results.
194
+
195
- `config: Functionable<{ [key]: FieldDescriptor }, values:object>` declare a set of admin-configurable values owned by the plugin
196
that will be displayed inside Admin-panel for change. Each property is identified by its key,
197
and the descriptor is another object with options about the field.
@@ -809,6 +798,27 @@ This section is still partially documented, and you may need to have a look at t
798
- parameters: { node, ctx }
799
- async supported
800
- stoppable
801
+- `dirEntry` called for each entry before it is sent to the frontend list
802
+ - parameters: { entry, listUri, ctx, node }
803
+ - `entry: DirEntryBackend`
804
+ - `listUri: string`
805
+ - `ctx: Context`
806
+ - `node: VfsNode`
807
+ - async supported
808
+ - preventable
809
+ - note: legacy `onDirEntry` hooks run first; use this event for new code
810
+ - types `DirEntryBackend` fields:
811
+ - `n: string` name of the entry. (May include the relative path when searching in subfolders.)
812
+ - `s?: number` size of the entry, in bytes. It may be missing, for example, for folders.
813
+ - `m?: Date` modified-time.
814
+ - `c?: Date` creation-time.
815
+ - `p?: string` permissions.
816
+ - `comment?: string` comment for the entry.
817
+ - `web?: boolean` true for web links.
818
+ - `url?: string` target url for links.
819
+ - `target?: string` target for links.
820
+ - `icon?: string | true` icon override or true for "specific for this file".
821
+ - `order?: number` custom sort order.
822
- `listDiskFolder` called when a list is read from the disk; useful to implement a cache
823
- parameters: { path, ctx? }
824
- async supported
@@ -1166,3 +1176,5 @@ If you want to override a text regardless of the language, use the special langu
1176
- api.normalizeFilename
1177
- 12.97 (v0.57.28)
1178
- HFS.customizeText
1179
+- 13 (v3.1.0)
1180
+ - backend event: dirEntry
\ No newline at end of file
src/api.get_file_list.ts
+3
@@ -15,6 +15,7 @@ import { updateConnectionForCtx } from './connections'
15
import { ctxAdminAccess } from './adminApis'
16
import { dontOverwriteUploading } from './upload'
17
import { SendListReadable } from './SendList'
18
+import events from './events'
19
20
export interface DirEntry { n:string, s?:number, m?:Date, c?:Date, p?: string, comment?: string, web?: boolean, url?: string, target?: string, icon?: string | true, order?: number }
21
@@ -89,6 +90,8 @@ export const get_file_list: ApiHandler = async ({ uri='/', offset, limit, c, onl
90
const res = await Promise.all(onDirEntryHandlers.map(cb => cb(cbParams)))
91
if (res.some(x => x === false))
92
continue
93
+ if ((await events.emitAsync('dirEntry', cbParams))?.isDefaultPrevented())
94
+ continue
95
}
96
catch(e) {
97
console.log("a plugin with onDirEntry is causing problems:", e)
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.97
12
+export const API_VERSION = 13
13
export const COMPATIBLE_API_VERSION = 1 // the day we break with the past, we'll update this
14
15
export const DEV = process.env.DEV ? 'DEV' : ''
tests/test.ts
+17
-1
@@ -6,7 +6,7 @@ import { basename, dirname, resolve } from 'path'
6
import { exec } from 'child_process'
7
import _ from 'lodash'
8
import yaml from 'yaml'
9
-import { findDefined, pathEncode, randomId, try_, tryJson, UPLOAD_TEMP_HASH, wait } from '../src/cross'
9
+import { findDefined, pathEncode, randomId, try_, tryJson, UPLOAD_TEMP_HASH, wait, waitFor } from '../src/cross'
10
import { httpStream, parseHttpUrl, stream2string, XRequestOptions } from '../src/util-http'
11
import { ThrottledStream, ThrottleGroup } from '../src/ThrottledStream'
12
import { mkdir, rm, rename, writeFile, access } from 'fs/promises'
@@ -738,6 +738,22 @@ describe('admin', () => {
738
throw "plugin didn't start"
739
}, { auth })()
740
})
741
+ test('plugins.dirEntry event', async () => {
742
+ const script = `exports.init = api => api.events.on('dirEntry', ({ entry }) => entry.n === 'f2/' && api.events.stop)`
743
+ await switchIt(true).finally(() => switchIt(false).catch(() => {}))
744
+
745
+ async function switchIt(on: boolean) {
746
+ let lastNames: any
747
+ await reqApi('set_config', { values: { server_code: on ? script : '' } }, 200, { auth })()
748
+ const good = await waitFor(async () => {
749
+ const res = await reqList('/f1/', { status: 200 })()
750
+ lastNames = res?.list?.map((x: any) => x.n)
751
+ return on === !isInList(res, 'f2/')
752
+ }, { interval: 100, timeout: 3000 })
753
+ if (!good)
754
+ throw Error("condition not met on list: " + JSON.stringify(lastNames))
755
+ }
756
+ })
757
test('plugins.download-counter percent name', async () => {
758
const id = 'download-counter'
759
await reqApi('start_plugin', { id }, 200, { auth })()