Skip to content

Latest commit

 

History

History
461 lines (386 loc) · 22.5 KB

dev-plugins.md

File metadata and controls

461 lines (386 loc) · 22.5 KB

For plug-in makers

If the information you are searching for is not in this document, please ask.

A plug-in is a folder with a plugin.js file in it. To install a plugin you just copy the folder into plugins folder. You will find plugins folder near config.yaml, and then in USER_FOLDER/.hfs for Linux and Mac, or near hfs.exe on Windows.

Plug-ins can be hot-swapped, and at some extent can be edited without restarting the server.

Each plug-in has access to the same set of features. Normally you'll have a plug-in that's a theme, and another that's a firewall, but nothing is preventing a single plug-in from doing both tasks.

Exported object

plugin.js is a javascript module, and its main way to communicate with HFS is by exporting things. For example, it can define its description like this

exports.description = "I'm a nice plugin"

The set of things exported goes by the name "exported object". A plugin can define an init function like this:

exports.init = api => ({
    frontend_css: 'mystyle.css'
})

The init function is called by HFS when the module is loaded and should return an object with more things to add/merge to the exported object. In the example above we are asking a css file to be loaded in the frontend. Since it's a basic example, you could have simply defined it like this:

exports.frontend_css = 'mystyle.css'

but in more complex cases you'll need go through the init. Thus, you can decide to return things in the init function, or directly in the exports. If you need to access the api you must use init, since that's the only place where it is found, otherwise you can go directly with exports. The parameter api of the init is an object containing useful things we'll see later.

Let's first look at the things you can export:

Things a plugin can export

All the following properties are optional unless otherwise specified.

  • description: string try to explain what this plugin is for. (JSON syntax)
  • version: number use progressive numbers to distinguish each release
  • apiRequired: number | [min:number,max:number] declare version(s) for which the plugin is designed for. Mandatory. Refer to API version history
  • depend: { repo: string, version: number }[] declare what other plugins this depends on. (JSON syntax)
  • repo: string | object pointer to a GitHub repo where this plugin is hosted. (JSON syntax)
    • the string form is for GitHub repos. Example: "rejetto/file-icons"

    • the object form will point to other custom repo. Object properties:

      • web: string link to a web page
      • main: string link to the plugin.js (can be relative to web)
      • zip: string link to the zip with the whole plugin (can be relative to web)
      • zipRoot: string optional, in case the plugin in the zip is inside a folder

      Example:

      { 
        "web": "https://github.com/rejetto/file-icons", 
        "zip": "/archive/refs/heads/main.zip",
        "zipRoot: "file-icons-main/dist", 
        "main": "https://raw.githubusercontent.com/rejetto/file-icons/main/dist/plugin.js" 
      }
      

      Note that in this example we are pointing to a github repo just for clarity. You are not supposed to use this complicated object form to link github, use the string form. Plugins with custom repos are not included in search results, but the update feature will still work.

WARNING: All the properties above are a bit special and must go in exports only (thus, not returned in init) and the syntax used must be strictly JSON (thus, no single quotes, only double quotes for strings and objects).

  • init described in the previous section.

  • 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). You can also include external files, by entering a full URL. Multiple files can be specified as ['file1.css', 'file2.css'].

  • 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). You can also include external files, by entering a full URL.

  • middleware: (Context) => void | true | function a function that will be used as a middleware: use this to interfere with http activity.

    exports.middleware = ctx => {
      ctx.body = "You are in the wrong place"
      ctx.status = 404
    }

    You'll find more examples by studying plugins like vhosting or antibrute. This API is based on Koa, because that's what HFS is using. To know what the Context object contains please refer to Koa documentation. You don't get the next parameter as in standard Koa middlewares because this is different, but we are now explaining how to achieve the same results. To interrupt other middlewares on this http request, return true. If you want to execute something in the "upstream" of middlewares, return a function.

  • unload: function called when unloading a plugin. This is a good place for example to clearInterval().

  • onDirEntry: ({ entry: DirEntry, listUri: string }) => Promisable<void | false> by providing this callback you can manipulate the record that is sent to the frontend (entry), or you can return false to exclude this entry from the results. Refer to source frontend/src/stats.ts.

  • config: { [key]: FieldDescriptor } declare a set of admin-configurable values owned by the plugin that will be displayed inside Admin-panel for change. Each property is identified by its key, and the descriptor is another object with options about the field.

    Eg: you want a message text. You add this to your plugin.js:

    exports.config = { message: {} }

    Once the admin has chosen a value for it, the value will be saved in the main config file, under the plugins_config property.

    plugins_config:
      name_of_the_plugin:
        message: Hi there!

    When necessary your plugin will read its value using api.getConfig('message').

  • configDialog: DialogOptions object to override dialog options. Please refer to sources for details.

  • onFrontendConfig: (config: object) => void | object manipulate config values exposed to front-end

