Compare commits

...

4 Commits

Author SHA1 Message Date
Eli Bosley
e030c0604c feat: more dino changes 2024-10-15 11:45:41 -04:00
Eli Bosley
c847bbaf26 fix: skypack for lodash 2024-10-11 13:29:10 -04:00
Eli Bosley
7a5ff92c72 feat: keep trying deno 2024-10-11 13:03:52 -04:00
Eli Bosley
056f2abf74 feat: begin moving to deno 2024-10-11 12:33:06 -04:00
69 changed files with 1210 additions and 810 deletions

View File

@@ -13,7 +13,11 @@ RUN apt-get update -y && apt-get install -y \
jq \
zstd \
git \
build-essential
build-essential \
zip \
curl
RUN curl -fsSL https://deno.land/install.sh | sh -s -- -y
WORKDIR /app
@@ -28,12 +32,17 @@ COPY tsconfig.json tsup.config.ts .eslintrc.cjs .npmrc .env.production .env.stag
COPY package.json package-lock.json ./
# Install pkg
RUN npm i -g pkg zx
# Install deps
RUN npm i
RUN npm i -g node-gyp
RUN deno install --allow-read --allow-write --allow-env --allow-net --allow-run
# Add zx and pkg to the path
ENV PATH /root/.deno/bin:$PATH
ENV PATH /app/node_modules/.bin:$PATH
EXPOSE 4000
###########################################################

10
api/deno.json Normal file
View File

@@ -0,0 +1,10 @@
{
"imports": {
"lodash": "https://cdn.skypack.dev/lodash",
"xml2js": "npm:xml2js",
"@app/": "./src/",
"@std/collections": "jsr:@std/collections@^1.0.8",
"@vmngr/libvirt": "https://cdn.jsdelivr.net/gh/elibosley/libvirt-deno@HEAD/lib/index.ts"
},
"nodeModulesDir": "auto"
}

View File

@@ -22,6 +22,7 @@ x-volumes: &volumes
- ./.pkg-cache:/app/.pkg-cache
- ./codegen.yml:/app/codegen.yml
- ./fix-array-type.cjs:/app/fix-array-type.cjs
- ./deno.json:/app/deno.json
- /var/run/docker.sock:/var/run/docker.sock
services:

716
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -43,7 +43,7 @@
"install:unraid": "./scripts/install-in-unraid.sh",
"start:plugin": "INTROSPECTION=true LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty LOG_LEVEL=trace unraid-api start --debug",
"start:plugin-verbose": "LOG_CONTEXT=true LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty LOG_LEVEL=trace unraid-api start --debug",
"start:dev": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs start --debug'",
"start:dev": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development deno -A dist/unraid-api.cjs start --debug'",
"restart:dev": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs restart --debug'",
"stop:dev": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development tsup --config ./tsup.config.ts --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs stop --debug'",
"start:report": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development LOG_CONTEXT=true tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs report --debug'",
@@ -60,7 +60,7 @@
"unraid-api"
],
"dependencies": {
"@apollo/client": "^3.10.4",
"@apollo/client": "^3.11.8",
"@apollo/server": "^4.10.4",
"@as-integrations/fastify": "^2.1.1",
"@fastify/cookie": "^9.4.0",
@@ -79,7 +79,6 @@
"@reflet/cron": "^1.3.1",
"@runonflux/nat-upnp": "^1.0.2",
"accesscontrol": "^2.2.1",
"am": "github:unraid/am",
"async-exit-hook": "^2.0.1",
"btoa": "^1.2.1",
"bycontract": "^2.0.11",
@@ -134,13 +133,14 @@
"reflect-metadata": "^0.1.14",
"request": "^2.88.2",
"semver": "^7.6.2",
"stoppable": "^1.1.0",
"systeminformation": "^5.22.9",
"ts-command-line-args": "^2.5.1",
"uuid": "^10.0.0",
"ws": "^8.17.0",
"wtfnode": "^0.9.2",
"xhr2": "^0.2.1",
"xml2js": "^0.6.2",
"zen-observable-ts": "^1.1.0",
"zod": "^3.23.8"
},
"devDependencies": {
@@ -180,7 +180,6 @@
"@types/wtfnode": "^0.7.3",
"@typescript-eslint/eslint-plugin": "^7.9.0",
"@typescript-eslint/parser": "^7.9.0",
"@unraid/eslint-config": "github:unraid/eslint-config",
"@vitest/coverage-v8": "^2.1.1",
"@vitest/ui": "^2.1.1",
"camelcase-keys": "^8.0.2",
@@ -212,12 +211,9 @@
"vitest": "^2.1.1",
"zx": "^7.2.3"
},
"optionalDependencies": {
"@vmngr/libvirt": "github:unraid/libvirt"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
"overrides": {
"dependencies": {
"graphql-tag": "^2.12.6"
}
}
}

View File

@@ -1,7 +1,7 @@
import { test, expect } from 'vitest';
import { parseConfig } from '@app/core/utils/misc/parse-config';
import { Parser as MultiIniParser } from 'multi-ini';
import { readFile, writeFile } from 'fs/promises';
import { readFile, writeFile } from 'node:fs/promises';
import { parse } from 'ini';
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer';

View File

@@ -0,0 +1,16 @@
import { join } from 'node:path';
import { expect, test } from 'vitest';
import { store } from '@app/store';
import type { DevicesIni } from '@app/store/state-parsers/devices';
test('Returns parsed state file', async () => {
const { parse } = await import('@app/store/state-parsers/devices');
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
const { paths } = store.getState();
const filePath = join(paths.states, 'devs.ini');
const stateFile = parseConfig<DevicesIni>({
filePath,
type: 'ini',
});
expect(parse(stateFile)).toMatchInlineSnapshot('[]');
});

View File

