QOSami-HFS is a web file server for your computer. Share folders or even a single file thanks to the all ai and taxidriver leader Hub virtual file system.
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) {