FieldDescriptor

Currently, these properties are supported:

  • type: 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'real_path' | 'array' . Default is string.
  • label: string what name to display next to the field. Default is based on key.
  • defaultValue: any value to be used when nothing is set.
  • helperText: string extra text printed next to the field.
  • frontend: boolean expose this setting on the frontend, so that javascript can access it as HFS.getPluginConfig()[CONFIG_KEY] but also css can access it as var(--PLUGIN_NAME-CONFIG_KEY)

Based on type, other properties are supported:

  • string
    • multiline: boolean. Default is false.
  • number
    • min: number
    • max: number
  • select
    • options: { [label]: AnyJsonValue }
  • multiselect it's like select but its result is an array of values.
  • array list of objects
    • fields: an object of FieldDescriptors, i.e. same format as config. This field will be use for both the configuration of the grid's column, and the form's field. Other than properties of FieldDescriptor you get these extra properties:
      • $column: where you can put all the properties you want specifically to be set on the grid's column.
      • $width: a shortcut property that can substitute $column: { width } or $column: { flex }. By default, a column gets flex:1 unless you specify $width. A value of 8 and higher is considered width's pixels, while lower are flex-values.
  • real_path path to server disk
    • files: boolean allow to select a file. Default is true.
    • folders: boolean allow to select a folder. Default is false.
    • 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.
    • fileMask: string restrict files that are displayed. E.g. *.jpg|*.png

api object

The api object you get as parameter of the init contains the following:

  • getConfig(key: string): any get config's value set up by using exports.config.

  • setConfig(key: string, value: any) set config's value set up by using exports.config.

  • subscribeConfig(key: string, callback: (value: any) => void): Unsubscriber will call callback with initial value and then at each change.

  • getHfsConfig(key: string): any similar to getConfig, but retrieves HFS' config instead.

  • log(...args) print log in a standard form for plugins.

  • Const: object all constants of the const.ts file are exposed here. E.g. BUILD_TIMESTAMP, API_VERSION, etc.

  • getConnections: Connections[] retrieve current list of active connections.

  • storageDir: string folder where a plugin is supposed to store run-time data. This folder is preserved during an update of the plugin, while the rest could be deleted.

  • events: EventEmitter this is the main events emitter used by HFS.

  • require: function use this instead of standard require function to access modules already loaded by HFS. Example:

    const { watchLoad } = api.require('./watchLoad')

    You should try to keep this kind of behavior at its minimum, as name of sources and elements can change, and your plugin can become incompatible with future versions. If you need something for your plugin that's not covered by api, you can test it with this method, but you should then discuss it on the forum because an addition to api is your best option for making a future-proof plugin.

  • customApiCall: (method: string, ...params) => any[] this will invoke other plugins if they define method exported inside customApi: object

Front-end specific

The following information applies to the default front-end, and may not apply to a custom one.

Once your script is loaded into the frontend (via frontend_js), you will have access to the HFS object in the global scope.

The HFS objects contains many properties:

  • onEvent this is the main API function inside the frontend. Refer to dedicated section below.
  • apiCall
  • useApi
  • reloadList
  • logout
  • prefixUrl: string normally an empty string, it will be set in case a reverse-proxy wants to mount HFS on a path.
  • state object with many values in it
  • watchState: (key: string, callback)=>function
    • watch the key property of the state object above
    • callback(newValue) will be called at each change
    • use returned callback to stop watching
  • React whole React object, as for require('react') (JSX syntax is not supported here)
  • h shortcut for React.createElement
  • t translator function
  • _ lodash library
  • toast: (message: string | ReactElement, type: ToastType='info')
    • show a brief message that doesn't steal focus
    • ToastType = 'error' | 'warning' | 'info' | 'success'
  • dialogLib this exposes all functions available in dialog.ts, for example alertDialog and newDialog. These are not documented yet, and subject to change without notification, but you can study the sources if you are interested in using them.
  • misc many functions and constants available in 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.
  • navigate: (uri: string): void use this if you have to change the page address without causing reload
  • emit: (name: string, params?: object) => any[] use this to emit a custom event. Prefix name with your plugin name to avoid conflicts.
  • Icon: ReactComponent Properties:
    • name: string refer to file icons.ts for names, but you can also enter an emoji instead.
  • useBatch: (worker, job) => any