@@ -0,0 +1,81 @@
import { join } from 'node:path';
import { expect, test } from 'vitest';
import { store } from '@app/store';
import type { NetworkIni } from '@app/store/state-parsers/network';
test('Returns parsed state file', async () => {
const { parse } = await import('@app/store/state-parsers/network');
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
const { paths } = store.getState();
const filePath = join(paths.states, 'network.ini');
const stateFile = parseConfig<NetworkIni>({
filePath,
type: 'ini',
});
expect(parse(stateFile)).toMatchInlineSnapshot(`
[
{
"bonding": true,
"bondingMiimon": "100",
"bondingMode": "1",
"bondname": "",
"bondnics": [
"eth0",
"eth1",
"eth2",
"eth3",
],
"brfd": "0",
"bridging": true,
"brname": "",
"brnics": "bond0",
"brstp": "0",
"description": [
"",
],
"dhcp6Keepresolv": false,
"dhcpKeepresolv": false,
"dnsServer1": "1.1.1.1",
"dnsServer2": "8.8.8.8",
"gateway": [
"192.168.1.1",
],
"gateway6": [
"",
],
"ipaddr": [
"192.168.1.150",
],
"ipaddr6": [
"",
],
"metric": [
"",
],
"metric6": [
"",
],
"mtu": "",
"netmask": [
"255.255.255.0",
],
"netmask6": [
"",
],
"privacy6": [
"",
],
"protocol": [
"",
],
"type": "access",
"useDhcp": [
true,
],
"useDhcp6": [
false,
],
},
]
`);
});

View File

@@ -0,0 +1,203 @@
import { join } from 'node:path';
import { expect, test } from 'vitest';
import { store } from '@app/store';
import type { NfsSharesIni } from '@app/store/state-parsers/nfs';
test('Returns parsed state file', async () => {
const { parse } = await import('@app/store/state-parsers/nfs');
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
const { paths } = store.getState();
const filePath = join(paths.states, 'sec_nfs.ini');
const stateFile = parseConfig<NfsSharesIni>({
filePath,
type: 'ini',
});
expect(parse(stateFile)).toMatchInlineSnapshot(`
[
{
"enabled": false,
"hostList": "",
"name": "disk1",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk2",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk3",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk4",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk5",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk6",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk7",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk8",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk9",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk10",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk11",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk12",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk13",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk14",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk15",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk16",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk17",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk18",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk19",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk20",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk21",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk22",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "abc",
"readList": [],
"security": "public",
"writeList": [],
},
]
`);
});

View File

@@ -1,4 +1,4 @@
import { join } from 'path';
import { join } from 'node:path';
import { expect, test } from 'vitest';
import { store } from '@app/store';
import type { NginxIni } from '@app/store/state-parsers/nginx';

View File

@@ -1,4 +1,4 @@
import { join } from 'path';
import { join } from 'node:path';
import { expect, test } from 'vitest';
import { store } from '@app/store';
import type { SharesIni } from '@app/store/state-parsers/shares';

View File

@@ -1,4 +1,4 @@
import { join } from 'path';
import { join } from 'node:path';
import { expect, test } from 'vitest';
import { store } from '@app/store';
import type { SlotsIni } from '@app/store/state-parsers/slots';

View File

@@ -0,0 +1,306 @@
import { join } from 'node:path';
import { expect, test } from 'vitest';
import { store } from '@app/store';
import type { SmbIni } from '@app/store/state-parsers/smb';
test('Returns parsed state file', async () => {
const { parse } = await import('@app/store/state-parsers/smb');
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
const { paths } = store.getState();
const filePath = join(paths.states, 'sec.ini');
const stateFile = parseConfig<SmbIni>({
filePath,
type: 'ini',
});
expect(parse(stateFile)).toMatchInlineSnapshot(`
[
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk1",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk2",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk3",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk4",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk5",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk6",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk7",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk8",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk9",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk10",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk11",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk12",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk13",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk14",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk15",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk16",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk17",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk18",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk19",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk20",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk21",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk22",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "abc",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"enabled": true,
"fruit": "no",
"name": "flash",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
]
`);
});

View File

@@ -0,0 +1,40 @@
import { join } from 'node:path';
import { expect, test } from 'vitest';
import { store } from '@app/store';
import type { UsersIni } from '@app/store/state-parsers/users';
test('Returns parsed state file', async () => {
const { parse } = await import('@app/store/state-parsers/users');
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
const { paths } = store.getState();
const filePath = join(paths.states, 'users.ini');
const stateFile = parseConfig<UsersIni>({
filePath,
type: 'ini',
});
expect(parse(stateFile)).toMatchInlineSnapshot(`
[
{
"description": "Console and webGui login account",
"id": "0",
"name": "root",
"password": true,
"role": "admin",
},
{
"description": "",
"id": "1",
"name": "xo",
"password": true,
"role": "user",
},
{
"description": "",
"id": "2",
"name": "test_user",
"password": false,
"role": "user",
},
]
`);
});

View File

@@ -1,4 +1,4 @@
import { join } from 'path';
import { join } from 'node:path';
import { expect, test } from 'vitest';
import { store } from '@app/store';
import type { VarIni } from '@app/store/state-parsers/var';

71
api/src/am.ts Normal file
View File

