mirror of
https://github.com/unraid/api.git
synced 2026-01-01 14:10:10 -06:00
feat: remove many unneded simple libraries
This commit is contained in:
922
api/package-lock.json
generated
922
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -70,6 +70,7 @@
|
||||
"bycontract": "^2.0.11",
|
||||
"bytes": "^3.1.2",
|
||||
"cacheable-lookup": "^6.1.0",
|
||||
"camelcase-keys": "^9.1.3",
|
||||
"catch-exit": "^1.2.2",
|
||||
"chokidar": "^3.6.0",
|
||||
"cli-table": "^0.3.11",
|
||||
@@ -79,11 +80,13 @@
|
||||
"docker-event-emitter": "^0.3.0",
|
||||
"dockerode": "^3.3.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"execa": "^9.4.1",
|
||||
"exit-hook": "^4.0.0",
|
||||
"express": "^4.19.2",
|
||||
"filenamify": "^6.0.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"global-agent": "^3.0.0",
|
||||
"got": "^14.4.3",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql-fields": "^2.0.3",
|
||||
"graphql-scalars": "^1.23.0",
|
||||
@@ -107,6 +110,7 @@
|
||||
"p-retry": "^4.6.2",
|
||||
"passport-custom": "^1.1.1",
|
||||
"passport-http-header-strategy": "^1.1.0",
|
||||
"path-type": "^6.0.0",
|
||||
"pidusage": "^3.0.2",
|
||||
"pino": "^9.1.0",
|
||||
"pino-http": "^9.0.0",
|
||||
@@ -160,22 +164,10 @@
|
||||
"@types/wtfnode": "^0.7.3",
|
||||
"@vitest/coverage-v8": "^2.1.1",
|
||||
"@vitest/ui": "^2.1.1",
|
||||
"camelcase-keys": "^8.0.2",
|
||||
"cz-conventional-changelog": "3.3.0",
|
||||
"eslint": "^9.12.0",
|
||||
"execa": "^7.1.1",
|
||||
"filter-obj": "^5.1.0",
|
||||
"got": "^13",
|
||||
"graphql-codegen-typescript-validation-schema": "^0.14.1",
|
||||
"ip-regex": "^5.0.0",
|
||||
"jiti": "^2.3.3",
|
||||
"json-difference": "^1.16.1",
|
||||
"map-obj": "^5.0.2",
|
||||
"p-props": "^5.0.0",
|
||||
"path-exists": "^5.0.0",
|
||||
"path-type": "^5.0.0",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"pretty-ms": "^8.0.0",
|
||||
"standard-version": "^9.5.0",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^8.10.0",
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { parse as parseIni } from 'ini';
|
||||
import camelCaseKeys from 'camelcase-keys';
|
||||
import { includeKeys } from 'filter-obj';
|
||||
import mapObject from 'map-obj';
|
||||
import { AppError } from '@app/core/errors/app-error';
|
||||
import { accessSync, readFileSync } from 'fs';
|
||||
import { access } from 'fs/promises';
|
||||
@@ -11,15 +9,15 @@ import { extname } from 'path';
|
||||
type ConfigType = 'ini' | 'cfg';
|
||||
|
||||
type OptionsWithPath = {
|
||||
/** Relative or absolute file path. */
|
||||
filePath: string;
|
||||
/** If the file is an "ini" or a "cfg". */
|
||||
type?: ConfigType;
|
||||
/** Relative or absolute file path. */
|
||||
filePath: string;
|
||||
/** If the file is an "ini" or a "cfg". */
|
||||
type?: ConfigType;
|
||||
};
|
||||
|
||||
type OptionsWithLoadedFile = {
|
||||
file: string;
|
||||
type: ConfigType;
|
||||
file: string;
|
||||
type: ConfigType;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -38,53 +36,66 @@ type OptionsWithLoadedFile = {
|
||||
* ```
|
||||
*/
|
||||
const fixObjectArrays = (object: Record<string, any>) => {
|
||||
// An object of arrays for keys that end in `:${number}`
|
||||
const temporaryArrays = {};
|
||||
// An object of arrays for keys that end in `:${number}`
|
||||
const temporaryArrays = {};
|
||||
|
||||
// An object without any array items
|
||||
const filteredObject = includeKeys(object, (key, value) => {
|
||||
|
||||
const [, name, index] = [...((key).match(/(.*):(\d+$)/) ?? [])];
|
||||
if (!name || !index) {
|
||||
return true;
|
||||
}
|
||||
// An object without any array items
|
||||
const filteredObject = Object.fromEntries(
|
||||
Object.entries(object).filter(([key, value]) => {
|
||||
const match = key.match(/(.*):(\d+$)/);
|
||||
if (!match) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create initial array
|
||||
if (!Array.isArray(temporaryArrays[name])) {
|
||||
temporaryArrays[name] = [];
|
||||
}
|
||||
const [, name, index] = match;
|
||||
if (!name || !index) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add value
|
||||
temporaryArrays[name].push(value);
|
||||
// Create initial array
|
||||
if (!Array.isArray(temporaryArrays[name])) {
|
||||
temporaryArrays[name] = [];
|
||||
}
|
||||
|
||||
// Remove the old field
|
||||
return false;
|
||||
});
|
||||
// Add value
|
||||
temporaryArrays[name].push(value);
|
||||
|
||||
return {
|
||||
...filteredObject,
|
||||
...temporaryArrays,
|
||||
};
|
||||
// Remove the old field
|
||||
return false;
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...filteredObject,
|
||||
...temporaryArrays,
|
||||
};
|
||||
};
|
||||
|
||||
export const fileExists = async (path: string) => access(path, F_OK).then(() => true).catch(() => false);
|
||||
export const fileExists = async (path: string) =>
|
||||
access(path, F_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
export const fileExistsSync = (path: string) => {
|
||||
try {
|
||||
accessSync(path, F_OK);
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
accessSync(path, F_OK);
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getExtensionFromPath = (filePath: string): string => extname(filePath);
|
||||
|
||||
const isFilePathOptions = (options: OptionsWithLoadedFile | OptionsWithPath): options is OptionsWithPath => Object.keys(options).includes('filePath');
|
||||
const isFileOptions = (options: OptionsWithLoadedFile | OptionsWithPath): options is OptionsWithLoadedFile => Object.keys(options).includes('file');
|
||||
const isFilePathOptions = (
|
||||
options: OptionsWithLoadedFile | OptionsWithPath
|
||||
): options is OptionsWithPath => Object.keys(options).includes('filePath');
|
||||
const isFileOptions = (
|
||||
options: OptionsWithLoadedFile | OptionsWithPath
|
||||
): options is OptionsWithLoadedFile => Object.keys(options).includes('file');
|
||||
|
||||
export const loadFileFromPathSync = (filePath: string): string => {
|
||||
if (!fileExistsSync(filePath)) throw new Error(`Failed to load file at path: ${filePath}`);
|
||||
return readFileSync(filePath, 'utf-8').toString();
|
||||
if (!fileExistsSync(filePath)) throw new Error(`Failed to load file at path: ${filePath}`);
|
||||
return readFileSync(filePath, 'utf-8').toString();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -94,48 +105,51 @@ export const loadFileFromPathSync = (filePath: string): string => {
|
||||
*/
|
||||
const isValidConfigExtension = (extension: string): boolean => ['ini', 'cfg'].includes(extension);
|
||||
|
||||
export const parseConfig = <T extends Record<string, any>>(options: OptionsWithLoadedFile | OptionsWithPath): T => {
|
||||
let fileContents: string;
|
||||
let extension: string;
|
||||
export const parseConfig = <T extends Record<string, any>>(
|
||||
options: OptionsWithLoadedFile | OptionsWithPath
|
||||
): T => {
|
||||
let fileContents: string;
|
||||
let extension: string;
|
||||
|
||||
if (isFilePathOptions(options)) {
|
||||
const { filePath, type } = options;
|
||||
if (isFilePathOptions(options)) {
|
||||
const { filePath, type } = options;
|
||||
|
||||
const validFile = fileExistsSync(filePath);
|
||||
extension = type ?? getExtensionFromPath(filePath);
|
||||
const validExtension = isValidConfigExtension(extension);
|
||||
const validFile = fileExistsSync(filePath);
|
||||
extension = type ?? getExtensionFromPath(filePath);
|
||||
const validExtension = isValidConfigExtension(extension);
|
||||
|
||||
if (validFile && validExtension) {
|
||||
fileContents = loadFileFromPathSync(options.filePath);
|
||||
} else {
|
||||
throw new AppError(`Invalid File Path: ${options.filePath}, or Extension: ${extension}`);
|
||||
}
|
||||
} else if (isFileOptions(options)) {
|
||||
const { file, type } = options;
|
||||
fileContents = file;
|
||||
const extension = type;
|
||||
if (!isValidConfigExtension(extension)) {
|
||||
throw new AppError(`Invalid Extension for Ini File: ${extension}`);
|
||||
}
|
||||
} else {
|
||||
throw new AppError('Invalid Parameters Passed to ParseConfig');
|
||||
}
|
||||
if (validFile && validExtension) {
|
||||
fileContents = loadFileFromPathSync(options.filePath);
|
||||
} else {
|
||||
throw new AppError(`Invalid File Path: ${options.filePath}, or Extension: ${extension}`);
|
||||
}
|
||||
} else if (isFileOptions(options)) {
|
||||
const { file, type } = options;
|
||||
fileContents = file;
|
||||
const extension = type;
|
||||
if (!isValidConfigExtension(extension)) {
|
||||
throw new AppError(`Invalid Extension for Ini File: ${extension}`);
|
||||
}
|
||||
} else {
|
||||
throw new AppError('Invalid Parameters Passed to ParseConfig');
|
||||
}
|
||||
|
||||
const data: Record<string, any> = parseIni(fileContents);
|
||||
// Remove quotes around keys
|
||||
const dataWithoutQuoteKeys = mapObject(data, (key, value) =>
|
||||
// @SEE: https://stackoverflow.com/a/19156197/2311366
|
||||
[(key).replace(/^"(.+(?="$))"$/, '$1'), value],
|
||||
);
|
||||
const data: Record<string, any> = parseIni(fileContents);
|
||||
// Remove quotes around keys
|
||||
const dataWithoutQuoteKeys = Object.fromEntries(
|
||||
Object.entries(data).map(([key, value]) => [key.replace(/^"(.+(?="$))"$/, '$1'), value])
|
||||
);
|
||||
|
||||
// Result object with array items as actual arrays
|
||||
const result = Object.fromEntries(
|
||||
Object.entries(dataWithoutQuoteKeys)
|
||||
.map(([key, value]) => [key, typeof value === 'object' ? fixObjectArrays(value) : value]),
|
||||
);
|
||||
// Result object with array items as actual arrays
|
||||
const result = Object.fromEntries(
|
||||
Object.entries(dataWithoutQuoteKeys).map(([key, value]) => [
|
||||
key,
|
||||
typeof value === 'object' ? fixObjectArrays(value) : value,
|
||||
])
|
||||
);
|
||||
|
||||
// Convert all keys to camel case
|
||||
return camelCaseKeys(result, {
|
||||
deep: true,
|
||||
}) as T;
|
||||
// Convert all keys to camel case
|
||||
return camelCaseKeys(result, {
|
||||
deep: true,
|
||||
}) as T;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pProps from 'p-props';
|
||||
import { type Domain } from '@app/core/types';
|
||||
import { getHypervisor } from '@app/core/utils/vms/get-hypervisor';
|
||||
|
||||
@@ -27,24 +26,34 @@ export const parseDomain = async (type: DomainLookupType, id: string): Promise<D
|
||||
const domain = await client[method](id);
|
||||
const info = await domain.getInfoAsync();
|
||||
|
||||
const results = await pProps({
|
||||
uuid: domain.getUUIDAsync(),
|
||||
osType: domain.getOSTypeAsync(),
|
||||
autostart: domain.getAutostartAsync(),
|
||||
maxMemory: domain.getMaxMemoryAsync(),
|
||||
schedulerType: domain.getSchedulerTypeAsync(),
|
||||
schedulerParameters: domain.getSchedulerParametersAsync(),
|
||||
securityLabel: domain.getSecurityLabelAsync(),
|
||||
name: domain.getNameAsync(),
|
||||
const [uuid, osType, autostart, maxMemory, schedulerType, schedulerParameters, securityLabel, name] = await Promise.all([
|
||||
domain.getUUIDAsync(),
|
||||
domain.getOSTypeAsync(),
|
||||
domain.getAutostartAsync(),
|
||||
domain.getMaxMemoryAsync(),
|
||||
domain.getSchedulerTypeAsync(),
|
||||
domain.getSchedulerParametersAsync(),
|
||||
domain.getSecurityLabelAsync(),
|
||||
domain.getNameAsync(),
|
||||
]);
|
||||
|
||||
const results = {
|
||||
uuid,
|
||||
osType,
|
||||
autostart,
|
||||
maxMemory,
|
||||
schedulerType,
|
||||
schedulerParameters,
|
||||
securityLabel,
|
||||
name,
|
||||
...info,
|
||||
state: info.state.replace(' ', '_'),
|
||||
});
|
||||
};
|
||||
|
||||
if (info.state === 'running') {
|
||||
results.vcpus = await domain.getVcpusAsync();
|
||||
results.memoryStats = await domain.getMemoryStatsAsync();
|
||||
}
|
||||
|
||||
// @ts-expect-error fix pProps inferred type
|
||||
return results;
|
||||
};
|
||||
|
||||
15
api/src/core/utils/write-to-boot.ts
Normal file
15
api/src/core/utils/write-to-boot.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { logger } from '@app/core/log';
|
||||
import convert from 'convert';
|
||||
|
||||
const writeFile = async (filePath: string, fileContents: string | Buffer) => {
|
||||
logger.debug(`Writing ${convert(fileContents.length, 'bytes').to('kilobytes')} to ${filePath}`);
|
||||
await fs.promises.writeFile(filePath, fileContents);
|
||||
};
|
||||
|
||||
export const writeToBoot = async (filePath: string, fileContents: string | Buffer) => {
|
||||
const basePath = '/boot/config/plugins/dynamix/';
|
||||
const resolvedPath = path.resolve(basePath, filePath);
|
||||
await writeFile(resolvedPath, fileContents);
|
||||
};
|
||||
@@ -22,7 +22,6 @@ import { getUnraidVersion } from '@app/common/dashboard/get-unraid-version';
|
||||
import { AppError } from '@app/core/errors/app-error';
|
||||
import { cleanStdout } from '@app/core/utils/misc/clean-stdout';
|
||||
import { execaCommandSync, execa } from 'execa';
|
||||
import { pathExists } from 'path-exists';
|
||||
import { isSymlink } from 'path-type';
|
||||
import type { PciDevice } from '@app/core/types';
|
||||
import { vmRegExps } from '@app/core/utils/vms/domain/vm-regexps';
|
||||
@@ -31,6 +30,7 @@ import { filterDevices } from '@app/core/utils/vms/filter-devices';
|
||||
import { sanitizeVendor } from '@app/core/utils/vms/domain/sanitize-vendor';
|
||||
import { sanitizeProduct } from '@app/core/utils/vms/domain/sanitize-product';
|
||||
import { bootTimestamp } from '@app/common/dashboard/boot-timestamp';
|
||||
import { access } from 'fs/promises';
|
||||
|
||||
export const generateApps = async (): Promise<InfoApps> => {
|
||||
const installed = await docker
|
||||
@@ -201,11 +201,12 @@ export const generateDevices = async (): Promise<Devices> => {
|
||||
|
||||
// Remove devices with no IOMMU support
|
||||
const filteredDevices = await Promise.all(
|
||||
devices.map((device: Readonly<PciDevice>) =>
|
||||
pathExists(`${basePath}${device.id}/iommu_group/`).then((exists) =>
|
||||
exists ? device : null
|
||||
)
|
||||
)
|
||||
devices.map(async (device: Readonly<PciDevice>) => {
|
||||
const exists = await access(`${basePath}${device.id}/iommu_group/`)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
return exists ? device : null;
|
||||
})
|
||||
).then((devices) => devices.filter((device) => device !== null));
|
||||
|
||||
/**
|
||||
@@ -281,7 +282,6 @@ export const generateDevices = async (): Promise<Devices> => {
|
||||
const usbHubs = await execa('cat /sys/bus/usb/drivers/hub/*/modalias', { shell: true })
|
||||
.then(({ stdout }) =>
|
||||
stdout.split('\n').map((line) => {
|
||||
|
||||
const [, id] = line.match(/usb:v(\w{9})/) ?? [];
|
||||
return id.replace('p', ':');
|
||||
})
|
||||
@@ -316,7 +316,7 @@ export const generateDevices = async (): Promise<Devices> => {
|
||||
|
||||
// Parse the line
|
||||
const [, _] = line.split(/[ \t]{2,}/).filter(Boolean);
|
||||
|
||||
|
||||
const match = _.match(/^(\S+)\s(.*)/)?.slice(1);
|
||||
|
||||
// If there's no match return nothing
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { startAppListening } from '@app/store/listeners/listener-middleware';
|
||||
import { getDiff } from 'json-difference';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { logger } from '@app/core/log';
|
||||
import {
|
||||
@@ -40,15 +39,6 @@ export const enableConfigFileListener = (mode: ConfigType) => () =>
|
||||
action.type !== loadConfigFile.fulfilled.type &&
|
||||
action.type !== loadConfigFile.rejected.type
|
||||
) {
|
||||
logger.trace(
|
||||
{
|
||||
diff: getDiff(oldFlashConfig ?? {}, newFlashConfig),
|
||||
},
|
||||
`${mode} Config Changed!`,
|
||||
'Action:',
|
||||
action.type
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user