mirror of
https://github.com/unraid/api.git
synced 2026-01-02 14:40:01 -06:00
Compare commits
4 Commits
refactor/r
...
feat/deno
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e030c0604c | ||
|
|
c847bbaf26 | ||
|
|
7a5ff92c72 | ||
|
|
056f2abf74 |
@@ -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
10
api/deno.json
Normal 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"
|
||||
}
|
||||
@@ -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
716
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
16
api/src/__test__/store/state-parsers/devices.test.ts
Normal file
16
api/src/__test__/store/state-parsers/devices.test.ts
Normal 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('[]');
|
||||
});
|
||||
81
api/src/__test__/store/state-parsers/network.test.ts
Normal file
81
api/src/__test__/store/state-parsers/network.test.ts
Normal 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,
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
203
api/src/__test__/store/state-parsers/nfs.test.ts
Normal file
203
api/src/__test__/store/state-parsers/nfs.test.ts
Normal 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": [],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
306
api/src/__test__/store/state-parsers/smb.test.ts
Normal file
306
api/src/__test__/store/state-parsers/smb.test.ts
Normal 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": [],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
40
api/src/__test__/store/state-parsers/users.test.ts
Normal file
40
api/src/__test__/store/state-parsers/users.test.ts
Normal 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",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
@@ -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
71
api/src/am.ts
Normal 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);
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { format } from 'util';
|
||||
import { format } from 'node:util';
|
||||
import { AppError } from '@app/core/errors/app-error';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
3
api/src/core/utils/files/get-extension-from-path.ts
Normal file
3
api/src/core/utils/files/get-extension-from-path.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { extname } from 'node:path';
|
||||
|
||||
export const getExtensionFromPath = (filePath: string): string => extname(filePath);
|
||||
23
api/src/core/utils/files/load-file-from-path.ts
Normal file
23
api/src/core/utils/files/load-file-from-path.ts
Normal 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}`);
|
||||
};
|
||||
9
api/src/core/utils/misc/attempt-read-file-sync.ts
Normal file
9
api/src/core/utils/misc/attempt-read-file-sync.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
20
api/src/core/utils/misc/get-key-file.ts
Normal file
20
api/src/core/utils/misc/get-key-file.ts
Normal 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, '');
|
||||
};
|
||||
23
api/src/core/utils/misc/get-machine-id.ts
Normal file
23
api/src/core/utils/misc/get-machine-id.ts
Normal 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;
|
||||
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
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 '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);
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]>;
|
||||
|
||||
65
api/src/graphql/resolvers/query/cloud/check-dns.ts
Normal file
65
api/src/graphql/resolvers/query/cloud/check-dns.ts
Normal 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: '' };
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
57
api/src/store/modules/registration.ts
Normal file
57
api/src/store/modules/registration.ts
Normal 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 });
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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
|
||||
|
||||
21
api/src/store/sync/config-disk-sync.ts
Normal file
21
api/src/store/sync/config-disk-sync.ts
Normal 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);
|
||||
};
|
||||
|
||||
@@ -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: {
|
||||
|
||||
28
api/src/store/watch/config-watch.ts
Normal file
28
api/src/store/watch/config-watch.ts
Normal 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)');
|
||||
}
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { execSync } from 'child_process';
|
||||
import 'dotenv/config';
|
||||
import { defineConfig } from 'tsup';
|
||||
import { version } from './package.json';
|
||||
|
||||
Reference in New Issue
Block a user