@samitouri / QOSami-HFS / commits / a09eceb1

better code: simpler

Massimo Melina committed Jun 27, 2023 at 14:53 UTC a09eceb18038f7863689035ff578dd8800b2fce0
3 files changed +83 -93
src/index.ts
+4 -11
@@ -6,17 +6,10 @@ import mount from 'koa-mount'
6 import { apiMiddleware } from './apiMiddleware'
7 import { API_URI, DEV } from './const'
8 import { frontEndApis } from './frontEndApis'
9 -import { log } from './log'
9 +import { logMw } from './log'
10 import { pluginsMiddleware } from './plugins'
11 import { throttler } from './throttler'
12 -import {
13 - headRequests,
14 - gzipper,
15 - serveGuiAndSharedFiles,
16 - someSecurity,
17 - prepareState,
18 - paramsDecoder
19 -} from './middlewares'
12 +import { headRequests, gzipper, serveGuiAndSharedFiles, someSecurity, prepareState, paramsDecoder } from './middlewares'
13 import './listen'
14 import './commands'
15 import { adminApis } from './adminApis'
@@ -34,11 +27,11 @@ app.use(someSecurity)
27 .use(session({ key: 'hfs_$id', signed: true, rolling: true }, app))
28 .use(prepareState)
29 .use(headRequests)
37 - .use(log())
30 + .use(logMw)
31 .use(throttler)
32 .use(gzipper)
33 .use(paramsDecoder) // must be done before plugins, so they can manipulate params
41 - .use(pluginsMiddleware())
34 + .use(pluginsMiddleware)
35 .use(mount(API_URI, apiMiddleware({ ...frontEndApis, ...adminApis })))
36 .use(serveGuiAndSharedFiles)
37 .on('error', errorHandler)
src/log.ts
+48 -49
@@ -62,57 +62,56 @@ errorLogFile.sub(path => {
62 const logRotation = defineConfig('log_rotation', 'weekly')
63 const dontLogNet = defineConfig('dont_log_net', '127.0.0.1|::1', v => makeNetMatcher(v))
64
65 -export function log(): Koa.Middleware {
66 - const debounce = _.debounce(cb => cb(), 1000)
67 - return async (ctx, next) => { // wrapping in a function will make it use current 'mw' value
68 - const now = new Date()
69 - await next()
70 - console.debug(ctx.status, ctx.method, ctx.path)
71 - Promise.race([ once(ctx.res, 'finish'), once(ctx.res, 'close') ]).then(() => {
72 - if (dontLogNet.compiled()(ctx.ip)) return
73 - const isError = ctx.status >= 400
74 - const logger = isError && accessErrorLog || accessLogger
75 - const rotate = logRotation.get()?.[0]
76 - let { stream, last, path } = logger
77 - if (!stream) return
78 - logger.last = now
79 - if (rotate && last) { // rotation enabled and a file exists?
80 - const passed = Number(now) - Number(last)
81 - - 3600_000 // be pessimistic and count a possible DST change
82 - if (rotate === 'm' && (passed >= 31*DAY || now.getMonth() !== last.getMonth())
83 - || rotate === 'd' && (passed >= DAY || now.getDate() !== last.getDate()) // checking passed will solve the case when the day of the month is the same but a month has passed
84 - || rotate === 'w' && (passed >= 7*DAY || now.getDay() < last.getDay())) {
85 - stream.end()
86 - const postfix = last.getFullYear() + '-' + doubleDigit(last.getMonth() + 1) + '-' + doubleDigit(last.getDate())
87 - try { // other logging requests shouldn't happen while we are renaming. Since this is very infrequent we can tolerate solving this by making it sync.
88 - renameSync(path, path + '-' + postfix)
89 - }
90 - catch(e) { // ok, rename failed, but this doesn't mean we ain't gonna log
91 - console.error(e)
92 - }
93 - stream = logger.reopen() // keep variable updated
94 - if (!stream) return
65 +const debounce = _.debounce(cb => cb(), 1000)
66 +
67 +export const logMw: Koa.Middleware = async (ctx, next) => {
68 + const now = new Date()
69 + await next()
70 + console.debug(ctx.status, ctx.method, ctx.path)
71 + Promise.race([ once(ctx.res, 'finish'), once(ctx.res, 'close') ]).then(() => {
72 + if (dontLogNet.compiled()(ctx.ip)) return
73 + const isError = ctx.status >= 400
74 + const logger = isError && accessErrorLog || accessLogger
75 + const rotate = logRotation.get()?.[0]
76 + let { stream, last, path } = logger
77 + if (!stream) return
78 + logger.last = now
79 + if (rotate && last) { // rotation enabled and a file exists?
80 + const passed = Number(now) - Number(last)
81 + - 3600_000 // be pessimistic and count a possible DST change
82 + if (rotate === 'm' && (passed >= 31*DAY || now.getMonth() !== last.getMonth())
83 + || rotate === 'd' && (passed >= DAY || now.getDate() !== last.getDate()) // checking passed will solve the case when the day of the month is the same but a month has passed
84 + || rotate === 'w' && (passed >= 7*DAY || now.getDay() < last.getDay())) {
85 + stream.end()
86 + const postfix = last.getFullYear() + '-' + doubleDigit(last.getMonth() + 1) + '-' + doubleDigit(last.getDate())
87 + try { // other logging requests shouldn't happen while we are renaming. Since this is very infrequent we can tolerate solving this by making it sync.
88 + renameSync(path, path + '-' + postfix)
89 + }
90 + catch(e) { // ok, rename failed, but this doesn't mean we ain't gonna log
91 + console.error(e)
92 }
93 + stream = logger.reopen() // keep variable updated
94 + if (!stream) return
95 }
97 - const format = '%s - %s [%s] "%s %s HTTP/%s" %d %s\n' // Apache's Common Log Format
98 - const a = now.toString().split(' ')
99 - const date = a[2]+'/'+a[1]+'/'+a[3]+':'+a[4]+' '+a[5]?.slice(3)
100 - const user = getCurrentUsername(ctx)
101 - const length = ctx.state.length ?? ctx.length
102 - debounce(() => // once in a while we check if the file is still good (not deleted, etc), or we'll reopen it
103 - stat(logger.path).catch(() => logger.reopen())) // async = smoother but we may lose some entries
104 - stream!.write(util.format( format,
105 - ctx.ip,
106 - user || '-',
107 - date,
108 - ctx.method,
109 - ctx.path,
110 - ctx.req.httpVersion,
111 - ctx.status,
112 - length?.toString() ?? '-',
113 - ))
114 - })
115 - }
96 + }
97 + const format = '%s - %s [%s] "%s %s HTTP/%s" %d %s\n' // Apache's Common Log Format
98 + const a = now.toString().split(' ')
99 + const date = a[2]+'/'+a[1]+'/'+a[3]+':'+a[4]+' '+a[5]?.slice(3)
100 + const user = getCurrentUsername(ctx)
101 + const length = ctx.state.length ?? ctx.length
102 + debounce(() => // once in a while we check if the file is still good (not deleted, etc), or we'll reopen it
103 + stat(logger.path).catch(() => logger.reopen())) // async = smoother but we may lose some entries
104 + stream!.write(util.format( format,
105 + ctx.ip,
106 + user || '-',
107 + date,
108 + ctx.method,
109 + ctx.path,
110 + ctx.req.httpVersion,
111 + ctx.status,
112 + length?.toString() ?? '-',
113 + ))
114 + })
115 }
116
117 function doubleDigit(n: number) {
src/plugins.ts
+31 -33
@@ -109,42 +109,40 @@ export function getPluginConfigFields(id: string) {
109 return plugins[id]?.getData().config
110 }
111
112 -export function pluginsMiddleware(): Koa.Middleware {
113 - return async (ctx, next) => {
114 - const after: Dict<CallMeAfter> = {}
115 - // run middleware plugins
116 - for (const [id,pl] of Object.entries(plugins))
117 - try {
118 - const res = await pl.middleware?.(ctx)
119 - if (res === true)
120 - ctx.pluginStopped = true
121 - if (typeof res === 'function')
122 - after[id] = res
123 - }
124 - catch(e){
125 - printError(id, e)
126 - }
127 - // expose public plugins' files
128 - const { path } = ctx
129 - if (!ctx.pluginStopped) {
130 - if (path.startsWith(PLUGINS_PUB_URI)) {
131 - const a = path.substring(PLUGINS_PUB_URI.length).split('/')
132 - const name = a.shift()!
133 - if (plugins.hasOwnProperty(name)) // do it only if the plugin is loaded
134 - await serveFile(ctx, plugins[name]!.folder + '/public/' + a.join('/'), 'auto')
135 - return
136 - }
137 - await next()
112 +export const pluginsMiddleware: Koa.Middleware = async (ctx, next) => {
113 + const after: Dict<CallMeAfter> = {}
114 + // run middleware plugins
115 + for (const [id,pl] of Object.entries(plugins))
116 + try {
117 + const res = await pl.middleware?.(ctx)
118 + if (res === true)
119 + ctx.pluginStopped = true
120 + if (typeof res === 'function')
121 + after[id] = res
122 + }
123 + catch(e){
124 + printError(id, e)
125 }
139 - for (const [id,f] of Object.entries(after))
140 - try { await f() }
141 - catch (e) { printError(id, e) }
126 + // expose public plugins' files
127 + const { path } = ctx
128 + if (!ctx.pluginStopped) {
129 + if (path.startsWith(PLUGINS_PUB_URI)) {
130 + const a = path.substring(PLUGINS_PUB_URI.length).split('/')
131 + const name = a.shift()!
132 + if (plugins.hasOwnProperty(name)) // do it only if the plugin is loaded
133 + await serveFile(ctx, plugins[name]!.folder + '/public/' + a.join('/'), 'auto')
134 + return
135 + }
136 + await next()
137 }
138 + for (const [id,f] of Object.entries(after))
139 + try { await f() }
140 + catch (e) { printError(id, e) }
141 +}
142
144 - function printError(id: string, e: any) {
145 - console.log(`error middleware plugin ${id}: ${e?.message || e}`)
146 - console.debug(e)
147 - }
143 +function printError(id: string, e: any) {
144 + console.log(`error middleware plugin ${id}: ${e?.message || e}`)
145 + console.debug(e)
146 }
147
148 // return false to ask to exclude this entry from results