| 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 |