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