The following properties are accessible only immediately at top-level; don't call it later in a callback.

  • getPluginConfig() returns object of all config keys that are declared frontend-accessible by this plugin.
  • getPluginPublic() returns plugin's public folder, with final slash. Useful to point to public files.

Front-end API events

API at this level is done with frontend-events, that you can handle by calling

HFS.onEvent(eventName, callback)

//type callback = (parameters: object) => any

Parameters of your callback and meaning of returned value varies with the event name. Refer to the specific event for further information. HFS object is the same you access globally. Here just for legacy, consider it deprecated.

Some frontend-events can return Html, which can be expressed in several ways

  • as string, containing markup
  • as DOM Nodes, as for document.createElement()
  • as ReactElement
  • null, undefined, false and empty-string will just be discarded

This is a list of available frontend-events, with respective object parameter and output.

  • additionalEntryDetails
    • you receive each entry of the list, and optionally produce HTML code that will be added in the entry-details container.

    • parameter { entry: Entry }

      The Entry type is an object with the following properties:

      • name: string name of the entry.
      • ext: string just the extension part of the name, dot excluded and lowercase.
      • isFolder: boolean true if it's a folder.
      • n: string name of the entry, including relative path when searched in sub-folders.
      • uri: string relative url of the entry.
      • s?: number size of the entry, in bytes. It may be missing, for example for folders.
      • t?: Date generic timestamp, combination of creation-time and modified-time.
      • c?: Date creation-time.
      • m?: Date modified-time.
      • p?: string permissions missing
      • cantOpen: boolean true if current user has no permission to open this entry
      • getNext/getPrevious: ()=>Entry return next/previous Entry in list
      • getNextFiltered/getPreviousFiltered: ()=>Entry as above, but considers the filtered-list instead
      • getDefaultIcon: ()=>ReactElement produces the default icon for this entry
    • output Html

  • entry
    • you receive each entry of the list, and optionally produce HTML code that will completely replace the entry row/slot.
    • parameter { entry: Entry } (refer above for Entry object)
    • output Html
  • afterEntryName
    • you receive each entry of the list, and optionally produce HTML code that will be added after the name of the entry.
    • parameter { entry: Entry } (refer above for Entry object)
    • output Html
  • entryIcon
    • you receive an entry of the list and optionally produce HTML that will be used in place of the standard icon.
    • parameter { entry: Entry } (refer above for Entry object)
    • output Html
  • beforeHeader & afterHeader
    • use this to produce content that should go right before/after the header part
    • output Html
  • beforeLogin
    • no parameter
    • output Html
  • fileMenu
    • add or manipulate entries of the menu. If you return something, that will be added to the menu. You can also delete or replace the content of the menu array.
    • parameter { entry: Entry, menu: FileMenuEntry[], props: FileMenuProp[] }
    • output undefined | FileMenuEntry | FileMenuEntry[]
      interface FileMenuEntry {
          id?: string, 
          label: ReactNode,
          subLabel: ReactNode,
          href?: string, // use this if you want your entry to be a link
          icon?: string, // supports: emoji, name from a limited set
          onClick?: () => (Promisable<boolean>) // return false to not close menu dialog
          //...rest is transfered to <a> element, for example 'target', or 'title' 
      }
      type FileMenuProp = [ReactNode,ReactNode] | ReactElement
      Example, if you want to remove the 'show' item of the menu:
      HFS.onEvent('fileMenu', ({ entry, menu }) => {
        const index = menu.findIndex(x => x.id === 'show')
        if (index >= 0)
            menu.splice(index, 1)
      })
      or if you like lodash, you can simply HFS._.remove(menu, { id: 'show' })
  • fileShow
    • you receive an entry of the list, and optionally produce React Component for visualization.
    • parameter { entry: Entry } (refer above for Entry object)
    • output ReactComponent
  • userPanelAfterInfo
    • no parameter
    • output Html
  • uriChanged
    • DEPRECATED: use watchState('uri', callback) instead.
    • parameter { uri: string, previous: string }

Other files

Together with the main file (plugin.js), you can have other files, both for data and javascript to include with require('./other-file'). Notice that in this case you don't use api.require but classic require because it's in your plugin folder.

These files have a special meaning:

  • public folder, and its files will be accessible at /~/plugins/PLUGIN_NAME/FILENAME
  • custom.html file, that works exactly like the main custom.html. Even when same section is specified by 2 (or more) files, both contents are appended.

Dependencies

You run vanilla javascript here, in the backend and/or in the browser, so the tools you have for dependencies are the ones provided by node.js and/or the browser. 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. If you want to use a module for node.js, just include "node_modules" folder (not in "public" folder). You can decide if you want to use some building system/transpiler, but you'll have to set it up yourself.

