now plugins will be disabled by default, and you'll have to enable them (inverted logic)
Massimo Melina committed
Apr 26, 2022 at 10:45 UTC
b05aa56965fe1c32e00e5a88ed680a242e86a2b3
9 files changed
+56
-58
README.md
+9
-8
@@ -59,10 +59,10 @@ If your system is not covered, you can try this alternative version:
59
60
# Plug-ins
61
62
-If a `plugins` folder is present, HFS monitors it.
63
-Each plug-in is a sub-folder of `plugins` folder.
62
+To install a plugin you just copy its folder inside `plugins` folder.
63
+
64
+Delete it to uninstall.
65
65
-Plug-ins can be hot-swapped, and at some extent can be edited without restarting the server.
66
HFS will ignore all folders with `-disabled` at the end of the name.
67
68
# Developers section
@@ -97,12 +97,12 @@ In this latter case, the `DEV=1` you set before will make the server get the fil
97
98
A plug-in is a folder with a `plugin.js` file in it.
99
100
+Plug-ins can be hot-swapped, and at some extent can be edited without restarting the server.
101
+
102
Each plug-in has access to the same set of features.
103
Normally you'll have a plug-in that's a theme, and another that's a firewall,
104
but nothing is preventing a single plug-in from doing both tasks.
105
104
-You can find some examples distributed as `plugins.zip`.
105
-
106
`plugin.js` is a javascript module that exports an `init` function like this:
107
```js
108
exports.init = api => ({
@@ -111,10 +111,10 @@ exports.init = api => ({
111
```
112
113
The init function is called when the module is loaded and should return an object with things to customize.
114
-In this example we are asking a css file to be loaded in the frontend.
114
+In the example above we are asking a css file to be loaded in the frontend.
115
The parameter `api` object contains some useful things we'll see later.
116
-You can decide to return things in the `init` function, or directly in the `exports`. Normally you should use `init`
117
-if you need to access the api, otherwise you can go directly with `exports`.
116
+You can decide to return things in the `init` function, or directly in the `exports`.
117
+If you need to access the api you must use `init`, otherwise you can go directly with `exports`.
118
119
Let's first look at the things you can return:
120
@@ -250,6 +250,7 @@ Supported entries are:
250
```
251
Syntax supports, other than simple address, `*` as wildcard and CIDR format.
252
- `plugins_config` this is a generic place where you can find/put configuration for each plugin, at least those that need configuration.
253
+- `enable_plugins` if a plugin is not present here, it won't run. Defaults is `[ antibrute ]`.
254
- `custom_header` provide HTML code to be put at the top of your Frontend. Default is none.
255
- `localhost_admin` should Admin be accessed without credentials when on localhost. Default is true.
256
- `proxies` number of proxies between server and clients to be trusted about providing clients' IP addresses. Default is 0.
admin/src/PluginsPage.ts
+1
-1
@@ -52,7 +52,7 @@ export default function PluginsPage() {
52
icon: PowerSettingsNew,
53
title: (row.started ? "Stop" : "Start") + ' ' + id,
54
onClick: () =>
55
- apiCall('set_plugin', { id, disable: !!row.started }).then(() =>
55
+ apiCall('set_plugin', { id, enabled: !row.started }).then(() =>
56
alertDialog(row.started ? "Plugin is stopping" : "Plugin is starting")),
57
}),
58
h(IconBtn, {
plugins/middleware-example-disabled/plugin.js
deleted
-6
@@ -1,6 +0,0 @@
1
-exports.init = api => ({
2
- middleware(ctx) {
3
- ctx.body = 'This plugin is stopping you: ' + api.getConfig('message')
4
- return true // true = please stop
5
- }
6
-})
plugins/theme-example-disabled/plugin.js
-1
@@ -1,2 +1 @@
1
exports.frontend_css = 'style.css'
2
-exports.frontend_js = 'test.js'
plugins/theme-example-disabled/public/test.js
deleted
-1
@@ -1 +0,0 @@
1
-alert('ciao')
\ No newline at end of file
server/src/adminApis.ts
+12
-15
@@ -3,7 +3,7 @@
3
import { ApiError, ApiHandlers } from './apiMiddleware'
4
import { defineConfig, getConfig, getWholeConfig, setConfig } from './config'
5
import { getStatus, getUrls } from './listen'
6
-import { API_VERSION, BUILD_TIMESTAMP, CFG_PLUGINS_CONFIG,
6
+import { API_VERSION, BUILD_TIMESTAMP, CFG_ENABLE_PLUGINS, CFG_PLUGINS_CONFIG,
7
COMPATIBLE_API_VERSION, FORBIDDEN, HFS_STARTED, IS_WINDOWS, VERSION } from './const'
8
import vfsApis from './api.vfs'
9
import accountsApis from './api.accounts'
@@ -19,7 +19,7 @@ import { writeFile } from 'fs/promises'
19
import { createReadStream } from 'fs'
20
import * as readline from 'readline'
21
import { loggers } from './log'
22
-import { mapPlugins, getAvailablePlugins, Plugin } from './plugins'
22
+import { mapPlugins, getAvailablePlugins, Plugin, AvailablePlugin } from './plugins'
23
import { execFile } from 'child_process'
24
import { promisify } from 'util'
25
@@ -155,27 +155,24 @@ export const adminApis: ApiHandlers = {
155
get_plugins({}, ctx) {
156
const list = sendList([ ...mapPlugins(serialize), ...getAvailablePlugins() ])
157
return list.events(ctx, {
158
- pluginLoaded: p => list.add(serialize(p)),
159
- pluginReloaded: p => {
158
+ pluginInstalled: p => list.add(serialize(p)),
159
+ 'pluginStarted pluginStopped': p => {
160
const { id, ...rest } = serialize(p)
161
list.update({ id }, rest)
162
},
163
- pluginUnloaded: id => list.remove({ id }),
164
- pluginAvailableNoMore: p => list.remove({ id: p.id }),
165
- pluginAvailable: p => list.add(p),
163
+ pluginUninstalled: id => list.remove({ id }),
164
})
165
168
- function serialize(p: Readonly<Plugin>) {
169
- return Object.assign(p.getData(), _.pick(p, ['id','started']))
166
+ function serialize(p: Readonly<Plugin> | AvailablePlugin) {
167
+ return Object.assign('getData' in p ? p.getData() : p, { started: null }, _.pick(p, ['id','started']))
168
}
169
},
170
173
- async set_plugin({ id, disable, config }) {
174
- if (disable !== undefined) {
175
- const cfgK = 'disable_plugins'
176
- const a = getConfig(cfgK)
177
- if (a.includes(id) !== disable)
178
- setConfig({ [cfgK]: disable ? [...a, id] : a.filter((x: string) => x !== id) })
171
+ async set_plugin({ id, enabled, config }) {
172
+ if (enabled !== undefined) {
173
+ const a = getConfig(CFG_ENABLE_PLUGINS)
174
+ if (a.includes(id) !== enabled)
175
+ setConfig({ [CFG_ENABLE_PLUGINS]: enabled ? [...a, id] : a.filter((x: string) => x !== id) })
176
}
177
if (config) {
178
config = _.pickBy(config, v => v !== null)
server/src/const.ts
+1
@@ -30,6 +30,7 @@ export const FORBIDDEN = 403
30
export const IS_WINDOWS = process.platform === 'win32'
31
32
export const CFG_PLUGINS_CONFIG = 'plugins_config'
33
+export const CFG_ENABLE_PLUGINS = 'enable_plugins'
34
35
// we want this to be the first stuff to be printed, then we print it in this module, that is executed at the beginning
36
if (DEV) console.clear()
server/src/misc.ts
+6
-4
@@ -167,11 +167,13 @@ export function pendingPromise<T>() {
167
// install multiple handlers and returns a handy 'uninstall' function which requires no parameter. Pass a map {event:handler}
168
export function onOff(em: EventEmitter, events: { [eventName:string]: (...args: any[]) => void }) {
169
events = { ...events } // avoid later modifications, as we need this later for uninstallation
170
- for (const k in events)
171
- em.on(k, events[k])
170
+ for (const [k,cb] of Object.entries(events))
171
+ for (const e of k.split(' '))
172
+ em.on(e, cb)
173
return () => {
173
- for (const k in events)
174
- em.off(k, events[k])
174
+ for (const [k,cb] of Object.entries(events))
175
+ for (const e of k.split(' '))
176
+ em.off(e, cb)
177
}
178
}
179
server/src/plugins.ts
+27
-22
@@ -3,8 +3,8 @@
3
import glob from 'fast-glob'
4
import { watchLoad } from './watchLoad'
5
import _ from 'lodash'
6
-import { resolve } from 'path'
7
-import { API_VERSION, CFG_PLUGINS_CONFIG, COMPATIBLE_API_VERSION, PLUGINS_PUB_URI } from './const'
6
+import pathLib from 'path'
7
+import { API_VERSION, CFG_ENABLE_PLUGINS, CFG_PLUGINS_CONFIG, COMPATIBLE_API_VERSION, PLUGINS_PUB_URI } from './const'
8
import * as Const from './const'
9
import Koa from 'koa'
10
import { debounceAsync, getOrSet, onProcessExit, wantArray, watchDir } from './misc'
@@ -91,7 +91,7 @@ export class Plugin {
91
console.warn('invalid', k)
92
}
93
}
94
- events.emit(old ? 'pluginReloaded' : 'pluginLoaded', this)
94
+ events.emit(old || availablePlugins[id] ? 'pluginStarted' : 'pluginInstalled', this)
95
}
96
get middleware(): undefined | PluginMiddleware {
97
return this.data?.middleware
@@ -111,14 +111,18 @@ export class Plugin {
111
}
112
113
unload() {
114
- console.log('unloading plugin', this.id)
114
+ const { id } = this
115
+ console.log('unloading plugin', id)
116
try { this.data?.unload?.() }
117
catch(e) {
117
- console.debug('error unloading plugin', this.id, String(e))
118
+ console.debug('error unloading plugin', id, String(e))
119
}
119
- delete plugins[this.id]
120
+ delete plugins[id]
121
this.unwatch()
121
- events.emit('pluginUnloaded', this.id)
122
+ if (availablePlugins[id])
123
+ events.emit('pluginStopped', availablePlugins[id])
124
+ else
125
+ events.emit('pluginUninstalled', id)
126
}
127
}
128
@@ -126,7 +130,8 @@ type PluginMiddleware = (ctx:Koa.Context) => void | Stop | CallMeAfter
130
type Stop = true
131
type CallMeAfter = ()=>void
132
129
-let availablePlugins: Record<string, { id: string, description?: string, version?: number, apiRequired?: number }> = {}
133
+export interface AvailablePlugin { id: string, description?: string, version?: number, apiRequired?: number }
134
+let availablePlugins: Record<string, AvailablePlugin> = {}
135
136
export function getAvailablePlugins() {
137
return Object.values(availablePlugins)
@@ -138,18 +143,18 @@ if (!existsSync(PATH))
143
catch {}
144
watchDir(PATH, rescanAsap)
145
141
-const defaultValue = ['download-counter', 'redirect-root','max-downloads-ip']
142
-subscribeConfig({ k:'disable_plugins', defaultValue }, rescanAsap)
146
+const defaultValue = ['antibrute']
147
+subscribeConfig({ k: CFG_ENABLE_PLUGINS, defaultValue }, rescanAsap)
148
149
async function rescan() {
150
console.debug('scanning plugins')
151
const found = []
152
const foundDisabled: typeof availablePlugins = {}
148
- const disable_plugins = wantArray(getConfig('disable_plugins'))
153
+ const enable_plugins = wantArray(getConfig(CFG_ENABLE_PLUGINS))
154
for (let f of await glob(PATH+'/*/plugin.js')) {
155
const id = f.split('/').slice(-2)[0]
156
if (id.endsWith('-disabled')) continue
152
- if (disable_plugins.includes(id)) {
157
+ if (!enable_plugins.includes(id)) {
158
const pl = foundDisabled[id] = { id } as typeof foundDisabled[0]
159
try {
160
const source = await readFile(f, 'utf8')
@@ -163,13 +168,13 @@ async function rescan() {
168
found.push(id)
169
if (plugins[id]) // already loaded
170
continue
166
- f = resolve(f) // without this, import won't work
171
+ const module = pathLib.resolve(f)
172
const { unwatch } = watchLoad(f, async () => {
173
try {
174
console.log(plugins[id] ? 'reloading plugin' : 'loading plugin', id)
170
- const { init, ...data } = await import(f)
175
+ const { init, ...data } = await import(module)
176
delete data.default
172
- deleteModule(require.resolve(f)) // avoid caching
177
+ deleteModule(require.resolve(module)) // avoid caching at next import
178
if (data.apiRequired > API_VERSION)
179
console.log('plugin', id, 'may not work correctly as it is designed for a newer version of HFS')
180
if (data.apiRequired < COMPATIBLE_API_VERSION)
@@ -190,16 +195,16 @@ async function rescan() {
195
}
196
})
197
}
198
+ for (const id in foundDisabled)
199
+ if (!availablePlugins[id] && !plugins[id])
200
+ events.emit('pluginInstalled', foundDisabled[id])
201
+ for (const id in availablePlugins)
202
+ if (!foundDisabled[id] && !plugins[id])
203
+ events.emit('pluginUninstalled', id)
204
+ availablePlugins = foundDisabled
205
for (const id in plugins)
206
if (!found.includes(id))
207
plugins[id].unload()
196
- for (const k in foundDisabled)
197
- if (!availablePlugins[k])
198
- events.emit('pluginAvailable', foundDisabled[k])
199
- for (const k in availablePlugins)
200
- if (!foundDisabled[k])
201
- events.emit('pluginAvailableNoMore', availablePlugins[k])
202
- availablePlugins = foundDisabled
208
}
209
210
function deleteModule(id: string) {