mirror of
https://github.com/unraid/api.git
synced 2026-01-03 06:59:50 -06:00
feat: support default image (#630)
This commit is contained in:
@@ -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');
|
||||
});
|
||||
53
api/src/core/utils/images/image-file-helpers.ts
Normal file
53
api/src/core/utils/images/image-file-helpers.ts
Normal file
@@ -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<boolean> => {
|
||||
try {
|
||||
const stats = await stat(path);
|
||||
if (stats.size < 25) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getCasePathIfPresent = async (): Promise<string | null> => {
|
||||
|
||||
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<string | null> => {
|
||||
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;
|
||||
};
|
||||
30
api/src/graphql/express/get-images.ts
Normal file
30
api/src/graphql/express/get-images.ts
Normal file
@@ -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');
|
||||
};
|
||||
@@ -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<SliceState> = {
|
||||
status: FileLoadStatus.UNLOADED,
|
||||
status: FileLoadStatus.UNLOADED,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -21,53 +21,69 @@ export const initialState: Partial<SliceState> = {
|
||||
*
|
||||
* Note: If the file doesn't exist this will fallback to default values.
|
||||
*/
|
||||
export const loadDynamixConfigFile = createAsyncThunk<RecursiveNullable<RecursivePartial<DynamixConfig>>, 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<RecursivePartial<DynamixConfig>>({
|
||||
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<DynamixConfig>;
|
||||
export const loadDynamixConfigFile = createAsyncThunk<
|
||||
RecursiveNullable<RecursivePartial<DynamixConfig>>,
|
||||
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<RecursivePartial<DynamixConfig>>({
|
||||
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<DynamixConfig>;
|
||||
});
|
||||
|
||||
export const dynamix = createSlice({
|
||||
name: 'dynamix',
|
||||
initialState,
|
||||
reducers: {
|
||||
updateDynamixConfig(state, action: PayloadAction<RecursivePartial<SliceState>>) {
|
||||
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<RecursivePartial<SliceState>>
|
||||
) {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user