@@ -0,0 +1,71 @@
const defaultErrorHandler = (error: Error) => {
console.error(error);
};
const getScriptArgs = (process: NodeJS.Process) => {
if (!process || typeof process !== 'object' || !Array.isArray(process.argv)) return [];
const [, , ...params] = process.argv;
return params;
};
const setExitCode = (process: NodeJS.Process, code = 1) => {
if (process && typeof process === 'object' && process.exitCode !== code) {
process.exitCode = code;
}
};
const handleError = async (mainError: Error, errorHandler: (error: Error) => Promise<void> | void) => {
setExitCode(process, 1);
if (errorHandler === defaultErrorHandler) return defaultErrorHandler(mainError);
try {
await errorHandler(mainError);
} catch (errorHandlerFailure) {
console.warn(`The custom error handler failed`, errorHandlerFailure);
defaultErrorHandler(mainError);
}
};
type RegisterUnhandledRejectionHandler = {
(process: NodeJS.Process): void;
done?: boolean;
};
const registerUnhandledRejectionHandler: RegisterUnhandledRejectionHandler = (
process: NodeJS.Process
) => {
if (
!process ||
typeof process !== 'object' ||
typeof process.on !== 'function' ||
registerUnhandledRejectionHandler.done
)
return;
const amUnhandledRejectionHandler: NodeJS.UnhandledRejectionListener = (error, failedPromise) => {
setExitCode(process, 2);
console.warn(`Unhandled Promise Rejection ${error}\n\tat: Promise ${failedPromise}`);
};
process.on('unhandledRejection', amUnhandledRejectionHandler);
registerUnhandledRejectionHandler.done = true;
};
const runMain = async (
asyncMain: (...args: string[]) => Promise<void> | void,
errorHandler: (error: Error) => Promise<void> | void,
process: NodeJS.Process
) => {
registerUnhandledRejectionHandler(process);
try {
return await asyncMain(...getScriptArgs(process));
} catch (mainError: unknown) {
await handleError(mainError as Error, errorHandler);
}
};
export const am = (
asyncMain: (...args: string[]) => Promise<void> | void,
errorHandler = defaultErrorHandler
) => runMain(asyncMain, errorHandler, process);

View File

@@ -1,6 +1,7 @@
import 'wtfnode';
import { am } from 'am';
import { am } from '@app/am';
import { main } from '@app/cli/index';
import { internalLogger } from '@app/core/log';

View File

@@ -17,7 +17,7 @@ import {
type ApolloQueryResult,
type ApolloClient,
type NormalizedCacheObject,
} from '@apollo/client/core/core.cjs';
} from '@apollo/client/core/index.js';
import { MinigraphStatus } from '@app/graphql/generated/api/types';
import { API_VERSION } from '@app/environment';
import { loadStateFiles } from '@app/store/modules/emhttp';

View File

