From f9d656fc0dc6e266da0d5522701e0bf14b308d36 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Mon, 1 May 2023 13:01:32 -0400 Subject: [PATCH] feat: support default image (#630) --- .../utils/images/image-file-helpers.test.ts | 23 ++++ .../core/utils/images/image-file-helpers.ts | 53 +++++++++ api/src/graphql/express/get-images.ts | 30 +++++ api/src/store/modules/dynamix.ts | 104 ++++++++++-------- 4 files changed, 166 insertions(+), 44 deletions(-) create mode 100644 api/src/__test__/core/utils/images/image-file-helpers.test.ts create mode 100644 api/src/core/utils/images/image-file-helpers.ts create mode 100644 api/src/graphql/express/get-images.ts diff --git a/api/src/__test__/core/utils/images/image-file-helpers.test.ts b/api/src/__test__/core/utils/images/image-file-helpers.test.ts new file mode 100644 index 000000000..37994111d --- /dev/null +++ b/api/src/__test__/core/utils/images/image-file-helpers.test.ts @@ -0,0 +1,23 @@ +import { getBannerPathIfPresent, getCasePathIfPresent } from "@app/core/utils/images/image-file-helpers"; +import { store } from "@app/store/index"; +import { loadDynamixConfigFile } from "@app/store/modules/dynamix"; + +import { expect, test } from "vitest"; + +test('get case path returns expected result', () => { + expect(getCasePathIfPresent()).resolves.toContain('/dev/dynamix/case-model.png') +}) + +test('get banner path returns null (state unloaded)', () => { + expect(getBannerPathIfPresent()).resolves.toMatchInlineSnapshot('null') +}) + +test('get banner path returns the banner (state loaded)', async() => { + await store.dispatch(loadDynamixConfigFile()).unwrap(); + expect(getBannerPathIfPresent()).resolves.toContain('/dev/dynamix/banner.png'); +}) + +test('get banner path returns null when no banner (state loaded)', async () => { + await store.dispatch(loadDynamixConfigFile()).unwrap(); + expect(getBannerPathIfPresent('notabanner.png')).resolves.toMatchInlineSnapshot('null'); +}); \ No newline at end of file diff --git a/api/src/core/utils/images/image-file-helpers.ts b/api/src/core/utils/images/image-file-helpers.ts new file mode 100644 index 000000000..84eccce28 --- /dev/null +++ b/api/src/core/utils/images/image-file-helpers.ts @@ -0,0 +1,53 @@ +import { getters } from '@app/store/index'; +import { FileLoadStatus } from '@app/store/types'; +import { readFile, stat } from 'node:fs/promises'; +import { join } from 'node:path'; + +const isImageFile = async (path: string): Promise => { + try { + const stats = await stat(path); + if (stats.size < 25) { + return false; + } + + return true; + } catch (error: unknown) { + return false; + } +}; + +export const getCasePathIfPresent = async (): Promise => { + + const dynamixBasePath = getters.paths()['dynamix-base']; + + const configFilePath = join(dynamixBasePath, 'case-model.cfg'); + const caseImagePath = join(dynamixBasePath, 'case-model.png'); + try { + const caseConfig = await readFile(configFilePath, 'utf-8'); + if (caseConfig.includes('.') && (await isImageFile(caseImagePath))) { + return caseImagePath; + } + + return null; + } catch (error: unknown) { + return null; + } +}; + +export const getBannerPathIfPresent = async ( + filename = 'banner.png' +): Promise => { + if (getters.dynamix().status === FileLoadStatus.LOADED && getters.dynamix().display?.banner) { + const dynamixBasePath = getters.paths()['dynamix-base']; + const customBannerPath = join(dynamixBasePath, filename); + const defaultBannerPath = '/usr/local/emhttp/plugins/dynamix/images/banner.png'; + if (await isImageFile(customBannerPath)) { + return customBannerPath; + } + if (await isImageFile(defaultBannerPath)) { + return defaultBannerPath; + } + } + + return null; +}; diff --git a/api/src/graphql/express/get-images.ts b/api/src/graphql/express/get-images.ts new file mode 100644 index 000000000..a1dc3530d --- /dev/null +++ b/api/src/graphql/express/get-images.ts @@ -0,0 +1,30 @@ +import { getBannerPathIfPresent, getCasePathIfPresent } from "@app/core/utils/images/image-file-helpers"; +import { apiKeyToUser } from "@app/graphql/index"; +import { type Request, type Response } from "express"; +export const getImages = async (req: Request, res: Response) => { + // @TODO - Clean up this function + const apiKey = req.headers['x-api-key']; + if ( + apiKey && + typeof apiKey === 'string' && + (await apiKeyToUser(apiKey)).role !== 'guest' + ) { + if (req.params.type === 'banner') { + const path = await getBannerPathIfPresent(); + if (path) { + res.sendFile(path); + return; + } + } else if (req.params.type === 'case') { + const path = await getCasePathIfPresent(); + if (path) { + res.sendFile(path); + return; + } + } + + return res.status(404).send('no customization of this type found'); + } + + return res.status(403).send('unauthorized'); +}; diff --git a/api/src/store/modules/dynamix.ts b/api/src/store/modules/dynamix.ts index b62469a67..646ef0702 100644 --- a/api/src/store/modules/dynamix.ts +++ b/api/src/store/modules/dynamix.ts @@ -9,11 +9,11 @@ import { toBoolean } from '@app/core/utils/casting'; import { type DynamixConfig } from '@app/core/types/ini'; export type SliceState = { - status: FileLoadStatus; + status: FileLoadStatus; } & DynamixConfig; export const initialState: Partial = { - status: FileLoadStatus.UNLOADED, + status: FileLoadStatus.UNLOADED, }; /** @@ -21,53 +21,69 @@ export const initialState: Partial = { * * Note: If the file doesn't exist this will fallback to default values. */ -export const loadDynamixConfigFile = createAsyncThunk>, string | undefined>('config/load-dynamix-config-file', async filePath => { - const store = await import('@app/store'); - const paths = store.getters.paths(); - const path = filePath ?? paths['dynamix-config']; - const fileExists = await access(path, F_OK).then(() => true).catch(() => false); - const file = fileExists ? parseConfig>({ - filePath: path, - type: 'ini', - }) : {}; - const { display } = file; - return merge(file, { - ...(display?.scale ? { scale: toBoolean(display?.scale) } : {}), - ...(display?.tabs ? { tabs: toBoolean(display?.tabs) } : {}), - ...(display?.resize ? { resize: toBoolean(display?.resize) } : {}), - ...(display?.wwn ? { wwn: toBoolean(display?.wwn) } : {}), - ...(display?.total ? { total: toBoolean(display?.total) } : {}), - ...(display?.usage ? { usage: toBoolean(display?.usage) } : {}), - ...(display?.text ? { text: toBoolean(display?.text) } : {}), - ...(display?.warning ? { warning: Number.parseInt(display?.warning, 10) } : {}), - ...(display?.critical ? { critical: Number.parseInt(display?.critical, 10) } : {}), - ...(display?.hot ? { hot: Number.parseInt(display?.hot, 10) } : {}), - ...(display?.max ? { max: Number.parseInt(display?.max, 10) } : {}), - locale: display?.locale ?? 'en_US', - }) as RecursivePartial; +export const loadDynamixConfigFile = createAsyncThunk< + RecursiveNullable>, + string | undefined +>('config/load-dynamix-config-file', async (filePath) => { + const store = await import('@app/store'); + const paths = store.getters.paths(); + const path = filePath ?? paths['dynamix-config']; + const fileExists = await access(path, F_OK) + .then(() => true) + .catch(() => false); + const file = fileExists + ? parseConfig>({ + filePath: path, + type: 'ini', + }) + : {}; + const { display } = file; + return merge(file, { + ...(display?.scale ? { scale: toBoolean(display?.scale) } : {}), + ...(display?.tabs ? { tabs: toBoolean(display?.tabs) } : {}), + ...(display?.resize ? { resize: toBoolean(display?.resize) } : {}), + ...(display?.wwn ? { wwn: toBoolean(display?.wwn) } : {}), + ...(display?.total ? { total: toBoolean(display?.total) } : {}), + ...(display?.usage ? { usage: toBoolean(display?.usage) } : {}), + ...(display?.text ? { text: toBoolean(display?.text) } : {}), + ...(display?.warning + ? { warning: Number.parseInt(display?.warning, 10) } + : {}), + ...(display?.critical + ? { critical: Number.parseInt(display?.critical, 10) } + : {}), + ...(display?.hot ? { hot: Number.parseInt(display?.hot, 10) } : {}), + ...(display?.max ? { max: Number.parseInt(display?.max, 10) } : {}), + locale: display?.locale ?? 'en_US', + }) as RecursivePartial; }); export const dynamix = createSlice({ - name: 'dynamix', - initialState, - reducers: { - updateDynamixConfig(state, action: PayloadAction>) { - return merge(state, action.payload); - }, - }, - extraReducers(builder) { - builder.addCase(loadDynamixConfigFile.pending, (state, _action) => { - state.status = FileLoadStatus.LOADING; - }); + name: 'dynamix', + initialState, + reducers: { + updateDynamixConfig( + state, + action: PayloadAction> + ) { + return merge(state, action.payload); + }, + }, + extraReducers(builder) { + builder.addCase(loadDynamixConfigFile.pending, (state) => { + state.status = FileLoadStatus.LOADING; + }); - builder.addCase(loadDynamixConfigFile.fulfilled, (state, action) => { - merge(state, action.payload, { status: FileLoadStatus.LOADED }); - }); + builder.addCase(loadDynamixConfigFile.fulfilled, (state, action) => { + merge(state, action.payload, { status: FileLoadStatus.LOADED }); + }); - builder.addCase(loadDynamixConfigFile.rejected, (state, action) => { - merge(state, action.payload, { status: FileLoadStatus.FAILED_LOADING }); - }); - }, + builder.addCase(loadDynamixConfigFile.rejected, (state, action) => { + merge(state, action.payload, { + status: FileLoadStatus.FAILED_LOADING, + }); + }); + }, }); export const { updateDynamixConfig } = dynamix.actions;