Publish your plug-in

Suggested method for publishing is to have a dedicated repository on GitHub, with topic hfs-plugin. To set the topic go on the repo home and click on the gear icon near the "About" box. Be sure to also fill the "description" field, especially with words that people may search for.

The files intended to be installed must go in a folder named dist. You can keep other files outside.

If you have platform-dependent files, you can put those files in dist-PLATFORM or dist-PLATFORM-ARCHITECTURE. For example, if you want some files to be installed only on Windows with Intel CPUs, put them in dist-win32-x64.

Possible values for platform are aix, darwin, freebsd, linux, openbsd, sunos, win32.

Possible values for CPUs are arm, arm64, ia32, mips, mipsel, ppc, ppc64, s390, s390x, x64.

You can refer to these published plugins for reference, like

Published plugins are required to specify the apiRequired property.

It is possible to publish different versions of the plugin to be compatible with different versions of HFS. To do that, just have your other versions in branches with name starting with api. HFS will scan through them in inverted alphabetical order searching for a compatible one.

React developers

Most React developers are used to JSX, which is not (currently) supported here. If you want, you can try solutions to JSX support, like transpiling. Anyway, React is not JSX, and can be easily used without.

Any time in JSX you do

<button onClick={() => console.log('hi')}>Say hi</button>

This is just translated to

h('button', { onClick: () => console.log('hi') }, 'Say hi')

Where h is just import { createElement as h } from 'react'.

Internationalization (i18n)

To make your plugin multi-language you can use HFS.t function in javascript, like this: HFS.t('myPlugin_greeting', "Hello!"). Now, to add translations, you'll add files like hfs-lang-XX.json to your plugin (same folder as plugin.js), where XX is the language code. The system is basically the same used to translate the rest of HFS, and you can read details here.

In the previous example myPlugin_greeting is the name of the translation, while Hello! is the default text. Instead of myPlugin use some text that you feel unique and no one else will use, to be sure that the same name is not used by another plugin, or even HFS in the future.

If you need to pass variables in the text, introduce a third parameter in the middle. Eg: HFS.t('myPlugin_filter_count', {n:filteredVariable}, "{n} filtered")

API version history

  • 8.72 (v0.52.0)
    • HFS.toast
    • HFS.misc functions
    • HFS.state.uri
    • new event: uriChanged
  • 8.65 (v0.51.0)
    • plugin's own hfs-lang files
    • HFS.state.props.can_overwrite
    • ctx.state.considerAsGui
    • new event: userPanelAfterInfo
    • breaking: moved custom properties from ctx to ctx.state
    • HFS.navigate
    • internationalization
  • 8.5 (v0.49.0)
    • new event: entry
    • exports.onDirEntry: entry.icon
    • customApiCall supports any number of parameters
  • 8.4 (v0.48.2)
    • HFS.fileShow
    • api.Const (api.const is now deprecated)
  • 8.3 (v0.47.0)
    • HFS.useBatch
    • FileMenuEntry.id, .subLabel
  • 8.23 (v0.46.0)
    • entry.getNext, getPrevious, getNextFiltered, getPreviousFiltered, getDefaultIcon
    • platform-dependent distribution
    • HFS.watchState, emit, useApi
    • api.storageDir, customApiCall
    • exports.depend
    • new event: fileShow
  • 8.1 (v0.45.0) should have been 0.44.0 but forgot to update number
    • full URL support for frontend_js and frontend_css
    • custom.html
    • entry.cantOpen, ext, isFolder
    • HFS.apiCall, reloadList, logout, h, React, state, t, _, dialogLib, Icon, getPluginPublic
    • second parameter of onEvent is now deprecated
    • renamed: additionalEntryProps > additionalEntryDetails & entry-props > entry-details
    • new event: entryIcon
  • 8 (v0.43.0)
    • entry.name & .uri
    • tools.dialogLib
    • HFS.getPluginConfig()
  • 7 (v0.42.0)
    • new event: fileMenu
    • HFS.SPECIAL_URI, PLUGINS_PUB_URI, FRONTEND_URI,
  • 6 (v0.38.0)
    • config.frontend
  • 5 (v0.33.0)
    • new event: afterEntryName
  • 4.1 (v0.23.4)
    • config.type:array added $width, $column and fixed height
  • 4 (v0.23.0)
    • config.type:real_path
    • api.subscribeConfig
    • api.setConfig
    • api.getHfsConfig
  • 3 (v0.21.0)
    • config.defaultValue
    • async for init/unload
    • api.log
  • 2
    • config.type:array