@@ -1,5 +1,5 @@
import { copyFile, readFile, writeFile } from 'fs/promises';
import { join } from 'path';
import { copyFile, readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { cliLogger } from '@app/core/log';
import { getUnraidApiPid } from '@app/cli/get-unraid-api-pid';
import { setEnv } from '@app/cli/set-env';

View File

@@ -1,5 +1,5 @@
import { getters, type RootState, store } from '@app/store';
import { uniq } from 'lodash';
import {
getServerIps,
getUrlForField,
@@ -19,6 +19,8 @@ const getAllowedSocks = (): string[] => [
'/var/run/unraid-cli.sock',
];
const getLocalAccessUrlsForServer = (
state: RootState = store.getState()
): string[] => {
@@ -102,11 +104,12 @@ const getApolloSandbox = (): string[] => {
export const getAllowedOrigins = (
state: RootState = store.getState()
): string[] =>
uniq([
Array.from(new Set([
...getAllowedSocks(),
...getLocalAccessUrlsForServer(),
...getRemoteAccessUrlsForAllowedOrigins(state),
...getExtraOrigins(),
...getConnectOrigins(),
...getApolloSandbox(),
]).map((url) => (url.endsWith('/') ? url.slice(0, -1) : url));
]))
.map((url) => (url.endsWith('/') ? url.slice(0, -1) : url));

View File

@@ -1,4 +1,4 @@
import { format } from 'util';
import { format } from 'node:util';
import { AppError } from '@app/core/errors/app-error';
/**

View File

@@ -1,4 +1,4 @@
import { writeFile } from 'fs/promises';
import { writeFile } from 'node:fs/promises';
import { fileExists } from '@app/core/utils/files/file-exists';
export const setupLogRotation = async () => {
@@ -9,9 +9,10 @@ export const setupLogRotation = async () => {
'/etc/logrotate.d/unraid-api',
`
/var/log/unraid-api/*.log {
rotate 1
rotate 0
missingok
size 5M
copytruncate
}
`,
{ mode: '644' }

View File

@@ -7,7 +7,9 @@ import {
import { store } from '@app/store/index';
import { FileLoadStatus } from '@app/store/types';
import { GraphQLError } from 'graphql';
import sum from 'lodash/sum';
// Utility function to sum an array of numbers
const sum = (numbers: number[]): number => numbers.reduce((acc, num) => acc + num, 0);
export const getArrayData = (getState = store.getState): ArrayType => {
// Var state isn't loaded
@@ -35,9 +37,9 @@ export const getArrayData = (getState = store.getState): ArrayType => {
const disks = allDisks.filter((disk) => disk.type === ArrayDiskType.DATA);
const caches = allDisks.filter((disk) => disk.type === ArrayDiskType.CACHE);
// Disk sizes
const disksTotalKBytes = sum(disks.map((disk) => disk.fsSize));
const disksFreeKBytes = sum(disks.map((disk) => disk.fsFree));
const disksUsedKBytes = sum(disks.map((disk) => disk.fsUsed));
const disksTotalKBytes = sum(disks.map((disk) => disk.fsSize).filter((size): size is number => size !== undefined));
const disksFreeKBytes = sum(disks.map((disk) => disk.fsFree).filter((free): free is number => free !== undefined));
const disksUsedKBytes = sum(disks.map((disk) => disk.fsUsed).filter((used): used is number => used !== undefined));
// Max
const maxDisks = emhttp.var.maxArraysz ?? disks.length;

View File

@@ -1,4 +1,4 @@
import fs from 'fs';
import fs from 'node:fs';
import camelCaseKeys from 'camelcase-keys';
import { catchHandlers } from '@app/core/utils/misc/catch-handlers';
import { getters, store } from '@app/store';

View File

@@ -4,7 +4,6 @@ import {
blockDevices,
diskLayout,
} from 'systeminformation';
import { map as asyncMap } from 'p-iteration';
import {
type Disk,
DiskInterfaceType,
@@ -13,6 +12,13 @@ import {
} from '@app/graphql/generated/api/types';
import { graphqlLogger } from '@app/core/log';
const asyncMap = async <T, U>(
array: T[],
callback: (item: T, index: number, array: T[]) => Promise<U>
): Promise<U[]> => {
return Promise.all(array.map(callback));
};
const getTemperature = async (
disk: Systeminformation.DiskLayoutData
): Promise<number> => {

View File

@@ -1,4 +1,4 @@
import { promises as fs } from 'fs';
import { promises as fs } from 'node:fs';
import { type CoreResult, type CoreContext } from '@app/core/types';
import { FileMissingError } from '@app/core/errors/file-missing-error';
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';

View File

@@ -1,5 +1,5 @@
import { PubSub } from 'graphql-subscriptions';
import EventEmitter from 'events';
import EventEmitter from 'node:events';
// Allow subscriptions to have 30 connections
const eventEmitter = new EventEmitter();

View File

@@ -4,11 +4,11 @@ import {
type SliceState as ConfigSliceState,
initialState,
} from '@app/store/modules/config';
import { type RecursivePartial } from '@app/types';
import type { RecursivePartial } from '@app/types/index.d.ts';
import type {
MyServersConfig,
MyServersConfigMemory,
} from '@app/types/my-servers-config';
} from '@app/types/my-servers-config.d.ts';
import { isEqual } from 'lodash';
export type ConfigType = 'flash' | 'memory';

View File

@@ -1,5 +1,5 @@
import { access } from 'fs/promises';
import { accessSync } from 'fs';
import { access } from 'node:fs/promises';
import { accessSync } from 'node:fs';
import { F_OK } from 'node:constants';
export const fileExists = async (path: string) => access(path, F_OK).then(() => true).catch(() => false);

View File

@@ -0,0 +1,3 @@
import { extname } from 'node:path';
export const getExtensionFromPath = (filePath: string): string => extname(filePath);

View File

@@ -0,0 +1,23 @@
import { readFile } from 'node:fs/promises';
import { fileExists, fileExistsSync } from './file-exists';
import { extname } from 'node:path';
import { readFileSync } from 'node:fs';
export const loadFileFromPath = async (filePath: string): Promise<{ fileContents: string; extension: string }> => {
if (await fileExists(filePath)) {
const fileContents = await readFile(filePath, 'utf-8');
const extension = extname(filePath);
return { fileContents, extension };
}
throw new Error(`Failed to load file at path: ${filePath}`);
};
export const loadFileFromPathSync = (filePath: string): string => {
if (fileExistsSync(filePath)) {
const fileContents = readFileSync(filePath, 'utf-8').toString();
return fileContents;
}
throw new Error(`Failed to load file at path: ${filePath}`);
};

View File

@@ -0,0 +1,9 @@
import { readFileSync } from 'node:fs';
export const attemptReadFileSync = (path: string, fallback: any = undefined) => {
try {
return readFileSync(path, 'utf-8');
} catch {
return fallback;
}
};

View File

@@ -0,0 +1,20 @@
import { type RootState, store } from '@app/store';
import btoa from 'btoa';
import { basename, join } from 'node:path';
import { readFile } from 'node:fs/promises';
// Get key file
export const getKeyFile = async function (appStore: RootState = store.getState()) {
const { emhttp, paths } = appStore;
// If emhttp's state isn't loaded then return null as we can't load the key yet
if (emhttp.var?.regFile === undefined) return null;
// If the key location is empty return an empty string as there is no key
if (emhttp.var?.regFile.trim() === '') return '';
const keyFileName = basename(emhttp.var?.regFile);
const registrationKeyFilePath = join(paths['keyfile-base'], keyFileName);
const keyFile = await readFile(registrationKeyFilePath, 'binary');
return btoa(keyFile).trim().replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
};

View File

@@ -0,0 +1,23 @@
import { FileMissingError } from '@app/core/errors/file-missing-error';
import { getters } from '@app/store';
import { readFile } from 'node:fs/promises';
let machineId: string | null = null
export const getMachineId = async (): Promise<string> => {
const path = getters.paths()['machine-id'];
if (machineId) {
return machineId;
}
if (!path) {
const error = new FileMissingError('/etc/machine-id');
error.fatal = false;
throw error;
}
machineId = await readFile(path, 'utf8').then(machineId => machineId.split('\n')[0].trim()).catch(() => '');
return machineId;
};

View File

@@ -3,10 +3,10 @@ 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';
import { F_OK } from 'constants';
import { extname } from 'path';
import { accessSync, readFileSync } from 'node:fs';
import { access } from 'node:fs/promises';
import { F_OK } from 'node:constants';
import { extname } from 'node:path';
type ConfigType = 'ini' | 'cfg';

View File

@@ -1,4 +1,4 @@
import path from 'path';
import path from 'node:path';
import { execa } from 'execa';
import { FileMissingError } from '@app/core/errors/file-missing-error';
import { type LooseObject, type LooseStringObject } from '@app/core/types';

View File

@@ -1,6 +1,6 @@
import { getters } from '@app/store/index';
import crypto from 'crypto';
import { hostname } from 'os';
import crypto from 'node:crypto';
import { hostname } from 'node:os';
export const getServerIdentifier = (): string => {
const flashGuid = getters.emhttp()?.var?.flashGuid ?? 'FLASH_GUID_NOT_FOUND';
return crypto

View File

@@ -1,7 +1,14 @@
import { execa } from 'execa';
import { map as asyncMap } from 'p-iteration';
import { sync as commandExistsSync } from 'command-exists';
const asyncMap = async <T, U>(
array: T[],
callback: (item: T, index: number, array: T[]) => Promise<U>
): Promise<U[]> => {
return Promise.all(array.map(callback));
};
interface Device {
id: string;
allowed: boolean;

View File

@@ -1,5 +1,5 @@
import { access } from 'fs/promises';
import { constants } from 'fs';
import { access } from 'node:fs/promises';
import { constants } from 'node:fs';
import { Hypervisor } from '@vmngr/libvirt';
import { libvirtLogger } from '@app/core/log';

View File

@@ -0,0 +1,15 @@
import fs from 'node:fs';
import path from 'node:path';
import prettyBytes from 'pretty-bytes';
import { logger } from '@app/core/log';
const writeFile = async (filePath: string, fileContents: string | Buffer) => {
logger.debug(`Writing ${prettyBytes(fileContents.length)} 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);
};

View File

@@ -3,16 +3,16 @@ import {
HttpLink,
InMemoryCache,
split,
} from '@apollo/client/core/core.cjs';
import { onError } from '@apollo/client/link/error';
} from '@apollo/client/core/index.js';
import { onError } from '@apollo/client/link/error/index.js';
import { getInternalApiAddress } from '@app/consts';
import WebSocket from 'ws';
import { fetch } from 'cross-fetch';
import { getMainDefinition } from '@apollo/client/utilities';
import { getMainDefinition } from '@apollo/client/utilities/index.js';
import { graphqlLogger } from '@app/core/log';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getters } from '@app/store/index';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions/index.js';
const getWebsocketWithHeaders = () => {
return class WebsocketWithOriginHeader extends WebSocket {

View File

@@ -3,7 +3,7 @@ import * as Types from '@app/graphql/generated/api/types';
import { z } from 'zod'
import { AccessUrl, AccessUrlInput, AllowedOriginInput, ApiKey, ApiKeyResponse, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskStatus, ArrayDiskType, ArrayPendingState, ArrayState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, Connect, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, Docker, DockerContainer, DockerNetwork, DynamicRemoteAccessStatus, DynamicRemoteAccessType, EnableDynamicRemoteAccessInput, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Node, Notification, NotificationCounts, NotificationData, NotificationFilter, NotificationOverview, NotificationType, Notifications, NotificationslistArgs, Os, Owner, ParityCheck, Partition, Pci, ProfileModel, Registration, RegistrationState, RelayResponse, RemoteAccess, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, URL_TYPE, UnassignedDevice, Uptime, Usb, User, UserAccount, Vars, Versions, VmDomain, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addApiKeyInput, addUserInput, arrayDiskInput, authenticateInput, deleteUserInput, mdState, registrationType, updateApikeyInput, usersInput } from '@app/graphql/generated/api/types'
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core/typings/index.d.ts';
type Properties<T> = Required<{
[K in keyof T]: z.ZodType<T[K], any, T[K]>;

View File

@@ -0,0 +1,65 @@
/*!
* Copyright 2022 Lime Technology Inc. All rights reserved.
* Written by: Alexis Tyler
*/
import { MOTHERSHIP_GRAPHQL_LINK } from '@app/consts';
import { store } from '@app/store';
import { getDnsCache } from '@app/store/getters';
import { setDNSCheck } from '@app/store/modules/cache';
import { lookup as lookupDNS, resolve as resolveDNS } from 'dns';
import { isPrivate as isPrivateIP } from 'ip';
import { promisify } from 'node:util';
const msHostname = new URL(MOTHERSHIP_GRAPHQL_LINK).host;
/**
* Check if the local and network resolvers are able to see mothership
*
* See: https://nodejs.org/docs/latest/api/dns.html#dns_implementation_considerations
*/
export const checkDNS = async (hostname = msHostname): Promise<{ cloudIp: string }> => {
const dnsCachedResuslt = getDnsCache();
if (dnsCachedResuslt) {
if (dnsCachedResuslt.cloudIp) {
return { cloudIp: dnsCachedResuslt.cloudIp };
}
if (dnsCachedResuslt.error) {
throw dnsCachedResuslt.error;
}
}
let local: string | null = null;
let network: string | null = null;
try {
// Check the local resolver like "ping" does
// Check the DNS server the server has set - does a DNS query on the network
const [localRes, networkRes] = await Promise.all([
promisify(lookupDNS)(hostname).then(({ address }) => address),
promisify(resolveDNS)(hostname).then(([address]) => address),
]);
local = localRes;
network = networkRes;
// The user's server and the DNS server they're using are returning different results
if (!local.includes(network)) throw new Error(`Local and network resolvers showing different IP for "${hostname}". [local="${local ?? 'NOT FOUND'}"] [network="${network ?? 'NOT FOUND'}"]`);
// The user likely has a PI-hole or something similar running.
if (isPrivateIP(local)) throw new Error(`"${hostname}" is being resolved to a private IP. [IP=${local ?? 'NOT FOUND'}]`);
} catch (error: unknown) {
if (!(error instanceof Error)) {
throw error;
}
store.dispatch(setDNSCheck({ cloudIp: null, error }));
}
if (typeof local === 'string' || typeof network === 'string') {
const validIp: string = local ?? network ?? '';
store.dispatch(setDNSCheck({ cloudIp: validIp, error: null }));
return { cloudIp: validIp };
}
return { cloudIp: '' };
};

View File

@@ -0,0 +1,28 @@
import type { QueryOptions } from "@apollo/client/core";
import { gql } from "graphql-tag";
;
interface ParsedQuery {
query?: string;
variables?: Record<string, string>;
}
export const parseGraphQLQuery = (body: string): QueryOptions => {
try {
const parsedBody: ParsedQuery = JSON.parse(body);
if (
parsedBody.query &&
parsedBody.variables &&
typeof parsedBody.variables === 'object'
) {
return {
query: gql(parsedBody.query),
variables: parsedBody.variables,
};
}
throw new Error('Invalid Body');
} catch (error) {
throw new Error('Invalid Body Provided');
}
};

View File

@@ -1,4 +1,4 @@
import { join } from 'path';
import { join } from 'node:path';
import { loadFilesSync } from '@graphql-tools/load-files';
import { mergeTypeDefs } from '@graphql-tools/merge';

View File

@@ -1,7 +1,7 @@
import 'reflect-metadata';
import 'global-agent/bootstrap';
import { am } from 'am';
import { am } from '@app/am';
import http from 'http';
import https from 'https';
import CacheableLookup from 'cacheable-lookup';
@@ -14,7 +14,7 @@ import { loadStateFiles } from '@app/store/modules/emhttp';
import { StateManager } from '@app/store/watch/state-watch';
import { setupRegistrationKeyWatch } from '@app/store/watch/registration-watch';
import { loadRegistrationKey } from '@app/store/modules/registration';
import { unlinkSync } from 'fs';
import { unlinkSync } from 'node:fs';
import { fileExistsSync } from '@app/core/utils/files/file-exists';
import { PORT, environment } from '@app/environment';
import { shutdownApiEvent } from '@app/store/actions/shutdown-api-event';

View File

@@ -11,8 +11,8 @@ import {
ApolloClient,
InMemoryCache,
type NormalizedCacheObject,
} from '@apollo/client/core/core.cjs';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
} from '@apollo/client/core/index.js';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions/index.js';
import { MinigraphStatus } from '@app/graphql/generated/api/types';
import { API_VERSION } from '@app/environment';
import {
@@ -20,8 +20,8 @@ import {
setMothershipTimeout,
} from '@app/store/modules/minigraph';
import { logoutUser } from '@app/store/modules/config';
import { RetryLink } from '@apollo/client/link/retry';
import { ErrorLink } from '@apollo/client/link/error';
import { RetryLink } from '@apollo/client/link/retry/index.js';
import { ErrorLink } from '@apollo/client/link/error/index.js';
import { isApiKeyValid } from '@app/store/getters/index';
import { buildDelayFunction } from '@app/mothership/utils/delay-function';
import { WebSocket } from 'ws';

View File

@@ -1,4 +1,4 @@
import { type DelayFunctionOptions } from '@apollo/client/link/retry/delayFunction';
import { type DelayFunctionOptions } from '@apollo/client/link/retry/delayFunction.js';
export function buildDelayFunction(
delayOptions?: DelayFunctionOptions,

View File

@@ -2,9 +2,9 @@ import { parseConfig } from '@app/core/utils/misc/parse-config';
import {
createAsyncThunk,
} from '@reduxjs/toolkit';
import { access } from 'fs/promises';
import { F_OK } from 'constants';
import { type RecursivePartial, type RecursiveNullable } from '@app/types';
import { access } from 'node:fs/promises';
import { F_OK } from 'node:constants';
import type { RecursivePartial, RecursiveNullable } from '@app/types/index.d.ts';
import { type DynamixConfig } from '@app/core/types/ini';
/**

View File

@@ -5,7 +5,7 @@ import {
WAN_FORWARD_TYPE,
} from '@app/graphql/generated/api/types';
import { type AppDispatch, type RootState } from '@app/store/index';
import { type MyServersConfig } from '@app/types/my-servers-config';
import type { MyServersConfig } from '@app/types/my-servers-config.d.ts';
import { createAsyncThunk } from '@reduxjs/toolkit';
const getDynamicRemoteAccessType = (

View File

@@ -15,7 +15,7 @@ import { FileLoadStatus } from '@app/store/types';
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer';
import { isFulfilled } from '@reduxjs/toolkit';
import { environment } from '@app/environment';
import { writeFileSync } from 'fs';
import { writeFileSync } from 'node:fs';
const actionIsLoginOrLogout = isFulfilled(logoutUser, loginUser);

View File

@@ -1,34 +1,48 @@
import { startAppListening } from '@app/store/listeners/listener-middleware';
import { subscribeToEvents } from '@app/mothership/subscribe-to-mothership';
import { getMothershipConnectionParams } from '@app/mothership/utils/get-mothership-websocket-headers';
import isEqual from 'lodash/isEqual';
import { GraphQLClient } from '@app/mothership/graphql-client';
import { MinigraphStatus } from '@app/graphql/generated/api/types';
import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-status';
import { minigraphLogger } from '@app/core/log';
import { isEqual } from 'lodash';
export const enableMothershipJobsListener = () => startAppListening({
predicate(action, currentState, previousState) {
// This event happens on first app load, or if a user signs out and signs back in, etc
if (!isEqual(getMothershipConnectionParams(currentState), getMothershipConnectionParams(previousState)) && getMothershipConnectionParams(currentState)?.apiKey) {
minigraphLogger.info('Connecting / Reconnecting Mothership Due to Changed Config File or First Load')
return true;
}
export const enableMothershipJobsListener = () =>
startAppListening({
predicate(action, currentState, previousState) {
// This event happens on first app load, or if a user signs out and signs back in, etc
if (
!isEqual(
getMothershipConnectionParams(currentState),
getMothershipConnectionParams(previousState)
) &&
getMothershipConnectionParams(currentState)?.apiKey
) {
minigraphLogger.info(
'Connecting / Reconnecting Mothership Due to Changed Config File or First Load'
);
return true;
}
if (setGraphqlConnectionStatus.match(action) && [MinigraphStatus.PING_FAILURE, MinigraphStatus.PRE_INIT].includes(action.payload.status)) {
minigraphLogger.info('Reconnecting Mothership - PING_FAILURE / PRE_INIT - SetGraphQLConnectionStatus Event')
return true;
}
if (
setGraphqlConnectionStatus.match(action) &&
[MinigraphStatus.PING_FAILURE, MinigraphStatus.PRE_INIT].includes(action.payload.status)
) {
minigraphLogger.info(
'Reconnecting Mothership - PING_FAILURE / PRE_INIT - SetGraphQLConnectionStatus Event'
);
return true;
}
return false;
}, async effect(_, { getState }) {
await GraphQLClient.clearInstance();
if (getMothershipConnectionParams(getState())?.apiKey) {
const client = GraphQLClient.createSingletonInstance();
if (client) {
await subscribeToEvents(getState().config.remote.apikey);
}
}
},
});
return false;
},
async effect(_, { getState }) {
await GraphQLClient.clearInstance();
if (getMothershipConnectionParams(getState())?.apiKey) {
const client = GraphQLClient.createSingletonInstance();
if (client) {
await subscribeToEvents(getState().config.remote.apikey);
}
}
},
});

View File

@@ -4,8 +4,7 @@ import { getServers } from '@app/graphql/schema/utils';
import { isAPIStateDataFullyLoaded } from '@app/mothership/graphql-client';
import { startAppListening } from '@app/store/listeners/listener-middleware';
import { FileLoadStatus } from '@app/store/types';
import isEqual from 'lodash/isEqual';
import { isEqual } from 'lodash';
export const enableServerStateListener = () =>
startAppListening({

View File

@@ -1,28 +1,27 @@
import { parseConfig } from '@app/core/utils/misc/parse-config';
import {
type MyServersConfig,
type MyServersConfigMemory,
} from '@app/types/my-servers-config';
import type {
MyServersConfig,
MyServersConfigMemory,
} from '@app/types/my-servers-config.d.ts';
import {
createAsyncThunk,
createSlice,
type PayloadAction,
} from '@reduxjs/toolkit';
import { access } from 'fs/promises';
import merge from 'lodash/merge';
import { access } from 'node:fs/promises';
import { FileLoadStatus } from '@app/store/types';
import { F_OK } from 'constants';
import { type RecursivePartial } from '@app/types';
import { F_OK } from 'node:constants';
import type { RecursivePartial } from '@app/types/index.d.ts';
import { DynamicRemoteAccessType, MinigraphStatus, type Owner } from '@app/graphql/generated/api/types';
import { type RootState } from '@app/store';
import { randomBytes } from 'crypto';
import { randomBytes } from 'node:crypto';
import { logger } from '@app/core/log';
import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-status';
import { getWriteableConfig } from '@app/core/utils/files/config-file-normalizer';
import { writeFileSync } from 'fs';
import { writeFileSync } from 'node:fs';
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer';
import { PUBSUB_CHANNEL, pubsub } from '@app/core/pubsub';
import { isEqual } from 'lodash';
import { isEqual, merge } from 'lodash';
import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access';
export type SliceState = {

View File

@@ -2,11 +2,11 @@ import {
createSlice,
type PayloadAction,
} from '@reduxjs/toolkit';
import merge from 'lodash/merge';
import { FileLoadStatus } from '@app/store/types';
import { type RecursivePartial } from '@app/types';
import type { RecursivePartial } from '@app/types/index.d.ts';
import { type DynamixConfig } from '@app/core/types/ini';
import { loadDynamixConfigFile } from '@app/store/actions/load-dynamix-config-file';
import { merge } from 'lodash';
export type SliceState = {
status: FileLoadStatus;

View File

@@ -1,7 +1,7 @@
import { FileLoadStatus, StateFileKey, type StateFileToIniParserMap } from '@app/store/types';
import { createAsyncThunk, createSlice, type PayloadAction } from '@reduxjs/toolkit';
import merge from 'lodash/merge';
import { join } from 'path';
import { merge } from 'lodash';
import { join } from 'node:path';
import { emhttpLogger } from '@app/core/log';
import { parseConfig } from '@app/core/utils/misc/parse-config';
import { type Devices } from '@app/core/types/states/devices';

View File

@@ -1,5 +1,5 @@
import { createSlice } from '@reduxjs/toolkit';
import { join, resolve as resolvePath } from 'path';
import { join, resolve as resolvePath } from 'node:path';
const initialState = {
core: __dirname,

View File

@@ -0,0 +1,57 @@
import { logger } from '@app/core/log';
import { getKeyFile } from '@app/core/utils/misc/get-key-file';
import { FileLoadStatus } from '@app/store/types';
import { createAsyncThunk, createSlice, type PayloadAction } from '@reduxjs/toolkit';
import { format } from 'node:util';
import type { RootState } from '@app/store';
import { merge } from 'lodash';
export type SliceState = {
status: FileLoadStatus;
keyFile: string | null;
};
const initialState = {
status: FileLoadStatus.UNLOADED,
keyFile: null,
};
export const loadRegistrationKey = createAsyncThunk<{ keyFile: string | null }, void, { state: RootState }>('registration/load-registration-key', async (_, { getState }) => {
try {
logger.trace('Loading registration key file');
return {
keyFile: await getKeyFile(getState()),
};
} catch (error: unknown) {
if (!(error instanceof Error)) throw new Error(format('Failed loading registration key with unknown error "%s"', String(error)));
logger.error('Failed loading registration key with "%s"', error.message);
}
return {
keyFile: null,
};
});
export const registration = createSlice({
name: 'registration',
initialState,
reducers: {
updateRegistrationState(state, action: PayloadAction<Partial<{ keyFile: string }>>) {
return merge(state, action.payload);
},
},
extraReducers(builder) {
builder.addCase(loadRegistrationKey.pending, (state) => {
state.status = FileLoadStatus.LOADING;
});
builder.addCase(loadRegistrationKey.fulfilled, (state, action) => {
merge(state, action.payload, { status: FileLoadStatus.LOADED });
});
builder.addCase(loadRegistrationKey.rejected, (state, action) => {
merge(state, action.payload, { status: FileLoadStatus.FAILED_LOADING });
});
},
});

View File

@@ -5,9 +5,9 @@ import { syncRegistration } from '@app/store/sync/registration-sync';
import { syncInfoApps } from '@app/store/sync/info-apps-sync';
import { setupConfigPathWatch } from '@app/store/watch/config-watch';
import { NODE_ENV } from '@app/environment';
import { writeFileSync } from 'fs';
import { writeFileSync } from 'node:fs';
import { isEqual } from 'lodash';
import { join } from 'path';
import { join } from 'node:path';
export const startStoreSync = async () => {
// The last state is stored so we don't end up in a loop of writing -> reading -> writing

View File

@@ -0,0 +1,21 @@
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer';
import { FileLoadStatus } from '@app/store/types';
import { type ConfigType, getWriteableConfig } from '@app/core/utils/files/config-file-normalizer';
import { store } from '@app/store';
import { writeFileSync } from 'node:fs';
import { logger } from '@app/core/log';
export const writeConfigSync = (mode: ConfigType) => {
const { config, paths } = store.getState();
if (config.status !== FileLoadStatus.LOADED) {
logger.warn('Configs not loaded, unable to write sync');
return;
}
const writeableConfig = getWriteableConfig(config, mode);
const path = mode === 'flash' ? paths['myservers-config'] : paths['myservers-config-states'];
const serializedConfig = safelySerializeObjectToIni(writeableConfig);
writeFileSync(path, serializedConfig);
};

View File

@@ -2,7 +2,7 @@ import { logger } from '@app/core/log';
import { PUBSUB_CHANNEL, pubsub } from '@app/core/pubsub';
import { store } from '@app/store';
import { FileLoadStatus, type StoreSubscriptionHandler } from '@app/store/types';
import isEqual from 'lodash/isEqual';
import { isEqual } from 'lodash';
export type RegistrationEvent = {
registration: {

View File

@@ -0,0 +1,28 @@
import { getters, store } from '@app/store';
import { watch } from 'chokidar';
import { loadConfigFile, logoutUser } from '@app/store/modules/config';
import { logger } from '@app/core/log';
import { existsSync, writeFileSync } from 'node:fs';
export const setupConfigPathWatch = () => {
const myServersConfigPath = getters.paths()?.['myservers-config'];
if (myServersConfigPath) {
logger.info('Watch Setup on Config Path: %s', myServersConfigPath);
if (!existsSync(myServersConfigPath)) {
writeFileSync(myServersConfigPath, '', 'utf-8');
}
const watcher = watch(myServersConfigPath, {
persistent: true,
ignoreInitial: false,
usePolling: process.env.NODE_ENV === 'development',
}).on('change', async () => {
await store.dispatch(loadConfigFile());
}).on('unlink', async () => {
watcher.close();
setupConfigPathWatch();
store.dispatch(logoutUser({ reason: 'Config File was Deleted'}))
});
} else {
logger.error('[FATAL] Failed to setup watch on My Servers Config (Could Not Read Config Path)');
}
};

View File

@@ -3,7 +3,7 @@ import { emhttpLogger } from '@app/core/log';
import { watch, type FSWatcher, type WatchOptions } from 'chokidar';
import { getters, store } from '@app/store';
import { StateFileKey } from '@app/store/types';
import { parse, join } from 'path';
import { parse, join } from 'node:path';
import { loadSingleStateFile } from '@app/store/modules/emhttp';
import { CHOKIDAR_USEPOLLING } from '@app/environment';

View File

@@ -2,7 +2,7 @@ import { fileExists } from '@app/core/utils/files/file-exists';
import { getters } from '@app/store';
import { batchProcess } from '@app/utils';
import { Injectable, Inject } from '@nestjs/common';
import { join } from 'path';
import { join } from 'node:path';
/** token for dependency injection of a session cookie options object */
export const SESSION_COOKIE_CONFIG = 'SESSION_COOKIE_CONFIG';

View File

@@ -4,7 +4,7 @@ import { convertToFuzzyTime } from '@app/mothership/utils/convert-to-fuzzy-time'
import { getters } from '@app/store/index';
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { readFile, writeFile } from 'fs/promises';
import { readFile, writeFile } from 'node:fs/promises';
@Injectable()
export class WriteFlashFileService {

View File

@@ -1,6 +1,6 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { WriteFlashFileService } from './write-flash-file.service';
import { readFileSync, writeFileSync } from 'fs';
import { readFileSync, writeFileSync } from 'node:fs';
import { getters } from '@app/store/index';
describe('WriteFlashFileService', () => {

View File

@@ -1,7 +1,7 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { NotificationsService } from './notifications.service';
import { describe, it, expect, vi, beforeAll, afterEach } from 'vitest';
import { existsSync } from 'fs';
import { existsSync } from 'node:fs';
import {
Importance,
type NotificationData,
@@ -12,7 +12,7 @@ import {
type NotificationFilter,
} from '@app/graphql/generated/api/types';
import { NotificationSchema } from '@app/graphql/generated/api/operations';
import { mkdir } from 'fs/promises';
import { mkdir } from 'node:fs/promises';
import { type NotificationIni } from '@app/core/types/states/notification';
import { execa } from 'execa';

View File

@@ -12,8 +12,8 @@ import {
} from '@app/graphql/generated/api/types';
import { getters } from '@app/store';
import { Injectable } from '@nestjs/common';
import { readdir, rename, unlink, writeFile } from 'fs/promises';
import { basename, join } from 'path';
import { readdir, rename, unlink, writeFile } from 'node:fs/promises';
import { basename, join } from 'node:path';
import { Logger } from '@nestjs/common';
import { batchProcess, isFulfilled, isRejected, unraidTimestamp } from '@app/utils';
import { FSWatcher, watch } from 'chokidar';

View File

@@ -1,4 +1,3 @@
import { execSync } from 'child_process';
import 'dotenv/config';
import { defineConfig } from 'tsup';
import { version } from './package.json';