@samitouri / QOSami-HFS / commits / af01f83a

admin/logs: download button

Massimo Melina committed Feb 8, 2024 at 23:28 UTC af01f83ae770d3207a191eb7b1c40a8bbbf37609
3 files changed +26 -6
admin/src/LogsPage.ts
+7 -2
@@ -11,7 +11,7 @@ import { NetmaskField, Flex, IconBtn, useBreakpoint, usePauseButton, useToggleBu
11 hTooltip } from './mui';
12 import { GridColDef } from '@mui/x-data-grid'
13 import _ from 'lodash'
14 -import { Settings, SmartToy } from '@mui/icons-material'
14 +import { Download, Settings, SmartToy } from '@mui/icons-material'
15 import md from './md'
16 import { ConfigForm } from './ConfigForm'
17 import { BoolField, SelectField } from '@hfs/mui-grid-form'
@@ -28,6 +28,7 @@ export default function LogsPage() {
28 disabled: tab >= 2,
29 }), true)
30 const shorterLabels = !useBreakpoint('sm') && { error_log: "Errors" }
31 + const file = files[tab]
32 return h(Fragment, {},
33 h(Flex, { gap: 0 },
34 h(Tabs, { value: tab, onChange(ev,i){ setTab(i) } },
@@ -41,7 +42,7 @@ export default function LogsPage() {
42 pauseButton,
43 h(IconBtn, { icon: Settings, title: "Options", onClick: showLogOptions })
44 ),
44 - h(LogFile, { key: tab, pause, showApi, file: files[tab] }), // without key, some state is accidentally preserved across files
45 + h(LogFile, { key: tab, pause, showApi, file }), // without key, some state is accidentally preserved across files
46 )
47
48 function showLogOptions() {
@@ -60,6 +61,10 @@ export default function LogsPage() {
61 }>, {
62 keys: [ CFG.log, CFG.error_log, CFG.log_rotation, CFG.dont_log_net, CFG.log_gui, CFG.log_api, CFG.log_ua ],
63 barSx: { gap: 2, width: '100%', ...useDialogBarColors() },
64 + addToBar: [
65 + h(Box, { flex: 1}),
66 + h(Btn, { icon: Download, variant: 'outlined', link: API_URL + `get_log_file?file=${file}` }, "Download file"),
67 + ],
68 form: {
69 stickyBar: true,
70 fields: [
src/api.log.ts
+13 -1
@@ -1,15 +1,27 @@
1 import { ApiHandlers } from './apiMiddleware'
2 import _ from 'lodash'
3 import { consoleLog } from './consoleLog'
4 -import { HTTP_NOT_FOUND, tryJson, wait } from './cross'
4 +import { HTTP_NOT_ACCEPTABLE, HTTP_NOT_FOUND, tryJson, wait } from './cross'
5 import events from './events'
6 import { loggers } from './log'
7 import { createReadStream } from 'fs'
8 import readline from 'readline'
9 import { onOff } from './misc'
10 import { SendListReadable } from './SendList'
11 +import { serveFile } from './serveFile'
12
13 export default {
14 + async get_log_file({ file = 'log' }, ctx) {
15 + const log = _.find(loggers, { name: file })
16 + if (!log)
17 + throw HTTP_NOT_FOUND
18 + if (!log.path)
19 + throw HTTP_NOT_ACCEPTABLE
20 + ctx.attachment(log.path)
21 + await serveFile(ctx, log.path)
22 + return null
23 + },
24 +
25 get_log({ file = 'log' }, ctx) {
26 return new SendListReadable({
27 bufferTime: 10,
src/apiMiddleware.ts
+6 -3
@@ -3,7 +3,7 @@
3 import Koa from 'koa'
4 import createSSE from './sse'
5 import { Readable } from 'stream'
6 -import { asyncGeneratorToReadable, CFG, removeStarting } from './misc'
6 +import { asyncGeneratorToReadable, CFG, Promisable, removeStarting } from './misc'
7 import { HTTP_BAD_REQUEST, HTTP_FOOL, HTTP_NOT_FOUND } from './const'
8 import { defineConfig } from './config'
9
@@ -12,8 +12,8 @@ export class ApiError extends Error {
12 super(typeof message === 'string' ? message : message && message instanceof Error ? message.message : JSON.stringify(message))
13 }
14 }
15 -type ApiHandlerResult = Record<string,any> | ApiError | Readable | AsyncGenerator<any>
16 -export type ApiHandler = (params:any, ctx:Koa.Context) => ApiHandlerResult | Promise<ApiHandlerResult>
15 +type ApiHandlerResult = Record<string,any> | ApiError | Readable | AsyncGenerator<any> | null
16 +export type ApiHandler = (params:any, ctx:Koa.Context) => Promisable<ApiHandlerResult>
17 export type ApiHandlers = Record<string, ApiHandler>
18
19 const logApi = defineConfig(CFG.log_api, true)
@@ -44,12 +44,15 @@ export function apiMiddleware(apis: ApiHandlers) : Koa.Middleware {
44 else if (typeof (v as any)?.[0] === 'string')
45 (v as string[]).forEach((x,i) => fixUri(v,i))
46 res = await apiFun(params, ctx)
47 + if (res === null) return
48
49 function fixUri(obj: any, k: string | number) {
50 obj[k] = removeStarting(ctx.state.revProxyPath, obj[k])
51 }
52 }
53 catch(e) {
54 + if (typeof e === 'number')
55 + e = new ApiError(HTTP_NOT_FOUND)
56 res = e
57 }
58 if (isAsyncGenerator(res))