plug-ins system
Massimo Melina committed
Jan 3, 2022 at 21:00 UTC
bf7d1eb83ef942cab3ca07952b4a526ccc053c93
10 files changed
+90
-4
README.md
+6
@@ -85,3 +85,9 @@ For this reason HFS needs that your accounts file is writable.
85
You'll find the output in `dist` folder.
86
87
Now to run it you should `cd dist` and `node .`
88
+
89
+# Plugins
90
+
91
+We are slowly introducing a plug-ins system.
92
+Each plug-in is a `plugin.yaml` file sub-folder of `plugins` folder.
93
+You can quickly disable a plug-in by appending `-disabled` to the plug-in folder.
frontend/src/App.tsx
+4
@@ -2,14 +2,18 @@ import { BrowserRouter, Route, Routes } from "react-router-dom"
2
import { createElement as h, Fragment } from 'react'
3
import { BrowseFiles } from "./BrowseFiles";
4
import { Dialogs } from './dialog'
5
+import { useApi } from "./api";
6
7
function App() {
8
+ const extras = useApi('extras_to_load')
9
return h(Fragment, {},
10
h(BrowserRouter, {},
11
h(Routes, {},
12
h(Route, { path:'*', element:h(BrowseFiles) })
13
)
14
),
15
+ extras?.css?.map((href:string) =>
16
+ h('link', { key:href, rel:"stylesheet", type:"text/css", href })),
17
h(Dialogs)
18
)
19
}
plugins/example-disabled/plugin.yaml
new
+4
@@ -0,0 +1,4 @@
1
+ver: 1
2
+frontend:
3
+ load:
4
+ css: style.css
plugins/example-disabled/public/star.png
Binary files /dev/null and b/plugins/example-disabled/public/star.png differ
plugins/example-disabled/public/style.css
new
+1
@@ -0,0 +1 @@
1
+header { background:#fff url(star.png) top right no-repeat; }
src/const.ts
+1
@@ -8,6 +8,7 @@ if (DEV)
8
const SPECIAL_URI = '/~/'
9
export const FRONTEND_URI = SPECIAL_URI + 'front/'
10
export const API_URI = SPECIAL_URI + 'api/'
11
+export const PLUGINS_PUB_URI = SPECIAL_URI + 'plugins/'
12
13
export const argv = minimist(process.argv.slice(2))
14
src/frontEndApis.ts
+11
-1
@@ -5,6 +5,9 @@ import { basename } from 'path'
5
import { getCurrentUsername, updateAccount, verifyLogin } from './perm'
6
import { stat } from 'fs/promises'
7
import { ApiHandlers } from './apis'
8
+import { plugins } from './plugins'
9
+import { wantArray } from './misc'
10
+import { PLUGINS_PUB_URI } from './const'
11
12
export const frontEndApis: ApiHandlers = {
13
@@ -86,7 +89,14 @@ export const frontEndApis: ApiHandlers = {
89
account.password = newPassword
90
})
91
return true
89
- }
92
+ },
93
+
94
+ async extras_to_load() {
95
+ const css = []
96
+ for (const k in plugins)
97
+ css.push(...wantArray(plugins[k].data.frontend?.load?.css).map(f => PLUGINS_PUB_URI + k + '/' + f))
98
+ return { css }
99
+ },
100
}
101
102
interface DirEntry { n:string, s?:number, m?:Date, c?:Date }
src/index.ts
+13
-1
@@ -3,7 +3,7 @@ import mount from 'koa-mount'
3
import bodyParser from 'koa-bodyparser'
4
import { apiMiddleware } from './apis'
5
import { serveFrontend } from './serveFrontend'
6
-import { API_URI, DEV, FRONTEND_URI } from './const'
6
+import { API_URI, DEV, FRONTEND_URI, PLUGINS_PUB_URI } from './const'
7
import { serveFile } from './serveFile'
8
import { vfs } from './vfs'
9
import { isDirectory } from './misc'
@@ -17,6 +17,10 @@ import session from 'koa-session'
17
import { zipStreamFromFolder } from './zip'
18
import { frontEndApis } from './frontEndApis'
19
import { log } from './log'
20
+import './plugins'
21
+import { createReadStream } from 'fs'
22
+import { PATH as PLUGINS_PATH } from './plugins'
23
+import mime from 'mime-types'
24
25
const BUILD_TIMESTAMP = ""
26
@@ -47,10 +51,18 @@ app.use(mount(API_URI, new Koa()
51
const serveFrontendPrefixed = mount(FRONTEND_URI.slice(0,-1), serveFrontend)
52
app.use(async (ctx, next) => {
53
const { path } = ctx
54
+ if (path.includes('..'))
55
+ ctx.throw(500)
56
if (ctx.body)
57
return await next()
58
if (path.startsWith(FRONTEND_URI))
59
return await serveFrontendPrefixed(ctx,next)
60
+ if (path.startsWith(PLUGINS_PUB_URI)) { // expose public plugins' files
61
+ const a = path.substring(PLUGINS_PUB_URI.length).split('/')
62
+ a.splice(1,0,'public')
63
+ ctx.type = mime.lookup(path) || ''
64
+ return ctx.body = createReadStream(PLUGINS_PATH + '/' + a.join('/'))
65
+ }
66
const decoded = decodeURI(path)
67
const node = await vfs.urlToNode(decoded, ctx)
68
if (!node)
src/misc.ts
+4
-2
@@ -2,6 +2,7 @@ import fs from 'fs/promises'
2
import { objSameKeys } from './obj'
3
import { FSWatcher, watch } from 'fs'
4
import yaml from 'yaml'
5
+import _ from 'lodash'
6
7
export function enforceFinal(sub:string, s:string) {
8
return s.endsWith(sub) ? s : s+sub
@@ -50,10 +51,11 @@ export function wantArray(x:any) {
51
export function watchLoad(path:string, parser:(data:any)=>void|Promise<void>) {
52
let doing = false
53
let watcher: FSWatcher
54
+ const debounced = _.debounce(load, 100)
55
const timer = setInterval(()=>{
56
try {
55
- watcher = watch(path, load)
56
- load().then()
57
+ watcher = watch(path, debounced)
58
+ debounced()
59
clearInterval(timer)
60
}
61
catch(e){
src/plugins.ts
new
+46
@@ -0,0 +1,46 @@
1
+import { watch } from 'fs'
2
+import glob from 'fast-glob'
3
+import { watchLoad } from './misc'
4
+import _ from 'lodash'
5
+
6
+export const PATH = 'plugins'
7
+
8
+try {
9
+ const debounced = _.debounce(rescan, 1000)
10
+ watch(PATH, debounced)
11
+ debounced()
12
+}
13
+catch(e){
14
+ console.debug('plugins not found')
15
+}
16
+
17
+interface Plugin {
18
+ data: any
19
+ unwatch: ()=>void
20
+}
21
+export const plugins: Record<string, Plugin> = {}
22
+
23
+async function rescan() {
24
+ console.debug('scanning plugins')
25
+ const found = []
26
+ for (const f of await glob(PATH+'/*/plugin.yaml')) {
27
+ const k = f.split('/').slice(-2)[0]
28
+ if (k.endsWith('-disabled')) continue
29
+ found.push(k)
30
+ if (plugins[k]) // already loaded
31
+ continue
32
+ const unwatch = watchLoad(f, data => {
33
+ console.log('loading plugin', k)
34
+ plugins[k] = { data, unwatch }
35
+ })
36
+ }
37
+ for (const k in plugins)
38
+ if (!found.includes(k))
39
+ unloadPlugin(k)
40
+}
41
+
42
+function unloadPlugin(k: string) {
43
+ console.log('unloading plugin', k)
44
+ plugins[k]?.unwatch()
45
+ delete plugins[k]
46
+}