custom.html

Massimo Melina committed Feb 24, 2023 at 19:54 UTC 9b9ba605ba61723152fedd96d8c291cb5fdf9b4b
7 files changed +62 -14
README.md
-1
@@ -165,7 +165,6 @@ an *env* called `HFS_CONFIG`. Any relative path provided is relative to the *cwd
165 Syntax supports, other than simple address, `*` as wildcard and CIDR format.
166 - `plugins_config` this is a generic place where you can find/put configuration for each plugin, at least those that need configuration.
167 - `enable_plugins` if a plugin is not present here, it won't run. Defaults is `[ antibrute ]`.
168 -- `custom_header` provide HTML code to be put at the top of your Frontend. Default is none.
168 - `localhost_admin` should Admin be accessed without credentials when on localhost. Default is true.
169 - `proxies` number of proxies between server and clients to be trusted about providing clients' IP addresses. Default is 0.
170 - `keep_unfinished_uploads` should unfinished uploads be deleted immediately when interrupted. Default is true.
admin/src/OptionsPage.ts
-3
@@ -128,9 +128,6 @@ export default function OptionsPage() {
128 label: "Min. available disk space", helperText: "Reject uploads that don't comply" },
129 { k: 'zip_calculate_size_for_seconds', comp: NumberField, label: "Calculate ZIP size for", unit: "seconds",
130 helperText: "If time is not enough, the browser will not show download percentage" },
131 - { k: 'custom_header', multiline: true, sm: 12, md: 6, sx: { '& textarea': { fontFamily: 'monospace' } },
132 - helperText: "Any HTML code here will be displayed on top of the Frontend"
133 - },
131 { k: 'mime', comp: StringStringField,
132 keyLabel: "Files", keyWidth: 7,
133 valueLabel: "Mime type", valueWidth: 4
frontend/src/components.ts
+6 -4
@@ -54,12 +54,14 @@ export function Html({ code, ...rest }:{ code:string } & HTMLAttributes<any>) {
54 }
55
56 export function useCustomCode(name: string, props={}) {
57 - const code = useMemo(()=>
58 - hfsEvent(name, props)
57 + const code = useMemo(() => {
58 + const ret = hfsEvent(name, props)
59 .filter(x => x === 0 || x)
60 .map((x, key) => isValidElement(x) ? h(Fragment, { key }, x)
61 : typeof x === 'string' ? h(Html, { key, code: x })
62 - : h('div', { key }, x)),
63 - Object.values(props))
62 + : h('div', { key }, x))
63 + ret.push((window as any).HFS.customHtml[name] || null)
64 + return ret
65 + }, Object.values(props))
66 return h(Fragment, { children: code })
67 }
src/customHtml.ts new
+40
@@ -0,0 +1,40 @@
1 +import { existsSync, writeFileSync } from 'fs'
2 +import events from './events'
3 +import { prefix } from './misc'
4 +import { customHeader } from './frontEndApis'
5 +import { watchLoad } from './watchLoad'
6 +import { proxy } from 'valtio'
7 +
8 +export const customHtmlState = proxy<{
9 + sections: Map<string,string>
10 +}>({
11 + sections: new Map()
12 +})
13 +
14 +const FILE = 'custom.html'
15 +
16 +if (!existsSync(FILE))
17 + events.once('config ready', () => {
18 + const legacy = prefix('[beforeHeader]\n', customHeader.get())
19 + writeFileSync(FILE, legacy)
20 + customHeader.set(undefined) // get rid of it
21 + })
22 +watchLoad(FILE, data => {
23 + const re = /^\[(\w+)] *$/gm
24 + customHtmlState.sections.clear()
25 + if (!data) return
26 + let name: string | undefined = 'top'
27 + do {
28 + let last = re.lastIndex
29 + const match = re.exec(data)
30 + const content = data.slice(last, !match ? undefined : re.lastIndex - (match?.[0]?.length || 0)).trim()
31 + if (content)
32 + customHtmlState.sections.set(name, content)
33 + name = match?.[1]
34 + } while (name)
35 +})
36 +
37 +export function getSection(name: string) {
38 + return customHtmlState.sections.get(name) || ''
39 +}
40 +
src/frontEndApis.ts
+1 -1
@@ -20,7 +20,7 @@ import { mkdir, readFile, rm } from 'fs/promises'
20 import { join } from 'path'
21 import { wantArray } from './misc'
22
23 -const customHeader = defineConfig('custom_header')
23 +export const customHeader = defineConfig<string | undefined>('custom_header')
24
25 export const frontEndApis: ApiHandlers = {
26 file_list,
src/misc.ts
+1 -1
@@ -20,7 +20,7 @@ export function enforceFinal(sub:string, s:string) {
20 return s.endsWith(sub) ? s : s+sub
21 }
22
23 -export function prefix(pre:string, v:string|number, post:string='') {
23 +export function prefix(pre:string, v:string|number|undefined, post:string='') {
24 return v ? pre+v+post : ''
25 }
26
src/serveGuiFiles.ts
+14 -4
@@ -18,6 +18,8 @@ import { ApiError } from './apiMiddleware'
18 import { join, extname } from 'path'
19 import { getOrSet, newObj, onlyTruthy } from './misc'
20 import { favicon, title } from './adminApis'
21 +import { subscribe } from 'valtio'
22 +import { customHtmlState, getSection } from './customHtml'
23 import _ from 'lodash'
24
25 // in case of dev env we have our static files within the 'dist' folder'
@@ -25,7 +27,8 @@ const DEV_STATIC = process.env.DEV ? 'dist/' : ''
27
28 function serveStatic(uri: string): Koa.Middleware {
29 const folder = uri.slice(2,-1) // we know folder is very similar to uri
28 - const cache: Record<string, Promise<string>> = {}
30 + let cache: Record<string, Promise<string>> = {}
31 + subscribe(customHtmlState, () => cache = {}) // reset cache at every change
32 return async (ctx, next) => {
33 if(ctx.method === 'OPTIONS') {
34 ctx.status = HTTP_NO_CONTENT
@@ -82,7 +85,7 @@ async function treatIndex(ctx: Koa.Context, body: string, filesUri: string) {
85 configs = getPluginInfo(name).onFrontendConfig?.(configs) || configs
86 return !_.isEmpty(configs) && [name, configs]
87 })))
85 - return body
88 + let ret = body
89 .replace(/((?:src|href) *= *['"])\/?(?![a-z]+:\/\/)/g, '$1' + filesUri)
90 .replace('<HFS/>', () => `
91 ${!isFrontend ? '' : `
@@ -94,8 +97,10 @@ async function treatIndex(ctx: Koa.Context, body: string, filesUri: string) {
97 VERSION,
98 API_VERSION,
99 session: session instanceof ApiError ? null : session,
97 - plugins
98 - }, null, 4)}
100 + plugins,
101 + customHtml: _.omit(Object.fromEntries(customHtmlState.sections),
102 + ['top','bottom']), // excluding sections we apply in this phase
103 + }, null, 4)}
104 document.documentElement.setAttribute('ver', '${VERSION.split('-')[0] /*for style selectors*/}')
105 </script>
106 <style>
@@ -107,6 +112,11 @@ async function treatIndex(ctx: Koa.Context, body: string, filesUri: string) {
112 ${css.map(uri => `<link rel='stylesheet' type='text/css' href='${uri}'/>`).join('\n')}
113 ${js.map(uri => `<script defer src='${uri}'></script>`).join('\n')}
114 `)
115 + if (isFrontend)
116 + ret = ret
117 + .replace('<body>', '<body>' + getSection('top'))
118 + .replace('</body>', getSection('bottom') + '</body>')
119 + return ret
120 }
121
122 function serializeCss(v: any) {