main
md 1,210 lines 60.6 KB
Rendered Raw
1 # For plugin makers
2
3 If the information you are searching for is not in this document, [feel free to ask](https://github.com/rejetto/hfs/discussions).
4
5 A plugin for HFS is a folder that contains a `plugin.js` file. To install a plugin you just copy the folder into the `plugins` folder.
6 You will find `plugins` folder near `config.yaml`, and then in `USER_FOLDER/.hfs` for Linux and macOS, or near `hfs.exe` on Windows.
7
8 plugins can be hot-swapped, and to some extent can be edited without restarting the server.
9
10 Normally you'll have a plugin that's a theme, and another that's a firewall,
11 but nothing is preventing a single plugin from doing both tasks.
12
13 ## Development environment
14
15 The simplest way to develop is to create your folder inside the `.hfs/plugins` folder, and work there.
16 Each time you make a change, you'll see it reflected in the running server.
17 This is probably the easiest form to start with.
18
19 A neater way is to keep it both in the form of github repo and installed plugin.
20 If you want to do so, have a folder with your github repo in it, *outside* your `.hfs` folder.
21 As you'll see in the [Publish your plugin](#publish-your-plugin) section, you should keep your files inside the `dist` subfolder.
22 Then you'll need to link the `dist` folder inside the `plugins` folder.
23 If you go in your `.hfs/plugins` folder on linux and mac, and enter
24
25 ln -s /PATH_TO_YOUR_REPO/dist MY_PLUGIN_NAME
26
27 On Windows, the command would be something like
28
29 mklink /d C:\path\to\hfs\plugins\my_plugin C:\my_code\my_plugin\dist
30
31 You'll install your repo so that you can edit the sources and see effects in real-time.
32 This allows you to continue editing your repo and be ready to commit changes.
33
34 ## Backend / Frontend
35
36 plugins can have a part running in the backend (the server) and a part running in the frontend (the browser).
37 Frontend files reside in the "public" folder, while all the rest is backend.
38
39 ## System icons
40
41 HFS defines "system icons" that are used in the frontend, like the icon for the login.
42 They can be customized by creating a folder called "icons" and putting an image file for each icon to be customized;
43 the filename (excluding extension) will match one of the list:
44 *login, user, filter, search, search_off, close, error, stop, options, archive, logout, home, parent, folder, file,
45 spinner, password, download, upload, reload, lock, admin, check, to_start, to_end, menu, list, play, pause, edit, zoom,
46 delete, comment, link, info, cut, paste, copy, shuffle, repeat, success, warning, audio, video, image, cancel, total*.
47
48 For example, put a file "login.png" into "icons" to customize that icon. Stadard web file formats are supported.
49
50 If the list above becomes outdated, you can always find an updated version at https://github.com/rejetto/hfs/blob/main/frontend/src/sysIcons.ts.
51
52 ## Definitions
53
54 In this document we define some types using pseudo-typescript syntax.
55 We use some predefined types for brevity:
56
57 `Promisable<Type> = Type | Promise<Type>` where Type can be wrapped in a promise or not (direct).
58 When this is used for the return type, the function *can* be async.
59
60 `Functionable<Type, Arguments> = Type | ((...args: Arguments) => Type)` where Type can be returned by a function or not (direct).
61
62 ## Exported object
63
64 `plugin.js` is a javascript module (executed by Node.js), and its main way to communicate with HFS is by exporting things.
65 For example, it can define its description like this
66 ```js
67 exports.description = "I'm a nice plugin"
68 ```
69
70 The set of things exported goes by the name "exported object".
71
72 ### init
73
74 A plugin can define an `init` function like this:
75 ```js
76 exports.init = function(api) {
77 return { frontend_css: 'mystyle.css' }
78 }
79 ```
80
81 The init function is called by HFS when the module is loaded and should return an object with more things to
82 add/merge to the exported object. In the example above we are asking a css file to be loaded in the frontend.
83 Since it's a basic example, you could have simply defined it like this:
84 ```js
85 exports.frontend_css = 'mystyle.css'
86 ```
87 but in more complex cases you'll need to go through the `init`.
88 If you need to access the API you must use `init`, since that's the only place where it is found, otherwise you
89 can just use `exports`. The parameter `api` of the init is an object containing useful things [we'll see later](#api-object).
90
91 Let's first look at the things you can export:
92
93 ## Things a plugin can export
94
95 All the following properties are optional unless otherwise specified.
96
97 ### How to write fields marked *[STATIC JSON]*
98
99 These fields are **statically parsed from source code** (for plugin discovery/update checks), so treat them as JSON literals, not generic JavaScript.
100
101 - Use double quotes for strings and for object keys.
102 - Do not use single quotes, template strings, variables, function calls, comments, or trailing commas.
103 - Keep these values in the form `exports.<field> = <valid JSON literal>` (not returned from `init`).
104
105 Examples:
106
107 ```js
108 // valid
109 exports.description = "My plugin"
110 exports.repo = "user/repo"
111 exports.preview = ["https://example.com/p1.png", "https://example.com/p2.png"]
112 exports.depend = [{ "repo": "rejetto/file-icons", "version": 1 }]
113
114 // invalid for fields marked *[STATIC JSON]* in this doc
115 exports.description = 'My plugin' // single quotes
116 exports.repo = SOME_VAR // variable
117 exports.preview = getPreviewList() // function call
118 exports.depend = [{ repo: "x", version: 1 }] // non-JSON object key
119 ```
120
121 ### The actual list
122
123 - `description: string` try to explain what this plugin is for. *[STATIC JSON]*
124 - `version: number` use progressive numbers to distinguish each release
125 - `apiRequired: number | [min:number,max:number]` declare version(s) for which the plugin is designed. Mandatory.
126 A single number represents the minimum required version; an array of two defines the min/max supported versions.
127 Refer to the [API version history](#api-version-history) to find the correct number for your case.
128 Set a maximum version only if you know your plugin is incompatible with later releases.
129
130 Backward compatibility is standard, with rare exceptions. Most breaking changes affecting plugins occur when relying
131 on undocumented features, particularly via `api.require`. If your plugin becomes incompatible with a new HFS version:
132 - If you don't want to release an update: you can push a commit to modify `apiRequired` and specify a maximum version.
133 - If you release a new version to fix the issue:
134 - You DO NOT to set a *max* version.
135 - Only if your update broke the compatibility with the previous HFS version:
136 - You MUST update `apiRequired` to the current API version
137 (found at the end of this document).
138 - You MAY create a branch from the previous commit and name it `api12.8` (where 12.8 is your previous *apiRequired*).
139 This optional step enables users of older versions of HFS to still install your plugin.
140 - `isTheme: boolean | "light" | "dark"` set true if this is a theme that's not supposed to work together with other themes.
141 Running a theme will cause other themes to be stopped. Missing this, HFS will check if the name of the plugin ends with `-theme`.
142 Special values "light" and "dark" to declare whether the theme is (for example) dark and forces HFS to use dark-theme as a base.
143 - `preview: string | string[]` one or more URLs to images you want to show before your plugin is downloaded. *[STATIC JSON]*
144 - `depend: { repo: string, version: number }[]` declare what other plugins this depends on. *[STATIC JSON]*
145 - `beforePlugin: string` control the order this plugin is executed relative to another
146 - `afterPlugin: string` control the order this plugin is executed relative to another
147 - `repo: string | object` pointer to a GitHub repo where this plugin is hosted. *[STATIC JSON]*
148 - the string form is for GitHub repos. This is optional, as HFS will automatically set this at installation time.
149 Example: "rejetto/file-icons"
150 - the object form will point to other custom repo. Object properties:
151 - `web: string` link to a web page
152 - `main: string` link to the plugin.js (can be relative to `web`)
153 - `zip: string` link to the zip with the whole plugin (can be relative to `web`)
154 - `zipRoot: string` optional, in case the plugin in the zip is inside a folder
155
156 Example:
157 ```
158 {
159 "web": "https://github.com/rejetto/file-icons",
160 "zip": "/archive/refs/heads/main.zip",
161 "zipRoot": "file-icons-main/dist",
162 "main": "https://raw.githubusercontent.com/rejetto/file-icons/main/dist/plugin.js"
163 }
164 ```
165 Note that in this example we are pointing to a github repo just for clarity. You are not supposed to use this
166 complicated object form to link github, use the string form.
167 Plugins with custom repos are not included in search results, but the update feature will still work.
168 - `changelog: { version: number, message: string }[]` the UI will show only entries with version greater than currently installed.
169 You can use `md` syntax inside the message. *[STATIC JSON]*
170 - `init: (api: object) => (void | object | function)` described in the previous section. If an object is returned,
171 it will be merged with other "exported" properties described in this section, so you can return `{ unload }` for example.
172 If you return a function, this is just a shorter way to return the `unload`.
173 - `frontend_css: string | string[]` path to one or more css files that you want the frontend to load. These are to be placed in the `public` folder (refer below).
174 You can also include external files, by entering a full URL. Multiple files can be specified as `['file1.css', 'file2.css']`.
175 - `frontend_js: string | string[]` path to one or more js files that you want the frontend to load. These are to be placed in the `public` folder (refer below).
176 You can also include external files, by entering a full URL.
177 - `middleware: (Context) => Promisable<void | function>` a function that will be used as middleware: use this to interfere with http activity.
178 E.g.:
179 ```js
180 exports.middleware = ctx => {
181 ctx.body = "You are in the wrong place"
182 ctx.status = 404
183 }
184 ```
185 To interrupt other middlewares on this http request, call `ctx.stop()`.
186 If you want to execute something in the "upstream" of middlewares, return a function.
187 Upstream you can access the response calculated by HFS and other middlewares, so you'll find both the status and body set.
188 See more at https://github.com/rejetto/hfs/wiki/Middlewares .
189 You can read more in [the ctx object](#the-ctx-object) section.
190
191 - `unload: function` called when unloading a plugin. This is a good place for example to clearInterval().
192 - `onDirEntry: ({ entry: DirEntryBackend, listUri: string, ctx, node: VfsNode }) => Promisable<void | false>`
193 legacy callback for compatibility. For new plugins prefer the backend event `dirEntry`, so all plugin hooks follow the same event-based DX.
194 You can manipulate the record that will be sent to the frontend (`entry`), or return false to exclude this entry from the results.
195
196 - `config: Functionable<{ [key]: FieldDescriptor }, values:object>` declare a set of admin-configurable values owned by the plugin
197 that will be displayed inside Admin-panel for change. Each property is identified by its key,
198 and the descriptor is another object with options about the field.
199
200 Eg: you want a `message` text. You add this to your `plugin.js`:
201 ```js
202 exports.config = { message: {} }
203 ```
204 This will produce a configuration form in the admin-panel.
205 Once the admin has customized the value, the latter will be saved in the main config file, under the `plugins_config` property.
206 ```yaml
207 plugins_config:
208 name_of_the_plugin:
209 message: Hi there!
210 ```
211
212 When necessary your plugin will read its value using `api.getConfig('message')` in the backend,
213 or `HFS.getPluginConfig('message')` in the frontend, but the latter must be enabled using the `frontend` flag in the config.
214 To handle more complex cases, you can pass a function to `config` instead of an object. The function will receive a parameter `values`.
215 If any of your config contains sensitive information, ensure the name ends with `password`, or it starts with `_`.
216 This way HFS will remove it when the user clicks "export without passwords".
217
218 - `configDialog: DialogOptions` object to override dialog options. Please refer to sources for details.
219 - `onFrontendConfig: (config: object) => (void | object)` manipulate config values exposed to frontend.
220 - `customHtml: object | () => object` return custom-html sections programmatically.
221 Each key is a section name, the value is the html (or js, or css). Refer to https://github.com/rejetto/hfs/wiki/Customization:-HTML-sections
222 - `customRest: { [name]: (parameters: object, ctx) => any }` declare backend functions to be called by frontend with `HFS.customRestCall`
223 E.g.
224 ```js
225 exports.customRest = {
226 myCommand({ text }) { console.log(text) }
227 }
228 // then, in the frontend yon can call HFS.customRestCall('myCommand', { text: 'hello' })
229 ```
230 - `customApi: { [name]: (parameters) => any }` declare functions to be called by other plugins (only backend, not frontend) using `api.customApiCall` (documented below)
231
232 ### FieldDescriptor
233
234 A FieldDescriptor is an object and can be empty. Currently, these optional properties are supported:
235 - `type: 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'real_path' | 'vfs_path' | 'array' | 'username' | 'color' | 'date_time' | 'show_html'` . Default is `string`.
236 - `label: string` what name to display next to the field. Default is based on `key`.
237 - `defaultValue: any` value to be used when nothing is set. Default is undefined.
238 - `helperText: string` extra text printed next to the field.
239 - `showIf: (values: object) => boolean` only show this field if the function returns truthy.
240 Must not reference variables of the outer scope. [See example](https://github.com/rejetto/rich-folder/blob/main/dist/plugin.js).
241 - `frontend: boolean` expose this setting on the frontend, so that javascript can access it
242 using `HFS.getPluginConfig()[CONFIG_KEY]` but also css can access it as `var(--PLUGIN_NAME-CONFIG_KEY)`.
243 Hint: if you need to use a numeric config in CSS but you need to add a unit (like `em`),
244 the trick is to use something like this `calc(var(--plugin-something) * 1em)`.
245 - `getError: (value: any, { values: object, fields: object }) => (boolean | string)` a validator for the field.
246 Return false if value is valid, true for generic error, or a string for specific error.
247
248 Based on `type`, other properties are supported:
249 - `string` simple text field. Spaces at the start/end are automatically removed.
250 - `multiline: boolean`. Default is `false`.
251 - `required: boolean`. Default is `false`.
252 - to make it a password field, use this property `inputProps: { type: 'password' }`; valid also for other standard html input types.
253 - `number`
254 - `min: number`
255 - `max: number`
256 - `select`
257 - `options: { [label]: AnyJsonValue }`
258 - `multiselect` it's like `select` but its result is an array of values.
259 - `array` list of objects
260 - `fields`: an object of `FieldDescriptor`s, i.e. same format as `config`.
261 This field will be use for both the configuration of the grid's column, and the form's field.
262 Other than properties of `FieldDescriptor` you get these extra properties:
263 - `$column`: where you can put all the properties you want specifically to be set on the [grid's column](https://mui.com/x/api/data-grid/grid-col-def/).
264 - `$width`: a shortcut property that can substitute `$column: { width }` or `$column: { flex }`.
265 By default, a column gets flex:1 unless you specify $width. A value of 8 and higher is considered width's pixels,
266 while lower are flex-values.
267 - `real_path` path to server disk
268 - `files: boolean` allow to select a file. Default is `true`.
269 - `folders: boolean` allow to select a folder. Default is `false`.
270 - `defaultPath: string` what path to start from if no value is set. E.g. __dirname if you want to start with your plugin's folder.
271 - `fileMask: string` restrict files that are displayed. E.g. `*.jpg|*.png`
272 - `vfs_path` path to VFS
273 - `folders: boolean` set false to forbid selection of folders. Default is true.
274 - `files: boolean | string` set force to forbid selection of files. If you set a string, it will be used as a file-mask.
275 E.g. `*.jpg|*.png` Default is true.
276 Note: the path you configure inside the admin-panel may differ to what the frontend sees, because:
277 - a "root" is applied to the specific host/domain;
278 - a reverse-proxy may add something in front.
279 For this reason, if your config is marked with `frontend: true`, the value that will be actually sent to the frontend may be adjusted if necessary.
280 - `username`
281 - `groups: undefined | boolean` true if you want only groups, false if you want only users. Default is undefined.
282 - `multiple: boolean` if you set this to true, the field will allow the selection of multiple accounts,
283 and the resulting value will be an array of strings, instead of a string. Default is false.
284 - `date_time` a string in the form yyyy-mm-ddThh:mm:ss.cccZ
285 - `net_mask` a string
286 - `show_html` not a real field, but let you display some static content
287 - `html: string` HTML code to display.
288
289 ## api object
290
291 The `api` object you get as parameter of the `init` contains the following:
292
293 - `getConfig(key?: string): any` get plugin's config value, described in `exports.config`.
294 If key is not provided, an object with all keys is returned.
295 If it's an array/object, DON'T modify it to then use setConfig, as it won't persist. You should first clone it.
296
297 - `setConfig(key: string, value: any)` set plugin's config value.
298
299 - `subscribeConfig(key: string | string[], callback: (value: any) => void): Unsubscriber`
300 will immediately call `callback` with initial value, and then at each change.
301 Passing an array of keys, the `value` parameter becomes an object with the specified keys and respective values.
302 Will be automatically unsubscribed at plugin's unload.
303
304 - `getHfsConfig(key: string): any` similar to getConfig, but retrieves HFS' config instead.
305
306 - `log(...args)` print log in a standard form for plugins.
307
308 - `addBlock({ ip, expire?, comment?, disabled? }, merge?)` add a blocking rule on specified IP. You can use merge to
309 append the IP to an existing rule (if any, otherwise is created). Eg:
310 ```js
311 // try to append to existing rule, by comment
312 addBlock({ ip: '1.2.3.4' }, { comment: "banned by my plugin" })
313 ```
314
315 - `Const: object` all constants of the `const.ts` file are exposed here. E.g. BUILD_TIMESTAMP, API_VERSION, etc.
316
317 - `getConnections(): Connections[]` retrieve current list of active connections.
318
319 - `getCurrentUsername(ctx: Context): string` an empty string if no user is logged in in the specified session, or its username otherwise.
320
321 - `storageDir: string` folder where a plugin is supposed to store run-time data. This folder is preserved during
322 an update of the plugin, while the rest could be deleted.
323
324 - `events` this is the main events emitter used by HFS. These are backend-side events, not to be confused with frontend ones.
325 It's not the standard EventEmitter class, but the API is mostly the same.
326
327 - `events.on(name: string, listener: Callback): Callback`
328
329 call your listener every time the event is emitted.
330 The returned callback will unsubscribe the event.
331
332 - `events.once(name: string, listener?: Callback): Promise<eventArguments>`
333
334 when the event is emitted, your (optional) listener is called, and the returned promise is resolved.
335
336 - `require(module: string)` use this instead of standard `require` function to access modules already loaded by HFS. Example:
337 ```js
338 const { watchLoad } = api.require('./watchLoad')
339 ```
340 You *should* try to keep this kind of behavior at its minimum, as the name of sources and elements can change, and your
341 plugin can become incompatible with future versions.
342 If you need something for your plugin that's not covered by `api`, you can test it with this method, but you should
343 then discuss it on the forum because an addition to `api` is your best option for making a future-proof plugin.
344
345 - `customApiCall(method: string, ...params): any[]` this will invoke other plugins if they define `method`
346 exported inside `customApi: object`
347
348 - `openDb(filename: string, options): Promise<{ get, put, del, close, unlink, sublevel }>` LevelDB-like class for storage.
349 The specified file name will be stored in the "storage" folder of the plugin, by default.
350 DB is automatically closed when the plugin is unloaded. Refer to [dedicated documentation](https://www.npmjs.com/package/@rejetto/kvstorage) for details.
351
352 - `notifyClient(channel: string, eventName: string, data?: any)` send a message to those frontends that are on the same channel.
353
354 - `i18n(ctx: Context): Promise<{ t }>` if you need to translate messages inside http body, without the GUI, use this function
355 to instantiate translation for the language of the browser. You can then use the `t` function as documented in [dedicated section](Internationalization-i18n).
356
357 - `ctxBelongsTo(ctx: Context, accounts: strings[]): boolean` check if the current username, or any group it belongs to,
358 matches the provided accounts list. Backend counterpart of `HFS.userBelongsTo`.
359
360 - `setError(error: string)` set an error message that will be displayed in the admin-panel. Use an empty string to clear it.
361
362 - `misc` many functions and constants available in [misc.ts](https://github.com/rejetto/hfs/blob/main/src/misc.ts).
363 These are not documented, probably never will, and are subject to change without notifications,
364 but you can study the sources if you are interested in using them. It's just a shorter version of `api.require('./misc')`
365
366 - `getAccount(username: string): Account | undefined` retrieve an account object, or undefined of not found.
367 The `Account` object has the following properties:
368 `username: string`
369 `srp?: string` if this value is not present, then it's a group
370 `belongs?: string[]` list of groups this account belongs to
371 `ignore_limits?: boolean` don't apply limits to this account
372 `disable_password_change?: boolean` don't allow password change
373 `admin?: boolean` allow access to admin-panel
374 `redirect?: string` redirect to this URL as soon as the user logs in
375 `disabled?: boolean` forbid login
376 `expire?: Date` account expiration date
377 `days_to_live?: number` set expiration date (after this many days) automatically at next login
378 `allow_net?: string` allow login of this account only from this network mask
379 `require_password_change?: boolean` ask user to change password at next login
380 `plugin?: object` this can contain any information needed by plugins. It's free-form, but some fields are standard:
381 - `id?: string` name of the plugin responsible for this account
382 - `auth?: true` if the plugin is responsible for this authentication.
383 It will cause HFS to fallback to `clearTextLogin`, and the plugin shall respond to its corresponding event.
384
385 - `getAccounts(): string[]` retrieve list of all usernames
386
387 - `addAccount(username: string, properties: Partial<Account>, updateExisting=false): Promise<Account> | undefined`
388 If username already exists, it will ignore the request and return undefined, unless you set `updateExisting` to true.
389
390 - `delAccount(username: string): boolean` returns true if it succeeds.
391
392 - `updateAccount(account: Account, changes: Partial<Account>)` apply specified changes.
393
394 - `renameAccount(from: string, to: string): boolean` returns true if it succeeds.
395
396 - `_` [lodash library](https://lodash.com/docs/)
397
398 - `setInterval`, `setTimeout` same as standard js functions, but will automatically cancel if the plugin is unloaded.
399
400 - `onServer(cb: (Server) => any)` execute your callback on every instance of Server created by HFS.
401 It is the standard Node.js class, and it can be http or https. It can be instantiated multiple times.
402
403 - `normalizeFilename(filename: string): string` HFS applies some normalization to files, and so should you.
404 It's necessary when running on Mac and Windows, as they are case-insensitive.
405
406 ## Frontend JS
407
408 The following information applies to the frontend bundled with HFS.
409
410 Once your script is loaded into the frontend (via `frontend_js`, refer above), it will be executed as any other script in the browser.
411
412 To avoid conflicts with other plugins, we suggest to wrap all your code like this:
413 ```js
414 'use strict';{
415 // your code here
416 console.log('hi')
417 }
418 ```
419
420 ### HFS object
421
422 In frontend you will have access to the `HFS` object of the global scope, which has many properties:
423 - `onEvent` this is the main hook inside the frontend. Refer to dedicated section below.
424 - `apiCall(cmd: string, params?: object, options?: object): Promise<any>` request an [HTTP API](https://hfs-3.apidog.io/),
425 where `cmd` is the name and `params` are the respective parameters. Options are:
426 - `timeout?: number | false` in seconds
427 - `onResponse?: (res: Response, body: any) => any`
428 - `method?: string`
429 - `skipParse?: boolean`
430 - `skipLog?: boolean`
431 - `restUri?: string`
432 - `useApi(cmd: string | Falsy, params?: object, options?: object): object` hook form of `apiCall`.
433 The returned object contains:
434 - `data: any` result of the api
435 - `error: any` in case the api resulted in an error
436 - `reload: function` call it if you want to call the api again
437 - `loading: boolean` true if the api is loading
438 - `getData(): any` if you need to access to `data` inside closures, where it is stale if accessed directly
439 - `setData(value: any)` if you need to overwrite `data`
440 - `sub: function(callback)` if you need to subscribe for when the api is called again
441 - `reloadList()` cause the list of files to be reloaded
442 - `logout(): Promise` logout the current user
443 - `prefixUrl: string` normally an empty string, it will be set in case a [reverse-proxy wants to mount HFS on a path](https://github.com/rejetto/hfs/wiki/Reverse-proxy).
444 - `state: StateObject` [object with many values in it](https://github.com/rejetto/hfs/blob/main/frontend/src/state.ts)
445 - you'll find here some interesting values, like `username` and `loading`.
446 - `watchState(key: string, callback, now?: boolean): function`
447 - watch the `key` property of the state object above
448 - `callback(newValue)` will be called at each change
449 - pass `true` for the third parameter to also call the callback immediately, with current value
450 - use returned callback to stop watching
451 - `useSnapState(): StateObject` React hook version of the `state` object above
452 - `React` whole React object, as for `require('react')` (JSX syntax is not supported here)
453 - `h` shortcut for React.createElement
454 - `t` [translator function](https://github.com/rejetto/hfs/blob/main/frontend/src/i18n.ts)
455 - `_` [lodash library](https://lodash.com/docs/)
456 - `toast(message: string | ReactElement, type: ToastType='info')`
457 - show a brief message that doesn't steal focus
458 - `ToastType = 'error' | 'warning' | 'info' | 'success'`
459 - `dialogLib` this exposes all functions available in [dialog.ts](https://github.com/rejetto/hfs/blob/main/frontend/src/dialog.ts),
460 for example alertDialog and newDialog. These are not documented yet, and subject to change without notification,
461 but you can study the sources if you are interested in using them.
462 - `misc` many functions and constants available in [cross.ts](https://github.com/rejetto/hfs/blob/main/src/cross.ts). These are not documented, probably never will, and are subject to change without notifications, but you can study the sources if you are interested in using them.
463 - `navigate(uri: string)` use this if you have to change the page address without causing reload
464 - `emit(name: string, params?: object): any[]` use this to emit a custom event. Prefix name with your plugin name to avoid conflicts.
465 - `Icon: ReactComponent` Properties:
466 - `name: string` refer to file `icons.ts` for names, but you can also enter an emoji instead.
467 - `iconBtn(icon: string, onClick: function, props?: any)` render a React Icon Button. For icons, refer to `Icon` component.
468 - `Btn: ReactComponent}` Properties:
469 - `icon?: string`, `label?: string`, `tooltip?: string`, `toggled?: boolean`, `onClick?: function`,
470 `onClickAnimation?: boolean`, `asText?: boolean`, `successFeedback?: boolean`
471 - `domOn(eventName: string, cb: function, { target }?): function` convenient alternative to addEventListener/removeEventListener.
472 The default target is window. Returns a callback to remove the listener.
473 - `useBatch(worker, job): { data }` this is a bit complicated, please refer to source `shared/react.ts`.
474 - `getNotifications(channel: string, cb: (eventName: string, data:any) => void)`
475 receive messages when the backend uses `notifyClient` on the same channel.
476 - `html(html: string): ReactNode` convert html code to React
477 - `debounceAsync: function` like lodash.debounce, but also avoids async invocations to overlap.
478 For details please refer to `src/debounceAsync.ts`.
479 - `loadScript(uri: string): Promise` load a js file. If uri is relative, it is based on the plugin's public folder.
480 - `customRestCall(name: string, parameters?: object): Promise<any>` call backend functions exported with `customRest`.
481 - `userBelongsTo(groupOrUsername: string | string[]): boolean` returns true if the current account is or belongs to the name(s) specified.
482 Frontend counterpart of `api.ctxBelongsTo`.
483 - `DirEntry: class_constructor(n :string, otherProps?: DirEntry)` this is the class of the objects inside `HFS.state.list`;
484 in case you need to add to the list, do it by instantiating this class. E.g. `new HFS.DirEntry(name)`
485 - `fileShow(entry: DirEntry, options?: { startPlaying: true ): boolean` open file-show on the specified entry.
486 Returns falsy if entry is not supported.
487 - `copyTextToClipboard(text: string)` self-explanatory.
488 - `urlParams: object` you'll find each parameter in the URL mapped in this object as string.
489 - `pathSeparator: string` you'll find \ or / depending on what OS HFS is running on
490 - `fileShowComponents: { Video, Audio }` exposes standard components used by file-show. Can be useful if you need extend them, inside `fileShow` event.
491 - `isShowSupported(entry: DirEntry): boolean` true if the entry is supported by Show.
492 - `textSortCompare(a: string, b: string): number` the function HFS will use for text sorting.
493 Returns a negative if `a` must go before `b`, a positive if `b` must go before `a`, or zero if they have same order.
494 It's exposed for you to use, or to overwrite if you need.
495 - `elementToEntry(el: HTMLElement): DirEntry | undefined` given a DOM element, returns the DirEntry associated to it, if any.
496 - `isVideoComponent(Component): boolean` tell if the component is used by show for video files.
497 - `markVideoComponent(Component): Component` if you replace the show-video component with yours, wrap it with this function.
498 - `isAudioComponent(Component): boolean` tell if the component is used by show for audio files.
499 - `markAudioComponent(Component): Component` if you replace the show-audio component with yours, wrap it with this function.
500 - `customizeText(changes: { [key]: string }, languageCode?: string)` customize text. Find text keys to change, and assign new text.
501 If you don't specify a languageCode, the change will apply to all languages (and has precedence).
502 Find they keys in this file https://github.com/rejetto/hfs/blob/main/src/langs/hfs-lang-en.json (english language).
503
504 - The following properties are accessible only immediately at top-level; don't call it later in a callback.
505 - `getPluginConfig()` returns object of all config keys that are declared frontend-accessible by this plugin.
506 - `getPluginPublic()` returns plugin's public folder, with final slash. Useful to point to public files.
507
508 ### Frontend API events
509
510 API at this level is done with frontend-events, that you can handle by calling
511
512 ```typescript
513 HFS.onEvent(eventName: string, callback: (parameters: object, extra: object) => any)
514 ```
515
516 All events of this type have all parameters in a single object, so it's technically a single parameter.
517 Its content, and what you can return in your callback, vary with the event name.
518 Refer to the specific event for further information.
519 Second parameter is explained in the dedicated section, below.
520
521 Some frontend events can return HTML, which can be expressed in several ways:
522 - as a string containing markup
523 - as DOM Nodes, using methods like `document.createElement()`
524 - as a ReactNode or an array of them
525 - as a Promise for any of the above
526
527 So when referring to type `Html`, below, we are actually meaning `Promisable<string | Element | ReactNode | ReactNode[]>`.
528
529 These events will receive, in addition event's specific properties, a `def` property
530 with the *default* content that will be displayed if no callback returns a valid output.
531 It is useful if you want to embed such default content inside your content.
532 Most events have this `def` undefined as they have no default content and are designed for custom insertions,
533 but when this is not the case, you can replace the default content with nothing by returning `null`.
534 You can produce output for such events also by adding sections (with same name as the event) to file `custom.html`.
535
536 #### Extra object
537
538 This is an advanced topic, rarely needed.
539 The "extra" object is the second parameter of your callback, and has the following properties:
540 - `output: any[]` array of values returned by all plugin/callbacks (so far).
541 - `setOrder(order: number)` if you need to prioritize your output (and see it before) with respect to other plugins,
542 you can specify a negative number. Use a positive number to get the opposite.
543
544 #### Execution order
545
546 Callbacks configured by all plugins are executed in the order onEvent was called.
547 You can require executing your callback after others by appending `:after` to the event name.
548 E.g. HFS.onEvent("entryIcon:after", ...)
549
550 #### List of frontend events
551
552 This is a list of available frontend-events, with respective object parameter and output.
553
554 - `additionalEntryDetails`
555 - use this to add HTML at the beginning of the `entry-details` container.
556 - parameter `{ entry: DirEntry }` current entry. `DirEntry` extends `DirEntryBackend` and adds:
557 - `name: string` name of the entry.
558 - `ext: string` just the extension part of the name, dot excluded and lowercase.
559 - `isFolder: boolean` true if it's a folder.
560 - `uri: string` absolute uri of the entry.
561 - `cantOpen: boolean` true if current user has no permission to open this entry
562 - `getNext/getPrevious: ()=>DirEntry` return next/previous DirEntry in list
563 - `getNextFiltered/getPreviousFiltered: ()=>DirEntry` as above, but considers the filtered-list instead
564 - `getDefaultIcon: ()=>ReactElement` produces the default icon for this entry
565 - output `Html`
566 - `entry`
567 - called displaying each entry of the list, and optionally produce HTML code that will completely replace the entry row/slot.
568 - parameter `{ entry: DirEntry }` (refer above for DirEntry object)
569 - output `Html`
570 - return null if you want to hide this entry, or undefined to leave it unchanged
571 - `afterEntryName`
572 - use this to add HTML after the name of the entry.
573 - parameter `{ entry: DirEntry }` (refer above for DirEntry object)
574 - output `Html`
575 - `entryIcon`
576 - use this to change the entry icon.
577 - parameter `{ entry: DirEntry }` (refer above for DirEntry object)
578 - output `Html`
579 - `beforeHeader` & `afterHeader`
580 - use this to add HTML right before/after the `header` part
581 - output `Html`
582 - `beforeLogin`
583 - no parameter
584 - output `Html`
585 - you can generate inputs with a name, and they will be sent to the login API
586 - `beforeLoginSubmit`
587 - no parameter
588 - output `Html`
589 - you can generate inputs with a name, and they will be sent to the login API
590 - `loginUsernameField`
591 - no parameter
592 - output `Html`
593 - `loginPasswordField`
594 - no parameter
595 - output `Html`
596 - `fileMenu`
597 - add or manipulate entries of the menu. If you return something, that will be added to the menu.
598 You can also delete or replace the content of the `menu` array.
599 - parameter `{ entry: DirEntry, menu: FileMenuEntry[], props: FileMenuProp[] }`
600 - output `Promisable<undefined | FileMenuEntry | FileMenuEntry[]>`
601 ```typescript
602 interface FileMenuEntry {
603 id?: string,
604 label: ReactNode,
605 subLabel: ReactNode,
606 href?: string, // use this if you want your entry to be a link
607 icon?: string, // supports: emoji, name from a limited set
608 onClick?: () => (Promisable<boolean>) // return false to not close menu dialog
609 //...rest is transfered to <a> element, for example 'target', or 'title'
610 }
611 type FileMenuProp = { id?: string, label: ReactNode, value: ReactNode } | ReactElement
612 ```
613 Example, if you want to remove the 'show' item of the menu:
614 ```typescript
615 HFS.onEvent('fileMenu', ({ entry, menu }) => {
616 const index = menu.findIndex(x => x.id === 'show')
617 if (index >= 0)
618 menu.splice(index, 1)
619 })
620 ```
621 or if you like lodash, you can simply `HFS._.remove(menu, { id: 'show' })`
622 - `fileShow`
623 - you receive an entry of the list, and the default Component that will be used.
624 You can optionally replace Component in the parameters object, or return it, but replacing is chainable with other plugins.
625 Your component will be rendered with the following props:
626 - `src`: string, uri of the entry
627 - `className`: string, this must be reported in your component
628 - `onLoad`
629 - `onError`
630 - `onPlay`
631 - parameter `{ entry: DirEntry, Component: FC }` (refer above for DirEntry object)
632 - output `ReactComponent | undefined`
633 - `showPlay`
634 - emitted on each file played inside file-show. Use setCover if you want to customize the background picture.
635 - parameter `{ entry: DirEntry, setCover(uri: string), meta: { title, album, artist, year } }`
636 - `menuZip`
637 - parameter `{ def: ReactNode }`
638 - output `Html`
639 - `userPanelAfterInfo`
640 - no parameter
641 - output `Html`
642 - `uriChanged`
643 - DEPRECATED: use `watchState('uri', callback)` instead.
644 - parameter `{ uri: string, previous: string }`
645 - `sortCompare`
646 - you can decide the order of entries by comparing two entries.
647 Return a negative value if entry `a` must appear before `b`, or positive if you want the opposite.
648 Return zero or any falsy value if you want to leave the order to what the user decided in his options.
649 - parameter `{ a: DirEntry, b: DirEntry }`
650 - output `number | undefined`
651 - `enableEntrySelection`
652 - selection of multiple entries is used for some standard actions like deletion or zip.
653 When none of such standard actions is permitted on an entry, its selection control (checkbox) is disabled.
654 If you want to override this behavior, because you have a custom action that makes use of the selection, return `true`.
655 - parameter `{ entry: DirEntry }`
656 - output `boolean`
657 - `entryToggleSelection`
658 - an entry is being un/selected
659 - parameter `{ entry: DirEntry }`
660 - can be prevented
661 - `newListEntries`
662 - new entries for the list have being fetched from the server
663 - parameter `{ entries: DirEntry[] }`
664 - `loginOk`
665 - parameter `{ username }`
666 - `loginFailed`
667 - parameter `{ username, error }`
668 - All of the following have no parameters and you are supposed to output `Html` that will be displayed in the described place:
669 - `appendMenuBar` inside menu-bar, at the end
670 - `afterMenuBar` between menu-bar and breadcrumbs
671 - `afterBreadcrumbs` between breadcrumbs and folder-stats
672 - `afterFolderStats` between folder-stats and filter-bar
673 - `afterFilter` at the input of the filter-bar
674 - `afterList` at the end of the files list
675 - `footer` at the bottom of the screen, even after the clipboard-bar (when visible)
676 - `unauthorized` displayed behind the login dialog accessing a protected folder
677 - `userPanelAfterInfo` visible to logged-in users, after the click on the button with their username, between user-info and buttons
678
679 ## Backend events
680
681 These events happen in the server, and not in the browser.
682 You can listen to these events accessing `api.events` in the `init` function of the plugin.
683 E.g.:
684 ```js
685 exports.init = function(api) {
686 const cancelListening = api.events.on('spam', () => 'spam received!')
687 // pass the canceller callback to the 'unload', so the subscription will be correctly disposed when the plugin is stopped
688 return { unload: cancelListening }
689 }
690 ```
691
692 But javascript allows a shorter and equivalent syntax for the example above:
693
694 ```js
695 exports.init = api => ({
696 unload: api.events.on('spam', () => 'spam received!')
697 })
698 ```
699
700 ### Async
701
702 Only where specified, events support async listeners, like
703 ```js
704 api.events.on('deleting', async () => your-code-here)
705 ```
706
707 ### Stop, the way you prevent default behavior
708
709 Some events allow you to stop their default behavior, by returning `api.events.stop`.
710 This is reported in the list below with the word "preventable".
711
712 ```js
713 api.events.on('deleting', ({ node }) => {
714 if (!node.source.endsWith('.jpg'))
715 return api.events.stop
716 })
717 ```
718
719 The example above will return false only when the file is NOT ending with .jpg, thus allowing only jpg files to be deleted.
720
721 ### Available events
722
723 This section is still partially documented, and you may need to have a look at the sources for further details.
724
725 - `deleting` called just before trying to delete a file or folder (which still may not exist and fail)
726 - parameters: { node, ctx }
727 - async supported
728 - preventable
729 - `login`
730 - parameters: { ctx }
731 - `logout` called just before the logout is done
732 - parameters: { ctx }
733 - `attemptingLogin` called when the login process starts
734 - parameters: { ctx, username, via? }
735 - via?: string
736 - `'url'` if login is attempted via `?login=...`, or `'header'` if it's "Basic" authentication
737 (which includes credentials using the @-syntax in the URL), otherwise it's standard SRP login.
738 - async supported
739 - preventable
740 - `failedLogin`
741 - parameters: { ctx, username, via? }
742 - `clearTextLogin` give plugins the chance to authenticate users
743 - parameters: { ctx, username, password, via: 'url' | 'header' }
744 - async supported
745 - return: `true` to consider authentication done
746 - `finalizingLogin`
747 - parameters: { ctx, username, inputs }
748 - inputs: object
749 - merge of all inputs both from body and URL
750 - all fields with a `name` attribute in the form, included those added by plugins, are included
751 - async supported
752 - `configReady` when the config is fully loaded (the boolean flags whether we started without an existing config file)
753 - parameters: { startedWithoutConfig: boolean }
754 - `config.KEY` where KEY is the key of a config that has changed
755 - parameters: newValue
756 - `connectionClosed`
757 - parameters: connection
758 - `connection`
759 - parameters: connection
760 - `connectionUpdated`
761 - parameters: connection
762 - connectionNewIp
763 - parameters: connection
764 - `console`
765 - parameters: { ts, msg, k: 'log' | 'warn' | 'error' | 'debug' }
766 - `dynamicDnsError`
767 - parameters: { ts, error, url }
768 - `httpsReady` when the HTTPS server becomes available (no parameters)
769 - `spam`
770 - parameters: { ctx }
771 - `log`
772 - parameters: { ctx, length, user, ts, uri, extra }
773 - `error_log`
774 - parameters: { ctx, length, user, ts, uri, extra }
775 - `logRotated` called as soon as the zipping is done and just before the original is deleted.
776 If you need to work on the original, please be async (return promise) so that HFS knows when you are done and will delay deletion accordingly.
777 - parameters: { path, zipPath }
778 - async supported
779 - `accountRenamed`
780 - parameters: { from, to }
781 - `pluginDownload`
782 - `pluginUpdated`
783 - `pluginInstalled`
784 - parameters: plugin
785 - `pluginUninstalled`
786 - `pluginStopped`
787 - `pluginStarted`
788 - `listening`
789 - parameters: { server, port }
790 - `httpsServerOptions` if you need to customize the options of the https server.
791 - return: object with some properties [documented here](https://nodejs.org/api/https.html#httpscreateserveroptions-requestlistener).
792 - `uploadStart`
793 - parameters: { ctx, fullPath, tempName, resume, fullSize, writeStream }
794 - preventable
795 - return: callback to call when upload is finished
796 - `uploadFinished`
797 - parameters: { ctx, uri, fullPath, writeStream }
798 - if you change the file, you are responsible to update `uri` and `fullPath` of the object parameter.
799 - `publicIpsChanged`
800 - parameters: { IPs, IP4, IP6, IPX }
801 - `newSocket`
802 - parameters: { socket, ip }
803 - preventable
804 - return: you can return a string with a message that will be logged, and it will also cause disconnection
805 - `getList` called when get=list on legit requests to ?get=list
806 - parameters: { node, ctx }
807 - async supported
808 - stoppable
809 - `dirEntry` called for each entry before it is sent to the frontend list
810 - parameters: { entry, listUri, ctx, node }
811 - `entry: DirEntryBackend`
812 - `listUri: string`
813 - `ctx: Context`
814 - `node: VfsNode`
815 - async supported
816 - preventable (the entry will be skipped)
817 - note: legacy `onDirEntry` hooks run first; use this event for new code
818 - types `DirEntryBackend` fields:
819 - `n: string` name of the entry. (May include the relative path when searching in subfolders.)
820 - `s?: number` size of the entry, in bytes. It may be missing, for example, for folders.
821 - `m?: Date` modified-time.
822 - `c?: Date` creation-time.
823 - `p?: string` permissions.
824 - `comment?: string` comment for the entry.
825 - `web?: boolean` true for web links.
826 - `url?: string` target url for links.
827 - `target?: string` target for links.
828 - `icon?: string | true` icon override or true for "specific for this file".
829 - `order?: number` custom sort order.
830 - `listDiskFolder` called when a list is read from the disk; useful to implement a cache
831 - parameters: { path, ctx?, hidden }
832 - async supported
833 - return: to prevent the default listing and provide such a list yourself, return an array or iterator;
834 to let the default behavior while getting the content of the list, return a function, and it will be called for each
835 entry, passed as first parameter (an object of standard class fs.Dirent), and when the list is over it will be called
836 with a boolean, true if the list is completed and false if it was aborted
837 - `checkVfsPermission` called when a vfs permission is checked
838 - parameters: { node, perm, who, ctx }
839 - `perm: string` is the permission we are checking for
840 - `node: VfsNode` is the node on which the permission is checked
841 - `who: Who` is like `node[perm]`, that is the configured permission on the node,
842 but without references to other permissions or the object form, as they have already been translated
843 - return: an http error as number >= 400, or 0 or undefined
844 - `request` called for every incoming HTTP request, as an alternative to plugin `middleware` callbacks
845 - parameters: { ctx }
846 - async supported
847 - preventable
848 - return: callback(s) to run after request handling is completed, similar to `middleware` upstream
849 - `alert` called when HFS receives important announcements or security notices about your version (from the repo on GitHub)
850 - parameters: { message: string }
851
852 # Notifications (backend-to-frontend events)
853
854 You can send messages from the backend (plugin.js) using `api.notifyClient`, and receive on the frontend
855 using `HFS.getNotifications`. Find details in the reference above.
856
857 Example:
858
859 `plugin.js`
860 ```js
861 exports.init = api => {
862 const t = setInterval(() => api.notifyClient('test', 'message', 'hello'), 5000)
863 return {
864 frontend_js: 'main.js',
865 unload() {
866 clearInterval(t)
867 }
868 }
869 }
870 ```
871 `public/main.js`
872 ```js
873 HFS.getNotifications('test', console.log)
874 ```
875
876 # The `ctx` object
877
878 HFS is currently based on [Koa](https://koajs.com), so you'll see some things related to it in the backend API.
879 The most prominent is the `ctx` object, short for "context".
880 To know what the Context object offers, please refer to [Koa documentation](https://github.com/koajs/koa/blob/master/docs/api/context.md).
881 Additional methods you may be interested in:
882 - `ctx.disconnect(logMessage?: string)`
883 - `ctx.stop()` (explained in the *middleware* section)
884 - `ctx.isAborted(): boolean` if the client will actually never receive the response
885
886 HFS adds a few useful properties in the `ctx.state` object. Some of it may turn out to be useful,
887 so we prepared this list as a quick reference, but beware that it may become out of date and needs a double check.
888 If so, please report, and we'll do our best to update it asap.
889 Where there is too little information, you'll have to consult the source code. Apologies.
890
891 originalPath: string // before roots is applied
892 root?: string // root path applied to this request
893 browsing?: string // for admin/monitoring
894 dontLog?: boolean // don't log this request
895 logExtra?: object
896 completed?: Promise<unknown>
897 spam?: boolean // this request was marked as spam
898 params: Record<string, any>
899 account?: Account // user logged in
900 usernames?: Set<string> // cached expanded usernames for permission checks
901 revProxyPath: string
902 connection: Connection
903 whenProxyDetected?: Date
904 skipFilters?: boolean
905 vfsNode?: VfsNode
906 includesLastByte?: boolean
907 considerAsGui?: boolean // treat this request as part of the gui
908 serveApp?: boolean // please, serve the frontend app
909 uploadPath?: string // current one
910 uploads?: string[] // in case of request with potentially multiple uploads (POST), we register all filenames (no full path)
911 length?: number
912 opProgress?: number // current transfer progress, from 0 to 1
913 opTotal?: number // total bytes for the current operation
914 opOffset?: number // initial completed fraction for resumed operations
915 originalStream?: typeof ctx.body
916 uploadDestinationPath?: string // this value is the temporary file in uploadStart and the final one in uploadFinished
917 uploadSize?: number // final upload size
918 archive?: string
919 fileSource?: string // set when serving a file
920 fileStats?: Stat // file attributes
921 webdavDetected?: boolean // there's no clear way to detect a webdav client, but this is the result of the heuristic in HFS
922
923 ## Other files
924
925 Together with the main file (plugin.js), you can have other files, both for data and javascript to include with `require('./other-file')`.
926 Notice that in this case you don't use `api.require` but classic `require` because it's in your plugin folder.
927
928 These files have a special meaning:
929
930 - `public` folder, and its files will be accessible at `/~/plugins/PLUGIN_NAME/FILENAME`
931 - `custom.html` file, that works exactly like the main `custom.html`. Even when same section is specified
932 by 2 (or more) files, both contents are appended.
933
934 ## Node modules
935
936 You can run `npm install` inside your plugin's folder. This creates a `node_modules` directory that will be included as part of your plugin package.
937
938 ## Storage
939
940 Plugins that need to store generated data persistently should put all the files in the "storage" folder that is
941 automatically created for each plugin. In your plugin you can get this path by reading `api.storageDir`.
942 Failing to do so may cause your plugin to lose data during automatic updates.
943
944 There is a very powerful way to store data, that is by using `api.openDb`. This will automatically create the file
945 inside the storage folder. For further details please refer to the dedicated documentation.
946
947 ## Dependencies
948
949 You run vanilla javascript here, in the backend and/or in the browser, so the tools you have for dependencies
950 are the ones provided by node.js and/or the browser.
951 If you use a library for the browser, you'll have to keep it in the "public" folder, as the browser must be able to load it.
952 If you want to use a module for node.js, just include "node_modules" folder (not in "public" folder).
953 You can decide if you want to use some building system/transpiler, but you'll have to set it up yourself.
954
955 ## Publish your plugin
956
957 While you may just put a zip on any website, that would require manual installation.
958 If you want to appear in the Admin-panel, for easier finding and installation, please do as follows.
959
960 Be sure that you are exporting (not returning) the essential properties, like `apiRequired`.
961 Find the full list in the [Things a plugin can export](#things-a-plugin-can-export) section, tagged *[STATIC JSON]*.
962
963 Suggested method for publishing is to have a dedicated repository on GitHub, with topic `hfs-plugin`.
964 To set the topic go on the repo home and click on the gear icon near the "About" box.
965 Be sure to also fill the "exports.description" field, especially with words that people may search for.
966
967 If the name of the repository has the prefix "hfs-", it won't be displayed. Eg: "hfs-chat" will be displayed as "chat".
968 This is good way to have a clearer repository name on github, while avoiding being redundant within the context of the HFS' UI.
969
970 The files intended to be installed must go in a folder named `dist`.
971 You can keep other files outside.
972
973 If you have platform-dependent files, you can put those files in `dist-PLATFORM` or `dist-PLATFORM-ARCHITECTURE`.
974 For example, if you want some files to be installed only on Windows with Intel CPUs, put them in `dist-win32-x64`.
975
976 Possible values for platform are `aix`, `darwin`, `freebsd`, `linux`, `openbsd`, `sunos`, `win32`.
977
978 Possible values for CPUs are `arm`, `arm64`, `ia32`, `mips`, `mipsel`, `ppc`, `ppc64`, `s390`, `s390x`, `x64`.
979
980 You can refer to these published plugins for reference, like
981 - https://github.com/rejetto/simple-player/
982 - https://github.com/rejetto/theme-example/
983
984 Published plugins to have `exports.apiRequired`.
985
986 ### Multiple versions
987
988 It is possible to publish different versions of the plugin to be compatible with different versions of HFS.
989 To do that, just have your other versions in branches with name starting with `api`.
990 HFS will scan through them in inverted alphabetical order searching for a compatible one.
991
992 ## React developers
993
994 Using React is a good option to create a frontend parts for your plugin.
995 Most React developers are used to JSX, which is not (currently) supported here.
996 If you want, you can try solutions to support JSX, like transpiling.
997 Anyway, React is not JSX, and can be easily used without.
998
999 Any time in JSX you do
1000 ```jsx
1001 <button onClick={() => console.log('hi')}>Say hi</button>
1002 ```
1003
1004 This is just translated to
1005 ```js
1006 h('button', { onClick: () => console.log('hi') }, 'Say hi')
1007 ```
1008
1009 Where `h` is just `import { createElement as h } from 'react'`.
1010
1011 ## Internationalization (i18n)
1012
1013 To make your plugin multi-language you can use `t` function in javascript, like this: `t('myPlugin_greeting', "Hello!")`.
1014
1015 In frontend you get `t` from `HFS`, like this `const { t } = HFS`, while in backend you need to do
1016 `const { t } = await api.i18n(ctx)` inside an `async init` ([see example](https://github.com/rejetto/download-quota/blob/main/dist/plugin.js)).
1017 When possible, we suggest to do translation in the frontend.
1018
1019 Now that your code is ready, to translate in some language you'll add files like `hfs-lang-XX.json` to your plugin (same folder as plugin.js),
1020 where XX is the language code. The system is basically the same used to translate the rest of HFS,
1021 and you can [read details here](https://github.com/rejetto/hfs/wiki/Translation).
1022
1023 In the previous example `myPlugin_greeting` is the name of the translation, while `Hello!` is the default text.
1024 Instead of `myPlugin` use some text that you feel unique and no one else will use, to be sure that the same name
1025 is not used by another plugin, or even HFS in the future. We suggest to use your plugin's name in camelCase.
1026
1027 If you need to pass variables in the text, introduce a third parameter in the middle.
1028 Eg: `HFS.t('myPlugin_filter_count', {n:filteredVariable}, "{n} filtered")`
1029
1030 ### Language customization
1031
1032 One can change a specific text by overriding existing translation. Example: you want to change the text for "Options" to "Settings".
1033 If you want to override for a specific language, for example english with language-code `en`:
1034
1035 ```js
1036 HFS._.set(HFS.lang, 'en.translate.Options', 'Settings')
1037 ```
1038
1039 This works because all translations are stored inside `HFS.lang`.
1040 Using `HFS._.set` is not necessary, but in this case is convenient, because the language-code key may not exist.
1041
1042 If you want to override a text regardless of the language, use the special language-code `all`.
1043
1044 ## API version history
1045
1046 - 2
1047 - config.type:array
1048 - 3 (v0.21.0)
1049 - config.defaultValue
1050 - async for init/unload
1051 - api.log
1052 - 4 (v0.23.0)
1053 - config.type:real_path
1054 - api.subscribeConfig
1055 - api.setConfig
1056 - api.getHfsConfig
1057 - 4.1 (v0.23.4)
1058 - config.type:array added $width, $column and fixed height
1059 - 5 (v0.33.0)
1060 - frontend event: afterEntryName
1061 - 6 (v0.38.0)
1062 - config.frontend
1063 - 7 (v0.42.0)
1064 - frontend event: fileMenu
1065 - HFS.SPECIAL_URI, PLUGINS_PUB_URI, FRONTEND_URI,
1066 - 8 (v0.43.0)
1067 - entry.name & .uri
1068 - tools.dialogLib
1069 - HFS.getPluginConfig()
1070 - 8.1 (v0.45.0) should have been 0.44.0 but forgot to update number
1071 - full URL support for frontend_js and frontend_css
1072 - custom.html
1073 - entry.cantOpen, ext, isFolder
1074 - HFS.apiCall, reloadList, logout, h, React, state, t, _, dialogLib, Icon, getPluginPublic
1075 - second parameter of onEvent is now deprecated
1076 - renamed: additionalEntryProps > additionalEntryDetails & entry-props > entry-details
1077 - frontend event: entryIcon
1078 - 8.23 (v0.46.0)
1079 - entry.getNext, getPrevious, getNextFiltered, getPreviousFiltered, getDefaultIcon
1080 - platform-dependent distribution
1081 - HFS.watchState, emit, useApi
1082 - api.storageDir, customApiCall
1083 - exports.depend
1084 - frontend event: fileShow
1085 - 8.3 (v0.47.0)
1086 - HFS.useBatch
1087 - FileMenuEntry.id, .subLabel
1088 - 8.4 (v0.48.2)
1089 - HFS.fileShow
1090 - api.Const (api.const is now deprecated)
1091 - 8.5 (v0.49.0)
1092 - frontend event: entry
1093 - exports.onDirEntry: entry.icon
1094 - customApiCall supports any number of parameters
1095 - 8.65 (v0.51.0)
1096 - plugin's own hfs-lang files
1097 - HFS.state.props.can_overwrite
1098 - ctx.state.considerAsGui
1099 - frontend event: userPanelAfterInfo
1100 - breaking: moved custom properties from ctx to ctx.state
1101 - HFS.navigate
1102 - internationalization
1103 - 8.72 (v0.52.0)
1104 - HFS.toast
1105 - HFS.misc functions
1106 - HFS.state.uri
1107 - ~~frontend event: uriChanged~~
1108 - 8.891 (v0.53.0)
1109 - api.openDb
1110 - frontend event: menuZip
1111 - config.type:username
1112 - api.events class has changed
1113 - backend event: deleting
1114 - frontend event "fileMenu": changed props format
1115 - api.getConfig() without parameters
1116 - api.notifyClient + HFS.getNotifications
1117 - HFS.html
1118 - HFS.useSnapState
1119 - HFS.debounceAsync
1120 - HFS.loadScript
1121 - HFS.iconBtn
1122 - middleware: ctx.stop()
1123 - the old way of returning true is now deprecated
1124 - exports.customHtml
1125 - more functions in HFS.misc
1126 - frontend event 'entry' can now ask to skip an entry
1127 - backend events: login attemptingLogin failedLogin
1128 - 9.6 (v0.54.0)
1129 - frontend event: showPlay
1130 - api.addBlock
1131 - api.misc
1132 - api.events.stop
1133 - frontend event: paste
1134 - exports.customRest + HFS.customRestCall
1135 - config.type: vfs_path
1136 - frontend event: sortCompare
1137 - HFS.userBelongsTo
1138 - HFS.DirEntry
1139 - frontend event: appendMenuBar
1140 - config.helperText: basic md formatting
1141 - HFS.onEvent.setOrder
1142 - backend event: newSocket
1143 - 10.3 (v0.55.0)
1144 - HFS.copyTextToClipboard
1145 - HFS.urlParams
1146 - exports.beforePlugin + afterPlugin
1147 - config.type: color
1148 - config.showIf
1149 - init can now return directly the unload function
1150 - api.i18n
1151 - frontend event: newListEntries
1152 - HFS.fileShowComponents
1153 - api.ctxBelongsTo
1154 - api.getCurrentUsername
1155 - 11.6 (v0.56.0)
1156 - api.setError
1157 - frontend events: afterBreadcrumbs, afterFolderStats, afterFilter
1158 - config.type.vfs_path: folders, files
1159 - api.subscribeConfig supports multiple keys
1160 - api.getAccount, addAccount, delAccount, updateAccount, renameAccount, getUsernames
1161 - automatic unload of api.subscribeConfig
1162 - api._
1163 - config.type=showHtml
1164 - 12.3 (v0.57.0)
1165 - backend event: finalizingLogin, httpsServerOptions, clearTextLogin, listDiskFolder
1166 - frontend events: beforeLoginSubmit, loginUsernameField, loginPasswordField
1167 - exports.changelog
1168 - automatic unload of api.events listeners
1169 - removed DirEntry.t
1170 - api.setInterval, setTimeout
1171 - HFS.Btn
1172 - HFS.watchState added third parameter
1173 - frontend events: async for fileMenu and html-producers
1174 - config.type: date_time, net_mask
1175 - config.getError
1176 - 12.5 (v0.57.2)
1177 - changed parameters for events log, error_log, failedLogin, accountRenamed
1178 - HFS.fileShow return value
1179 - HFS.isShowSupported
1180 - 12.6 (v0.57.6)
1181 - HFS.textSortCompare
1182 - 12.8 (v0.57.10)
1183 - api.onServer
1184 - HFS.elementToEntry
1185 - backend event: checkVfsPermission
1186 - 12.9 (v0.57.14)
1187 - frontend event fileShow gets Component parameter
1188 - 12.91 (v0.57.15)
1189 - HFS.isVideoComponent, HFS.markVideoComponent, HFS.isAudioComponent, HFS.markAudioComponent
1190 - 12.92 (v0.57.16)
1191 - fixed checkVfsPermission
1192 - 12.93 (v0.57.17)
1193 - uploadStart now gets fullPath, tempName, resume, fullSize
1194 - ctx.disconnect(logMessage)
1195 - 12.94 (v0.57.24)
1196 - HFS.onEvent now supports :after
1197 - HFS.userBelongsTo now supports array of usernames
1198 - 12.96 (v0.57.27)
1199 - frontend events: loginOk, loginFailed
1200 - api.normalizeFilename
1201 - 12.97 (v0.57.28)
1202 - HFS.customizeText
1203 - 13 (v3.1.0)
1204 - backend events: dirEntry, request, alert
1205 - HFS.pathSeparator
1206 - config.type=show_html
1207 - 13.1 (v3.2.0)
1208 - backend events: logRotated
1209 - listDiskFolder gets "hidden" parameter
1210 - backend event uploadFinished: fullPath corresponds to the path that was actually written