mirror of
https://github.com/unraid/api.git
synced 2026-01-01 14:10:10 -06:00
chore: lint api codebase
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
// Preloading imports for faster tests
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { test, expect, vi } from 'vitest';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
vi.mock('@app/core/pubsub', () => ({
|
||||
pubsub: { publish: vi.fn() },
|
||||
}));
|
||||
|
||||
test('Creates an array event', async () => {
|
||||
const { getArrayData } = await import(
|
||||
'@app/core/modules/array/get-array-data'
|
||||
);
|
||||
const { getArrayData } = await import('@app/core/modules/array/get-array-data');
|
||||
const { store } = await import('@app/store');
|
||||
const { loadStateFiles } = await import('@app/store/modules/emhttp');
|
||||
const { loadConfigFile } = await import('@app/store/modules/config');
|
||||
@@ -17,196 +15,194 @@ test('Creates an array event', async () => {
|
||||
await store.dispatch(loadConfigFile());
|
||||
|
||||
const arrayEvent = getArrayData(store.getState);
|
||||
expect(arrayEvent).toMatchObject(
|
||||
{
|
||||
"boot": {
|
||||
"comment": "Unraid OS boot device",
|
||||
"critical": null,
|
||||
"device": "sda",
|
||||
"exportable": true,
|
||||
"format": "unknown",
|
||||
"fsFree": 3191407,
|
||||
"fsSize": 4042732,
|
||||
"fsType": "vfat",
|
||||
"fsUsed": 851325,
|
||||
"id": "Cruzer",
|
||||
"idx": 32,
|
||||
"name": "flash",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 3956700,
|
||||
"status": "DISK_OK",
|
||||
"temp": null,
|
||||
"transport": "usb",
|
||||
"type": "Flash",
|
||||
"warning": null,
|
||||
expect(arrayEvent).toMatchObject({
|
||||
boot: {
|
||||
comment: 'Unraid OS boot device',
|
||||
critical: null,
|
||||
device: 'sda',
|
||||
exportable: true,
|
||||
format: 'unknown',
|
||||
fsFree: 3191407,
|
||||
fsSize: 4042732,
|
||||
fsType: 'vfat',
|
||||
fsUsed: 851325,
|
||||
id: 'Cruzer',
|
||||
idx: 32,
|
||||
name: 'flash',
|
||||
numErrors: 0,
|
||||
numReads: 0,
|
||||
numWrites: 0,
|
||||
rotational: true,
|
||||
size: 3956700,
|
||||
status: 'DISK_OK',
|
||||
temp: null,
|
||||
transport: 'usb',
|
||||
type: 'Flash',
|
||||
warning: null,
|
||||
},
|
||||
"caches": [
|
||||
{
|
||||
"comment": "",
|
||||
"critical": null,
|
||||
"device": "sdi",
|
||||
"exportable": false,
|
||||
"format": "MBR: 4KiB-aligned",
|
||||
"fsFree": 111810683,
|
||||
"fsSize": 250059317,
|
||||
"fsType": "btrfs",
|
||||
"fsUsed": 137273827,
|
||||
"id": "Samsung_SSD_850_EVO_250GB_S2R5NX0H643734Z",
|
||||
"idx": 30,
|
||||
"name": "cache",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": false,
|
||||
"size": 244198552,
|
||||
"status": "DISK_OK",
|
||||
"temp": 22,
|
||||
"transport": "ata",
|
||||
"type": "Cache",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": null,
|
||||
"critical": null,
|
||||
"device": "nvme0n1",
|
||||
"exportable": false,
|
||||
"format": "MBR: 4KiB-aligned",
|
||||
"fsFree": null,
|
||||
"fsSize": null,
|
||||
"fsType": null,
|
||||
"fsUsed": null,
|
||||
"id": "KINGSTON_SA2000M8250G_50026B7282669D9E",
|
||||
"idx": 31,
|
||||
"name": "cache2",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": false,
|
||||
"size": 244198552,
|
||||
"status": "DISK_OK",
|
||||
"temp": 27,
|
||||
"transport": "nvme",
|
||||
"type": "Cache",
|
||||
"warning": null,
|
||||
},
|
||||
caches: [
|
||||
{
|
||||
comment: '',
|
||||
critical: null,
|
||||
device: 'sdi',
|
||||
exportable: false,
|
||||
format: 'MBR: 4KiB-aligned',
|
||||
fsFree: 111810683,
|
||||
fsSize: 250059317,
|
||||
fsType: 'btrfs',
|
||||
fsUsed: 137273827,
|
||||
id: 'Samsung_SSD_850_EVO_250GB_S2R5NX0H643734Z',
|
||||
idx: 30,
|
||||
name: 'cache',
|
||||
numErrors: 0,
|
||||
numReads: 0,
|
||||
numWrites: 0,
|
||||
rotational: false,
|
||||
size: 244198552,
|
||||
status: 'DISK_OK',
|
||||
temp: 22,
|
||||
transport: 'ata',
|
||||
type: 'Cache',
|
||||
warning: null,
|
||||
},
|
||||
{
|
||||
comment: null,
|
||||
critical: null,
|
||||
device: 'nvme0n1',
|
||||
exportable: false,
|
||||
format: 'MBR: 4KiB-aligned',
|
||||
fsFree: null,
|
||||
fsSize: null,
|
||||
fsType: null,
|
||||
fsUsed: null,
|
||||
id: 'KINGSTON_SA2000M8250G_50026B7282669D9E',
|
||||
idx: 31,
|
||||
name: 'cache2',
|
||||
numErrors: 0,
|
||||
numReads: 0,
|
||||
numWrites: 0,
|
||||
rotational: false,
|
||||
size: 244198552,
|
||||
status: 'DISK_OK',
|
||||
temp: 27,
|
||||
transport: 'nvme',
|
||||
type: 'Cache',
|
||||
warning: null,
|
||||
},
|
||||
],
|
||||
"capacity": {
|
||||
"disks": {
|
||||
"free": "27",
|
||||
"total": "30",
|
||||
"used": "3",
|
||||
},
|
||||
"kilobytes": {
|
||||
"free": "19495825571",
|
||||
"total": "41994745901",
|
||||
"used": "22498920330",
|
||||
},
|
||||
capacity: {
|
||||
disks: {
|
||||
free: '27',
|
||||
total: '30',
|
||||
used: '3',
|
||||
},
|
||||
kilobytes: {
|
||||
free: '19495825571',
|
||||
total: '41994745901',
|
||||
used: '22498920330',
|
||||
},
|
||||
},
|
||||
"disks": [
|
||||
{
|
||||
"comment": "Seagate Exos",
|
||||
"critical": 75,
|
||||
"device": "sdf",
|
||||
"exportable": false,
|
||||
"format": "GPT: 4KiB-aligned",
|
||||
"fsFree": 13882739732,
|
||||
"fsSize": 17998742753,
|
||||
"fsType": "xfs",
|
||||
"fsUsed": 4116003021,
|
||||
"id": "ST18000NM000J-2TV103_ZR5B1W9X",
|
||||
"idx": 1,
|
||||
"name": "disk1",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 17578328012,
|
||||
"status": "DISK_OK",
|
||||
"temp": 30,
|
||||
"transport": "ata",
|
||||
"type": "Data",
|
||||
"warning": 50,
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"critical": null,
|
||||
"device": "sdj",
|
||||
"exportable": false,
|
||||
"format": "GPT: 4KiB-aligned",
|
||||
"fsFree": 93140746,
|
||||
"fsSize": 11998001574,
|
||||
"fsType": "xfs",
|
||||
"fsUsed": 11904860828,
|
||||
"id": "WDC_WD120EDAZ-11F3RA0_5PJRD45C",
|
||||
"idx": 2,
|
||||
"name": "disk2",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 11718885324,
|
||||
"status": "DISK_OK",
|
||||
"temp": 30,
|
||||
"transport": "ata",
|
||||
"type": "Data",
|
||||
"warning": null,
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"critical": null,
|
||||
"device": "sde",
|
||||
"exportable": false,
|
||||
"format": "GPT: 4KiB-aligned",
|
||||
"fsFree": 5519945093,
|
||||
"fsSize": 11998001574,
|
||||
"fsType": "xfs",
|
||||
"fsUsed": 6478056481,
|
||||
"id": "WDC_WD120EMAZ-11BLFA0_5PH8BTYD",
|
||||
"idx": 3,
|
||||
"name": "disk3",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 11718885324,
|
||||
"status": "DISK_OK",
|
||||
"temp": 30,
|
||||
"transport": "ata",
|
||||
"type": "Data",
|
||||
"warning": null,
|
||||
},
|
||||
disks: [
|
||||
{
|
||||
comment: 'Seagate Exos',
|
||||
critical: 75,
|
||||
device: 'sdf',
|
||||
exportable: false,
|
||||
format: 'GPT: 4KiB-aligned',
|
||||
fsFree: 13882739732,
|
||||
fsSize: 17998742753,
|
||||
fsType: 'xfs',
|
||||
fsUsed: 4116003021,
|
||||
id: 'ST18000NM000J-2TV103_ZR5B1W9X',
|
||||
idx: 1,
|
||||
name: 'disk1',
|
||||
numErrors: 0,
|
||||
numReads: 0,
|
||||
numWrites: 0,
|
||||
rotational: true,
|
||||
size: 17578328012,
|
||||
status: 'DISK_OK',
|
||||
temp: 30,
|
||||
transport: 'ata',
|
||||
type: 'Data',
|
||||
warning: 50,
|
||||
},
|
||||
{
|
||||
comment: '',
|
||||
critical: null,
|
||||
device: 'sdj',
|
||||
exportable: false,
|
||||
format: 'GPT: 4KiB-aligned',
|
||||
fsFree: 93140746,
|
||||
fsSize: 11998001574,
|
||||
fsType: 'xfs',
|
||||
fsUsed: 11904860828,
|
||||
id: 'WDC_WD120EDAZ-11F3RA0_5PJRD45C',
|
||||
idx: 2,
|
||||
name: 'disk2',
|
||||
numErrors: 0,
|
||||
numReads: 0,
|
||||
numWrites: 0,
|
||||
rotational: true,
|
||||
size: 11718885324,
|
||||
status: 'DISK_OK',
|
||||
temp: 30,
|
||||
transport: 'ata',
|
||||
type: 'Data',
|
||||
warning: null,
|
||||
},
|
||||
{
|
||||
comment: '',
|
||||
critical: null,
|
||||
device: 'sde',
|
||||
exportable: false,
|
||||
format: 'GPT: 4KiB-aligned',
|
||||
fsFree: 5519945093,
|
||||
fsSize: 11998001574,
|
||||
fsType: 'xfs',
|
||||
fsUsed: 6478056481,
|
||||
id: 'WDC_WD120EMAZ-11BLFA0_5PH8BTYD',
|
||||
idx: 3,
|
||||
name: 'disk3',
|
||||
numErrors: 0,
|
||||
numReads: 0,
|
||||
numWrites: 0,
|
||||
rotational: true,
|
||||
size: 11718885324,
|
||||
status: 'DISK_OK',
|
||||
temp: 30,
|
||||
transport: 'ata',
|
||||
type: 'Data',
|
||||
warning: null,
|
||||
},
|
||||
],
|
||||
"id": expect.any(String),
|
||||
"parities": [
|
||||
{
|
||||
"comment": null,
|
||||
"critical": null,
|
||||
"device": "sdh",
|
||||
"exportable": false,
|
||||
"format": "GPT: 4KiB-aligned",
|
||||
"fsFree": null,
|
||||
"fsSize": null,
|
||||
"fsType": null,
|
||||
"fsUsed": null,
|
||||
"id": "ST18000NM000J-2TV103_ZR585CPY",
|
||||
"idx": 0,
|
||||
"name": "parity",
|
||||
"numErrors": 0,
|
||||
"numReads": 0,
|
||||
"numWrites": 0,
|
||||
"rotational": true,
|
||||
"size": 17578328012,
|
||||
"status": "DISK_OK",
|
||||
"temp": 25,
|
||||
"transport": "ata",
|
||||
"type": "Parity",
|
||||
"warning": null,
|
||||
},
|
||||
id: expect.any(String),
|
||||
parities: [
|
||||
{
|
||||
comment: null,
|
||||
critical: null,
|
||||
device: 'sdh',
|
||||
exportable: false,
|
||||
format: 'GPT: 4KiB-aligned',
|
||||
fsFree: null,
|
||||
fsSize: null,
|
||||
fsType: null,
|
||||
fsUsed: null,
|
||||
id: 'ST18000NM000J-2TV103_ZR585CPY',
|
||||
idx: 0,
|
||||
name: 'parity',
|
||||
numErrors: 0,
|
||||
numReads: 0,
|
||||
numWrites: 0,
|
||||
rotational: true,
|
||||
size: 17578328012,
|
||||
status: 'DISK_OK',
|
||||
temp: 25,
|
||||
transport: 'ata',
|
||||
type: 'Parity',
|
||||
warning: null,
|
||||
},
|
||||
],
|
||||
"state": "STOPPED",
|
||||
}
|
||||
);
|
||||
state: 'STOPPED',
|
||||
});
|
||||
});
|
||||
|
||||
22
api/src/__test__/core/notifiers/console.test.ts
Normal file
22
api/src/__test__/core/notifiers/console.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import { ConsoleNotifier } from '@app/core/notifiers/console';
|
||||
|
||||
vi.mock('@app/core/log', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
graphqlLogger: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
test('Creates a console notifier', () => {
|
||||
const notifier = new ConsoleNotifier();
|
||||
expect(notifier.level).toBe('info');
|
||||
expect(notifier.template).toBe('{{{ data }}}');
|
||||
});
|
||||
24
api/src/__test__/core/notifiers/unraid-local.test.ts
Normal file
24
api/src/__test__/core/notifiers/unraid-local.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import { UnraidLocalNotifier } from '@app/core/notifiers/unraid-local';
|
||||
|
||||
vi.mock('@app/core/log', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
graphqlLogger: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
test('Creates an email notifier', () => {
|
||||
const notifier = new UnraidLocalNotifier({ level: 'info' });
|
||||
expect(notifier.level).toBe('normal');
|
||||
expect(notifier.template).toBe('{{ message }}');
|
||||
const rendered = notifier.render({ message: 'Remote access started' });
|
||||
expect(rendered).toEqual('Remote access started');
|
||||
});
|
||||
23
api/src/__test__/core/utils/array/array-is-running.test.ts
Normal file
23
api/src/__test__/core/utils/array/array-is-running.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import type { SliceState } from '@app/store/modules/emhttp';
|
||||
import { getters } from '@app/store';
|
||||
|
||||
test('Returns true if the array is started', async () => {
|
||||
vi.spyOn(getters, 'emhttp').mockImplementation(
|
||||
() => ({ var: { mdState: 'STARTED' } }) as unknown as SliceState
|
||||
);
|
||||
|
||||
const { arrayIsRunning } = await import('@app/core/utils/array/array-is-running');
|
||||
expect(arrayIsRunning()).toBe(true);
|
||||
vi.spyOn(getters, 'emhttp').mockReset();
|
||||
});
|
||||
|
||||
test('Returns false if the array is stopped', async () => {
|
||||
vi.spyOn(getters, 'emhttp').mockImplementation(
|
||||
() => ({ var: { mdState: 'Stopped' } }) as unknown as SliceState
|
||||
);
|
||||
const { arrayIsRunning } = await import('@app/core/utils/array/array-is-running');
|
||||
expect(arrayIsRunning()).toBe(false);
|
||||
vi.spyOn(getters, 'emhttp').mockReset();
|
||||
});
|
||||
@@ -77,7 +77,7 @@ test('it creates a MEMORY config with NO OPTIONAL values', () => {
|
||||
|
||||
test('it creates a FLASH config with OPTIONAL values', () => {
|
||||
const basicConfig = cloneDeep(initialState);
|
||||
// 2fa & t2fa should be ignored
|
||||
// 2fa & t2fa should be ignored
|
||||
basicConfig.remote['2Fa'] = 'yes';
|
||||
basicConfig.local['2Fa'] = 'yes';
|
||||
basicConfig.local.showT2Fa = 'yes';
|
||||
@@ -117,7 +117,7 @@ test('it creates a FLASH config with OPTIONAL values', () => {
|
||||
|
||||
test('it creates a MEMORY config with OPTIONAL values', () => {
|
||||
const basicConfig = cloneDeep(initialState);
|
||||
// 2fa & t2fa should be ignored
|
||||
// 2fa & t2fa should be ignored
|
||||
basicConfig.remote['2Fa'] = 'yes';
|
||||
basicConfig.local['2Fa'] = 'yes';
|
||||
basicConfig.local.showT2Fa = 'yes';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { test, expect } from 'vitest';
|
||||
import { parse } from 'ini';
|
||||
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer';
|
||||
import { Serializer } from 'multi-ini';
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer';
|
||||
|
||||
test('MultiIni breaks when serializing an object with a boolean inside', async () => {
|
||||
const objectToSerialize = {
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { getBannerPathIfPresent, getCasePathIfPresent } from "@app/core/utils/images/image-file-helpers";
|
||||
import { loadDynamixConfigFile } from "@app/store/actions/load-dynamix-config-file";
|
||||
import { store } from "@app/store/index";
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import { expect, test } from "vitest";
|
||||
import { getBannerPathIfPresent, getCasePathIfPresent } from '@app/core/utils/images/image-file-helpers';
|
||||
import { loadDynamixConfigFile } from '@app/store/actions/load-dynamix-config-file';
|
||||
import { store } from '@app/store/index';
|
||||
|
||||
test('get case path returns expected result', async () => {
|
||||
await expect(getCasePathIfPresent()).resolves.toContain('/dev/dynamix/case-model.png')
|
||||
})
|
||||
await expect(getCasePathIfPresent()).resolves.toContain('/dev/dynamix/case-model.png');
|
||||
});
|
||||
|
||||
test('get banner path returns null (state unloaded)', async () => {
|
||||
await expect(getBannerPathIfPresent()).resolves.toMatchInlineSnapshot('null')
|
||||
})
|
||||
await expect(getBannerPathIfPresent()).resolves.toMatchInlineSnapshot('null');
|
||||
});
|
||||
|
||||
test('get banner path returns the banner (state loaded)', async() => {
|
||||
await store.dispatch(loadDynamixConfigFile()).unwrap();
|
||||
test('get banner path returns the banner (state loaded)', async () => {
|
||||
await store.dispatch(loadDynamixConfigFile()).unwrap();
|
||||
await expect(getBannerPathIfPresent()).resolves.toContain('/dev/dynamix/banner.png');
|
||||
})
|
||||
});
|
||||
|
||||
test('get banner path returns null when no banner (state loaded)', async () => {
|
||||
await store.dispatch(loadDynamixConfigFile()).unwrap();
|
||||
await expect(getBannerPathIfPresent('notabanner.png')).resolves.toMatchInlineSnapshot('null');
|
||||
});
|
||||
});
|
||||
|
||||
8
api/src/__test__/core/utils/misc/clean-stdout.test.ts
Normal file
8
api/src/__test__/core/utils/misc/clean-stdout.test.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import { cleanStdout } from '@app/core/utils/misc/clean-stdout';
|
||||
|
||||
test('Returns trimmed stdout from execa command', () => {
|
||||
expect(cleanStdout({ stdout: 'test' })).toBe('test');
|
||||
expect(cleanStdout({ stdout: 'test ' })).toBe('test');
|
||||
});
|
||||
64
api/src/__test__/core/utils/misc/get-key-file.test.ts
Normal file
64
api/src/__test__/core/utils/misc/get-key-file.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import { store } from '@app/store';
|
||||
import { FileLoadStatus, StateFileKey } from '@app/store/types';
|
||||
|
||||
import '@app/core/utils/misc/get-key-file';
|
||||
import '@app/store/modules/emhttp';
|
||||
|
||||
test('Before loading key returns null', async () => {
|
||||
const { getKeyFile } = await import('@app/core/utils/misc/get-key-file');
|
||||
const { status } = store.getState().registration;
|
||||
|
||||
expect(status).toBe(FileLoadStatus.UNLOADED);
|
||||
await expect(getKeyFile()).resolves.toBe(null);
|
||||
});
|
||||
|
||||
test('Requires emhttp to be loaded to find key file', async () => {
|
||||
const { getKeyFile } = await import('@app/core/utils/misc/get-key-file');
|
||||
const { loadRegistrationKey } = await import('@app/store/modules/registration');
|
||||
|
||||
// Load registration key into store
|
||||
await store.dispatch(loadRegistrationKey());
|
||||
|
||||
// Check if store has state files loaded
|
||||
const { status } = store.getState().registration;
|
||||
expect(status).toBe(FileLoadStatus.LOADED);
|
||||
await expect(getKeyFile()).resolves.toBe(null);
|
||||
});
|
||||
|
||||
test('Returns empty key if key location is empty', async () => {
|
||||
const { getKeyFile } = await import('@app/core/utils/misc/get-key-file');
|
||||
const { updateEmhttpState } = await import('@app/store/modules/emhttp');
|
||||
|
||||
// Set key file location as empty
|
||||
// This should only happen if the user doesn't have a key file
|
||||
store.dispatch(
|
||||
updateEmhttpState({
|
||||
field: StateFileKey.var,
|
||||
state: {
|
||||
regFile: '',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Check if store has state files loaded
|
||||
const { status } = store.getState().registration;
|
||||
expect(status).toBe(FileLoadStatus.LOADED);
|
||||
await expect(getKeyFile()).resolves.toBe('');
|
||||
});
|
||||
|
||||
test('Returns decoded key file if key location exists', async () => {
|
||||
const { getKeyFile } = await import('@app/core/utils/misc/get-key-file');
|
||||
const { loadStateFiles } = await import('@app/store/modules/emhttp');
|
||||
|
||||
// Load state files into store
|
||||
await store.dispatch(loadStateFiles());
|
||||
|
||||
// Check if store has state files loaded
|
||||
const { status } = store.getState().registration;
|
||||
expect(status).toBe(FileLoadStatus.LOADED);
|
||||
await expect(getKeyFile()).resolves.toMatchInlineSnapshot(
|
||||
'"hVs1tLjvC9FiiQsIwIQ7G1KszAcexf0IneThhnmf22SB0dGs5WzRkqMiSMmt2DtR5HOXFUD32YyxuzGeUXmky3zKpSu6xhZNKVg5atGM1OfvkzHBMldI3SeBLuUFSgejLbpNUMdTrbk64JJdbzle4O8wiQgkIpAMIGxeYLwLBD4zHBcfyzq40QnxG--HcX6j25eE0xqa2zWj-j0b0rCAXahJV2a3ySCbPzr1MvfPRTVb0rr7KJ-25R592hYrz4H7Sc1B3p0lr6QUxHE6o7bcYrWKDRtIVoZ8SMPpd1_0gzYIcl5GsDFzFumTXUh8NEnl0Q8hwW1YE-tRc6Y_rrvd7w"'
|
||||
);
|
||||
});
|
||||
@@ -1,9 +1,11 @@
|
||||
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 { parse } from 'ini';
|
||||
import { Parser as MultiIniParser } from 'multi-ini';
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer';
|
||||
import { parseConfig } from '@app/core/utils/misc/parse-config';
|
||||
|
||||
const iniTestData = `["root"]
|
||||
idx="0"
|
||||
@@ -22,11 +24,11 @@ desc=""
|
||||
passwd="no"`;
|
||||
|
||||
test('it loads a config from a passed in ini file successfully', () => {
|
||||
const res = parseConfig<any>({
|
||||
file: iniTestData,
|
||||
type: 'ini',
|
||||
});
|
||||
expect(res).toMatchInlineSnapshot(`
|
||||
const res = parseConfig<any>({
|
||||
file: iniTestData,
|
||||
type: 'ini',
|
||||
});
|
||||
expect(res).toMatchInlineSnapshot(`
|
||||
{
|
||||
"root": {
|
||||
"desc": "Console and webGui login account",
|
||||
@@ -48,26 +50,26 @@ test('it loads a config from a passed in ini file successfully', () => {
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(res?.root.desc).toEqual('Console and webGui login account');
|
||||
expect(res?.root.desc).toEqual('Console and webGui login account');
|
||||
});
|
||||
|
||||
test('it loads a config from disk properly', () => {
|
||||
const path = './dev/states/var.ini';
|
||||
const res = parseConfig<any>({ filePath: path, type: 'ini' });
|
||||
expect(res.DOMAIN_SHORT).toEqual(undefined);
|
||||
expect(res.domainShort).toEqual('');
|
||||
expect(res.shareCount).toEqual('0');
|
||||
const path = './dev/states/var.ini';
|
||||
const res = parseConfig<any>({ filePath: path, type: 'ini' });
|
||||
expect(res.DOMAIN_SHORT).toEqual(undefined);
|
||||
expect(res.domainShort).toEqual('');
|
||||
expect(res.shareCount).toEqual('0');
|
||||
});
|
||||
|
||||
test('Confirm Multi-Ini Parser Still Broken', () => {
|
||||
const parser = new MultiIniParser();
|
||||
const res = parser.parse(iniTestData);
|
||||
expect(res).toMatchInlineSnapshot('{}');
|
||||
const parser = new MultiIniParser();
|
||||
const res = parser.parse(iniTestData);
|
||||
expect(res).toMatchInlineSnapshot('{}');
|
||||
});
|
||||
|
||||
test('Combine Ini and Multi-Ini to read and then write a file with quotes', async () => {
|
||||
const parsedFile = parse(iniTestData);
|
||||
expect(parsedFile).toMatchInlineSnapshot(`
|
||||
const parsedFile = parse(iniTestData);
|
||||
expect(parsedFile).toMatchInlineSnapshot(`
|
||||
{
|
||||
"root": {
|
||||
"desc": "Console and webGui login account",
|
||||
@@ -90,10 +92,10 @@ test('Combine Ini and Multi-Ini to read and then write a file with quotes', asyn
|
||||
}
|
||||
`);
|
||||
|
||||
const ini = safelySerializeObjectToIni(parsedFile);
|
||||
await writeFile('/tmp/test.ini', ini);
|
||||
const file = await readFile('/tmp/test.ini', 'utf-8');
|
||||
expect(file).toMatchInlineSnapshot(`
|
||||
const ini = safelySerializeObjectToIni(parsedFile);
|
||||
await writeFile('/tmp/test.ini', ini);
|
||||
const file = await readFile('/tmp/test.ini', 'utf-8');
|
||||
expect(file).toMatchInlineSnapshot(`
|
||||
"[root]
|
||||
idx="0"
|
||||
name="root"
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import { getShares } from '@app/core/utils/shares/get-shares';
|
||||
import { store } from '@app/store';
|
||||
import { loadStateFiles } from '@app/store/modules/emhttp';
|
||||
|
||||
test('Returns both disk and user shares', async () => {
|
||||
await store.dispatch(loadStateFiles());
|
||||
await store.dispatch(loadStateFiles());
|
||||
|
||||
expect(getShares()).toMatchInlineSnapshot(`
|
||||
expect(getShares()).toMatchInlineSnapshot(`
|
||||
{
|
||||
"disks": [],
|
||||
"users": [
|
||||
@@ -96,8 +97,8 @@ test('Returns both disk and user shares', async () => {
|
||||
});
|
||||
|
||||
test('Returns shares by type', async () => {
|
||||
await store.dispatch(loadStateFiles());
|
||||
expect(getShares('user')).toMatchInlineSnapshot(`
|
||||
await store.dispatch(loadStateFiles());
|
||||
expect(getShares('user')).toMatchInlineSnapshot(`
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
@@ -119,7 +120,7 @@ test('Returns shares by type', async () => {
|
||||
"used": 33619300,
|
||||
}
|
||||
`);
|
||||
expect(getShares('users')).toMatchInlineSnapshot(`
|
||||
expect(getShares('users')).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"allocator": "highwater",
|
||||
@@ -203,12 +204,12 @@ test('Returns shares by type', async () => {
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(getShares('disk')).toMatchInlineSnapshot('null');
|
||||
expect(getShares('disks')).toMatchInlineSnapshot('[]');
|
||||
expect(getShares('disk')).toMatchInlineSnapshot('null');
|
||||
expect(getShares('disks')).toMatchInlineSnapshot('[]');
|
||||
});
|
||||
|
||||
test('Returns shares by name', async () => {
|
||||
expect(getShares('user', { name: 'domains' })).toMatchInlineSnapshot(`
|
||||
expect(getShares('user', { name: 'domains' })).toMatchInlineSnapshot(`
|
||||
{
|
||||
"allocator": "highwater",
|
||||
"cachePool": "cache",
|
||||
@@ -230,8 +231,8 @@ test('Returns shares by name', async () => {
|
||||
"used": 33619300,
|
||||
}
|
||||
`);
|
||||
expect(getShares('user', { name: 'non-existent-user-share' })).toMatchInlineSnapshot('null');
|
||||
// @TODO: disk shares need to be added to the dev ini files
|
||||
expect(getShares('disk', { name: 'disk1' })).toMatchInlineSnapshot('null');
|
||||
expect(getShares('disk', { name: 'non-existent-disk-share' })).toMatchInlineSnapshot('null');
|
||||
expect(getShares('user', { name: 'non-existent-user-share' })).toMatchInlineSnapshot('null');
|
||||
// @TODO: disk shares need to be added to the dev ini files
|
||||
expect(getShares('disk', { name: 'disk1' })).toMatchInlineSnapshot('null');
|
||||
expect(getShares('disk', { name: 'non-existent-disk-share' })).toMatchInlineSnapshot('null');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { afterEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import { checkDNS } from '@app/graphql/resolvers/query/cloud/check-dns';
|
||||
import { store } from '@app/store';
|
||||
import { clearKey } from '@app/store/modules/cache';
|
||||
import { CacheKeys } from '@app/store/types';
|
||||
|
||||
afterEach(() => {
|
||||
store.dispatch(clearKey(CacheKeys.checkDns));
|
||||
});
|
||||
|
||||
test('it resolves dns successfully', async () => {
|
||||
// @TODO
|
||||
const dns = await checkDNS('example.com');
|
||||
expect(dns.cloudIp).not.toBeNull();
|
||||
}, 25_000);
|
||||
|
||||
test('testing twice results in a cache hit', async () => {
|
||||
// Hit mothership
|
||||
const getters = await import('@app/store/getters');
|
||||
const dnsSpy = vi.spyOn(getters, 'getDnsCache');
|
||||
const dns = await checkDNS();
|
||||
expect(dns.cloudIp).toBeTypeOf('string');
|
||||
expect(dnsSpy.mock.results[0]).toMatchInlineSnapshot(`
|
||||
{
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
}
|
||||
`);
|
||||
const dnslookup2 = await checkDNS();
|
||||
expect(dnslookup2.cloudIp).toEqual(dns.cloudIp);
|
||||
expect(dnsSpy.mock.results[1].value.cloudIp).toEqual(dns.cloudIp);
|
||||
expect(store.getState().cache.nodeCache.getTtl(CacheKeys.checkDns)).toBeGreaterThan(500);
|
||||
});
|
||||
@@ -1,10 +1,16 @@
|
||||
import 'reflect-metadata';
|
||||
import { checkMothershipAuthentication } from "@app/graphql/resolvers/query/cloud/check-mothership-authentication";
|
||||
import { expect, test } from "vitest";
|
||||
import packageJson from '@app/../package.json'
|
||||
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import packageJson from '@app/../package.json';
|
||||
import { checkMothershipAuthentication } from '@app/graphql/resolvers/query/cloud/check-mothership-authentication';
|
||||
|
||||
test('It fails to authenticate with mothership with no credentials', async () => {
|
||||
await expect(checkMothershipAuthentication('BAD', 'BAD')).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Failed to connect to https://mothership.unraid.net/ws with a "426" HTTP error.]`);
|
||||
await expect(checkMothershipAuthentication('BAD', 'BAD')).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`[Error: Failed to connect to https://mothership.unraid.net/ws with a "426" HTTP error.]`
|
||||
);
|
||||
expect(packageJson.version).not.toBeNull();
|
||||
await expect(checkMothershipAuthentication(packageJson.version, 'BAD_API_KEY')).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Invalid credentials]`);
|
||||
}, 15_000)
|
||||
await expect(
|
||||
checkMothershipAuthentication(packageJson.version, 'BAD_API_KEY')
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Invalid credentials]`);
|
||||
}, 15_000);
|
||||
|
||||
@@ -1,124 +1,197 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { type Nginx } from '../../../../core/types/states/nginx';
|
||||
import { getUrlForField, getUrlForServer, getServerIps, type NginxUrlFields } from '@app/graphql/resolvers/subscription/network';
|
||||
|
||||
import type { NginxUrlFields } from '@app/graphql/resolvers/subscription/network';
|
||||
import { type Nginx } from '@app/core/types/states/nginx';
|
||||
import {
|
||||
getServerIps,
|
||||
getUrlForField,
|
||||
getUrlForServer,
|
||||
} from '@app/graphql/resolvers/subscription/network';
|
||||
import { store } from '@app/store';
|
||||
import { loadStateFiles } from '@app/store/modules/emhttp';
|
||||
import { loadConfigFile } from '@app/store/modules/config';
|
||||
import { loadStateFiles } from '@app/store/modules/emhttp';
|
||||
|
||||
test.each([
|
||||
[{ httpPort: 80, httpsPort: 443, url: 'my-default-url.com' }],
|
||||
[{ httpPort: 123, httpsPort: 443, url: 'my-default-url.com' }],
|
||||
[{ httpPort: 80, httpsPort: 12_345, url: 'my-default-url.com' }],
|
||||
[{ httpPort: 212, httpsPort: 3_233, url: 'my-default-url.com' }],
|
||||
[{ httpPort: 80, httpsPort: 443, url: 'https://BROKEN_URL' }],
|
||||
|
||||
[{ httpPort: 80, httpsPort: 443, url: 'my-default-url.com' }],
|
||||
[{ httpPort: 123, httpsPort: 443, url: 'my-default-url.com' }],
|
||||
[{ httpPort: 80, httpsPort: 12_345, url: 'my-default-url.com' }],
|
||||
[{ httpPort: 212, httpsPort: 3_233, url: 'my-default-url.com' }],
|
||||
[{ httpPort: 80, httpsPort: 443, url: 'https://BROKEN_URL' }],
|
||||
])('getUrlForField', ({ httpPort, httpsPort, url }) => {
|
||||
const responseInsecure = getUrlForField({
|
||||
port: httpPort,
|
||||
url,
|
||||
});
|
||||
const responseInsecure = getUrlForField({
|
||||
port: httpPort,
|
||||
url,
|
||||
});
|
||||
|
||||
const responseSecure = getUrlForField({
|
||||
portSsl: httpsPort,
|
||||
url,
|
||||
});
|
||||
if (httpPort === 80) {
|
||||
expect(responseInsecure.port).toBe('');
|
||||
} else {
|
||||
expect(responseInsecure.port).toBe(httpPort.toString());
|
||||
}
|
||||
const responseSecure = getUrlForField({
|
||||
portSsl: httpsPort,
|
||||
url,
|
||||
});
|
||||
if (httpPort === 80) {
|
||||
expect(responseInsecure.port).toBe('');
|
||||
} else {
|
||||
expect(responseInsecure.port).toBe(httpPort.toString());
|
||||
}
|
||||
|
||||
if (httpsPort === 443) {
|
||||
expect(responseSecure.port).toBe('');
|
||||
} else {
|
||||
expect(responseSecure.port).toBe(httpsPort.toString());
|
||||
}
|
||||
if (httpsPort === 443) {
|
||||
expect(responseSecure.port).toBe('');
|
||||
} else {
|
||||
expect(responseSecure.port).toBe(httpsPort.toString());
|
||||
}
|
||||
});
|
||||
|
||||
test('getUrlForServer - field exists, ssl disabled', () => {
|
||||
const result = getUrlForServer({ nginx: { lanIp: '192.168.1.1', sslEnabled: false, httpPort: 123, httpsPort: 445 } as const as Nginx,
|
||||
field: 'lanIp',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot('"http://192.168.1.1:123/"');
|
||||
const result = getUrlForServer({
|
||||
nginx: {
|
||||
lanIp: '192.168.1.1',
|
||||
sslEnabled: false,
|
||||
httpPort: 123,
|
||||
httpsPort: 445,
|
||||
} as const as Nginx,
|
||||
field: 'lanIp',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot('"http://192.168.1.1:123/"');
|
||||
});
|
||||
|
||||
test('getUrlForServer - field exists, ssl yes', () => {
|
||||
const result = getUrlForServer({
|
||||
nginx: { lanIp: '192.168.1.1', sslEnabled: true, sslMode: 'yes', httpPort: 123, httpsPort: 445 } as const as Nginx,
|
||||
field: 'lanIp',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot('"https://192.168.1.1:445/"');
|
||||
const result = getUrlForServer({
|
||||
nginx: {
|
||||
lanIp: '192.168.1.1',
|
||||
sslEnabled: true,
|
||||
sslMode: 'yes',
|
||||
httpPort: 123,
|
||||
httpsPort: 445,
|
||||
} as const as Nginx,
|
||||
field: 'lanIp',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot('"https://192.168.1.1:445/"');
|
||||
});
|
||||
|
||||
test('getUrlForServer - field exists, ssl yes, port empty', () => {
|
||||
const result = getUrlForServer(
|
||||
{ nginx: { lanIp: '192.168.1.1', sslEnabled: true, sslMode: 'yes', httpPort: 80, httpsPort: 443 } as const as Nginx,
|
||||
field: 'lanIp',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot('"https://192.168.1.1/"');
|
||||
const result = getUrlForServer({
|
||||
nginx: {
|
||||
lanIp: '192.168.1.1',
|
||||
sslEnabled: true,
|
||||
sslMode: 'yes',
|
||||
httpPort: 80,
|
||||
httpsPort: 443,
|
||||
} as const as Nginx,
|
||||
field: 'lanIp',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot('"https://192.168.1.1/"');
|
||||
});
|
||||
|
||||
test('getUrlForServer - field exists, ssl auto', async () => {
|
||||
const getResult = async () => getUrlForServer({
|
||||
nginx: { lanIp: '192.168.1.1', sslEnabled: true, sslMode: 'auto', httpPort: 123, httpsPort: 445 } as const as Nginx,
|
||||
field: 'lanIp',
|
||||
});
|
||||
await expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Cannot get IP Based URL for field: "lanIp" SSL mode auto]`);
|
||||
const getResult = async () =>
|
||||
getUrlForServer({
|
||||
nginx: {
|
||||
lanIp: '192.168.1.1',
|
||||
sslEnabled: true,
|
||||
sslMode: 'auto',
|
||||
httpPort: 123,
|
||||
httpsPort: 445,
|
||||
} as const as Nginx,
|
||||
field: 'lanIp',
|
||||
});
|
||||
await expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`[Error: Cannot get IP Based URL for field: "lanIp" SSL mode auto]`
|
||||
);
|
||||
});
|
||||
|
||||
test('getUrlForServer - field does not exist, ssl disabled', async () => {
|
||||
const getResult = async () => getUrlForServer(
|
||||
{
|
||||
nginx: { lanIp: '192.168.1.1', sslEnabled: false, sslMode: 'no' } as const as Nginx,
|
||||
ports: {
|
||||
port: ':123', portSsl: ':445', defaultUrl: new URL('https://my-default-url.unraid.net'),
|
||||
},
|
||||
// @ts-expect-error Field doesn't exist
|
||||
field: 'idontexist',
|
||||
});
|
||||
await expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: IP URL Resolver: Could not resolve any access URL for field: "idontexist", is FQDN?: false]`);
|
||||
const getResult = async () =>
|
||||
getUrlForServer({
|
||||
nginx: { lanIp: '192.168.1.1', sslEnabled: false, sslMode: 'no' } as const as Nginx,
|
||||
ports: {
|
||||
port: ':123',
|
||||
portSsl: ':445',
|
||||
defaultUrl: new URL('https://my-default-url.unraid.net'),
|
||||
},
|
||||
// @ts-expect-error Field doesn't exist
|
||||
field: 'idontexist',
|
||||
});
|
||||
await expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`[Error: IP URL Resolver: Could not resolve any access URL for field: "idontexist", is FQDN?: false]`
|
||||
);
|
||||
});
|
||||
|
||||
test('getUrlForServer - FQDN - field exists, port non-empty', () => {
|
||||
const result = getUrlForServer({
|
||||
nginx: { lanFqdn: 'my-fqdn.unraid.net', httpsPort: 445 } as const as Nginx,
|
||||
field: 'lanFqdn',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot('"https://my-fqdn.unraid.net:445/"');
|
||||
const result = getUrlForServer({
|
||||
nginx: { lanFqdn: 'my-fqdn.unraid.net', httpsPort: 445 } as const as Nginx,
|
||||
field: 'lanFqdn',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot('"https://my-fqdn.unraid.net:445/"');
|
||||
});
|
||||
|
||||
test('getUrlForServer - FQDN - field exists, port empty', () => {
|
||||
const result = getUrlForServer({ nginx: { lanFqdn: 'my-fqdn.unraid.net', httpPort: 80, httpsPort: 443 } as const as Nginx,
|
||||
field: 'lanFqdn',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot('"https://my-fqdn.unraid.net/"');
|
||||
const result = getUrlForServer({
|
||||
nginx: { lanFqdn: 'my-fqdn.unraid.net', httpPort: 80, httpsPort: 443 } as const as Nginx,
|
||||
field: 'lanFqdn',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot('"https://my-fqdn.unraid.net/"');
|
||||
});
|
||||
|
||||
test.each([
|
||||
[{ nginx: { lanFqdn: 'my-fqdn.unraid.net', sslEnabled: false, sslMode: 'no', httpPort: 80, httpsPort: 443 } as const as Nginx, field: 'lanFqdn' as NginxUrlFields }],
|
||||
[{ nginx: { wanFqdn: 'my-fqdn.unraid.net', sslEnabled: true, sslMode: 'yes', httpPort: 80, httpsPort: 443 } as const as Nginx, field: 'wanFqdn' as NginxUrlFields }],
|
||||
[{ nginx: { wanFqdn6: 'my-fqdn.unraid.net', sslEnabled: true, sslMode: 'auto', httpPort: 80, httpsPort: 443 } as const as Nginx, field: 'wanFqdn6' as NginxUrlFields }],
|
||||
|
||||
[
|
||||
{
|
||||
nginx: {
|
||||
lanFqdn: 'my-fqdn.unraid.net',
|
||||
sslEnabled: false,
|
||||
sslMode: 'no',
|
||||
httpPort: 80,
|
||||
httpsPort: 443,
|
||||
} as const as Nginx,
|
||||
field: 'lanFqdn' as NginxUrlFields,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
nginx: {
|
||||
wanFqdn: 'my-fqdn.unraid.net',
|
||||
sslEnabled: true,
|
||||
sslMode: 'yes',
|
||||
httpPort: 80,
|
||||
httpsPort: 443,
|
||||
} as const as Nginx,
|
||||
field: 'wanFqdn' as NginxUrlFields,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
nginx: {
|
||||
wanFqdn6: 'my-fqdn.unraid.net',
|
||||
sslEnabled: true,
|
||||
sslMode: 'auto',
|
||||
httpPort: 80,
|
||||
httpsPort: 443,
|
||||
} as const as Nginx,
|
||||
field: 'wanFqdn6' as NginxUrlFields,
|
||||
},
|
||||
],
|
||||
])('getUrlForServer - FQDN', ({ nginx, field }) => {
|
||||
const result = getUrlForServer({ nginx, field });
|
||||
expect(result.toString()).toBe('https://my-fqdn.unraid.net/');
|
||||
const result = getUrlForServer({ nginx, field });
|
||||
expect(result.toString()).toBe('https://my-fqdn.unraid.net/');
|
||||
});
|
||||
|
||||
test('getUrlForServer - field does not exist, ssl disabled', async () => {
|
||||
const getResult = async () => getUrlForServer({ nginx:
|
||||
{ lanFqdn: 'my-fqdn.unraid.net' } as const as Nginx,
|
||||
ports: { portSsl: '', port: '', defaultUrl: new URL('https://my-default-url.unraid.net') },
|
||||
// @ts-expect-error Field doesn't exist
|
||||
field: 'idontexist' });
|
||||
await expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: IP URL Resolver: Could not resolve any access URL for field: "idontexist", is FQDN?: false]`);
|
||||
const getResult = async () =>
|
||||
getUrlForServer({
|
||||
nginx: { lanFqdn: 'my-fqdn.unraid.net' } as const as Nginx,
|
||||
ports: { portSsl: '', port: '', defaultUrl: new URL('https://my-default-url.unraid.net') },
|
||||
// @ts-expect-error Field doesn't exist
|
||||
field: 'idontexist',
|
||||
});
|
||||
await expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`[Error: IP URL Resolver: Could not resolve any access URL for field: "idontexist", is FQDN?: false]`
|
||||
);
|
||||
});
|
||||
|
||||
test('integration test, loading nginx ini and generating all URLs', async () => {
|
||||
await store.dispatch(loadStateFiles());
|
||||
await store.dispatch(loadConfigFile());
|
||||
await store.dispatch(loadStateFiles());
|
||||
await store.dispatch(loadConfigFile());
|
||||
|
||||
const urls = getServerIps();
|
||||
expect(urls.urls).toMatchInlineSnapshot(`
|
||||
const urls = getServerIps();
|
||||
expect(urls.urls).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"ipv4": "https://tower.local:4443/",
|
||||
@@ -198,7 +271,7 @@ test('integration test, loading nginx ini and generating all URLs', async () =>
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(urls.errors).toMatchInlineSnapshot(`
|
||||
expect(urls.errors).toMatchInlineSnapshot(`
|
||||
[
|
||||
[Error: IP URL Resolver: Could not resolve any access URL for field: "lanIp6", is FQDN?: false],
|
||||
]
|
||||
|
||||
@@ -4,45 +4,45 @@ import { beforeEach, expect, test, vi } from 'vitest';
|
||||
import '@app/mothership/utils/convert-to-fuzzy-time';
|
||||
|
||||
vi.mock('fs', () => ({
|
||||
default: {
|
||||
readFileSync: vi.fn().mockReturnValue('my-file'),
|
||||
writeFileSync: vi.fn(),
|
||||
existsSync: vi.fn(),
|
||||
},
|
||||
readFileSync: vi.fn().mockReturnValue('my-file'),
|
||||
existsSync: vi.fn(),
|
||||
default: {
|
||||
readFileSync: vi.fn().mockReturnValue('my-file'),
|
||||
writeFileSync: vi.fn(),
|
||||
existsSync: vi.fn(),
|
||||
},
|
||||
readFileSync: vi.fn().mockReturnValue('my-file'),
|
||||
existsSync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@graphql-tools/schema', () => ({
|
||||
makeExecutableSchema: vi.fn(),
|
||||
makeExecutableSchema: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@app/core/log', () => ({
|
||||
default: { relayLogger: { trace: vi.fn() } },
|
||||
relayLogger: { trace: vi.fn() },
|
||||
logger: { trace: vi.fn() },
|
||||
default: { relayLogger: { trace: vi.fn() } },
|
||||
relayLogger: { trace: vi.fn() },
|
||||
logger: { trace: vi.fn() },
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const generateTestCases = () => {
|
||||
const cases: Array<{ min: number; max: number }> = [];
|
||||
for (let i = 0; i < 15; i += 1) {
|
||||
const min = Math.round(Math.random() * 100);
|
||||
const max = min + (Math.round(Math.random() * 20));
|
||||
cases.push({ min, max });
|
||||
}
|
||||
const cases: Array<{ min: number; max: number }> = [];
|
||||
for (let i = 0; i < 15; i += 1) {
|
||||
const min = Math.round(Math.random() * 100);
|
||||
const max = min + Math.round(Math.random() * 20);
|
||||
cases.push({ min, max });
|
||||
}
|
||||
|
||||
return cases;
|
||||
return cases;
|
||||
};
|
||||
|
||||
test.each(generateTestCases())('Successfully converts to fuzzy time %o', async ({ min, max }) => {
|
||||
const { convertToFuzzyTime } = await import('@app/mothership/utils/convert-to-fuzzy-time');
|
||||
const { convertToFuzzyTime } = await import('@app/mothership/utils/convert-to-fuzzy-time');
|
||||
|
||||
const res = convertToFuzzyTime(min, max);
|
||||
expect(res).toBeGreaterThanOrEqual(min);
|
||||
expect(res).toBeLessThanOrEqual(max);
|
||||
const res = convertToFuzzyTime(min, max);
|
||||
expect(res).toBeGreaterThanOrEqual(min);
|
||||
expect(res).toBeLessThanOrEqual(max);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { config } from 'dotenv';
|
||||
|
||||
config({
|
||||
path: './.env.test',
|
||||
debug: false,
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
});
|
||||
|
||||
6
api/src/__test__/setup/keyserver-mock.ts
Normal file
6
api/src/__test__/setup/keyserver-mock.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { vi } from 'vitest';
|
||||
|
||||
vi.mock('@app/core/utils/misc/send-form-to-keyserver', () => {
|
||||
const sendFormToKeyServer = vi.fn().mockResolvedValue({ body: JSON.stringify({ valid: true }) });
|
||||
return { sendFormToKeyServer };
|
||||
});
|
||||
@@ -27,7 +27,7 @@ test('After init returns values from cfg file for all fields', async () => {
|
||||
upnpStatus: '',
|
||||
},
|
||||
local: {
|
||||
sandbox: expect.any(String)
|
||||
sandbox: expect.any(String),
|
||||
},
|
||||
nodeEnv: 'test',
|
||||
remote: {
|
||||
@@ -77,7 +77,7 @@ test('updateUserConfig merges in changes to current state', async () => {
|
||||
upnpStatus: '',
|
||||
},
|
||||
local: {
|
||||
sandbox: expect.any(String)
|
||||
sandbox: expect.any(String),
|
||||
},
|
||||
nodeEnv: 'test',
|
||||
remote: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect } from 'vitest';
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import { store } from '@app/store';
|
||||
import { FileLoadStatus } from '@app/store/types';
|
||||
|
||||
@@ -6,9 +7,9 @@ import { FileLoadStatus } from '@app/store/types';
|
||||
import '@app/store/modules/emhttp';
|
||||
|
||||
test('Before init returns default values for all fields', async () => {
|
||||
const { status, ...state } = store.getState().emhttp;
|
||||
expect(status).toBe(FileLoadStatus.UNLOADED);
|
||||
expect(state).toMatchInlineSnapshot(`
|
||||
const { status, ...state } = store.getState().emhttp;
|
||||
expect(status).toBe(FileLoadStatus.UNLOADED);
|
||||
expect(state).toMatchInlineSnapshot(`
|
||||
{
|
||||
"devices": [],
|
||||
"disks": [],
|
||||
@@ -24,16 +25,27 @@ test('Before init returns default values for all fields', async () => {
|
||||
});
|
||||
|
||||
test('After init returns values from cfg file for all fields', async () => {
|
||||
const { loadStateFiles } = await import('@app/store/modules/emhttp');
|
||||
const { loadStateFiles } = await import('@app/store/modules/emhttp');
|
||||
|
||||
// Load state files into store
|
||||
await store.dispatch(loadStateFiles());
|
||||
// Load state files into store
|
||||
await store.dispatch(loadStateFiles());
|
||||
|
||||
// Check if store has state files loaded
|
||||
const { devices, networks, nfsShares, nginx, shares, disks, smbShares, status, users, var: varState } = store.getState().emhttp;
|
||||
expect(status).toBe(FileLoadStatus.LOADED);
|
||||
expect(devices).toMatchInlineSnapshot('[]');
|
||||
expect(networks).toMatchInlineSnapshot(`
|
||||
// Check if store has state files loaded
|
||||
const {
|
||||
devices,
|
||||
networks,
|
||||
nfsShares,
|
||||
nginx,
|
||||
shares,
|
||||
disks,
|
||||
smbShares,
|
||||
status,
|
||||
users,
|
||||
var: varState,
|
||||
} = store.getState().emhttp;
|
||||
expect(status).toBe(FileLoadStatus.LOADED);
|
||||
expect(devices).toMatchInlineSnapshot('[]');
|
||||
expect(networks).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"bonding": true,
|
||||
@@ -99,7 +111,7 @@ test('After init returns values from cfg file for all fields', async () => {
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(nginx).toMatchInlineSnapshot(`
|
||||
expect(nginx).toMatchInlineSnapshot(`
|
||||
{
|
||||
"certificateName": "*.thisisfourtyrandomcharacters012345678900.myunraid.net",
|
||||
"certificatePath": "/boot/config/ssl/certs/certificate_bundle.pem",
|
||||
@@ -184,7 +196,7 @@ test('After init returns values from cfg file for all fields', async () => {
|
||||
"wanIp": "",
|
||||
}
|
||||
`);
|
||||
expect(disks).toMatchInlineSnapshot(`
|
||||
expect(disks).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"comment": null,
|
||||
@@ -356,7 +368,7 @@ test('After init returns values from cfg file for all fields', async () => {
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(shares).toMatchInlineSnapshot(`
|
||||
expect(shares).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"allocator": "highwater",
|
||||
@@ -432,7 +444,7 @@ test('After init returns values from cfg file for all fields', async () => {
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(nfsShares).toMatchInlineSnapshot(`
|
||||
expect(nfsShares).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"enabled": false,
|
||||
@@ -620,7 +632,7 @@ test('After init returns values from cfg file for all fields', async () => {
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(smbShares).toMatchInlineSnapshot(`
|
||||
expect(smbShares).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"caseSensitive": "auto",
|
||||
@@ -911,7 +923,7 @@ test('After init returns values from cfg file for all fields', async () => {
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(users).toMatchInlineSnapshot(`
|
||||
expect(users).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"description": "Console and webGui login account",
|
||||
@@ -936,7 +948,7 @@ test('After init returns values from cfg file for all fields', async () => {
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(varState).toMatchInlineSnapshot(`
|
||||
expect(varState).toMatchInlineSnapshot(`
|
||||
{
|
||||
"bindMgt": false,
|
||||
"cacheNumDevices": NaN,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import { setupNotificationWatch } from '@app/core/modules/notifications/setup-notification-watch';
|
||||
import { sleep } from '@app/core/utils/misc/sleep';
|
||||
import { loadDynamixConfigFile } from '@app/store/actions/load-dynamix-config-file';
|
||||
import { store } from '@app/store/index';
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
test('loads notifications properly', async () => {
|
||||
await store.dispatch(loadDynamixConfigFile()).unwrap();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import { store } from '@app/store';
|
||||
|
||||
test('Returns paths', async () => {
|
||||
|
||||
66
api/src/__test__/store/modules/registration.test.ts
Normal file
66
api/src/__test__/store/modules/registration.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import { store } from '@app/store';
|
||||
import { loadRegistrationKey } from '@app/store/modules/registration';
|
||||
import { FileLoadStatus, StateFileKey } from '@app/store/types';
|
||||
|
||||
// Preloading imports for faster tests
|
||||
|
||||
test('Before loading key returns null', async () => {
|
||||
const { status, keyFile } = store.getState().registration;
|
||||
expect(status).toBe(FileLoadStatus.UNLOADED);
|
||||
expect(keyFile).toBe(null);
|
||||
});
|
||||
|
||||
test('Requires emhttp to be loaded to find key file', async () => {
|
||||
// Load registration key into store
|
||||
await store.dispatch(loadRegistrationKey());
|
||||
|
||||
// Check if store has state files loaded
|
||||
const { status, keyFile } = store.getState().registration;
|
||||
|
||||
expect(status).toBe(FileLoadStatus.LOADED);
|
||||
expect(keyFile).toBe(null);
|
||||
});
|
||||
|
||||
test('Returns empty key if key location is empty', async () => {
|
||||
const { updateEmhttpState } = await import('@app/store/modules/emhttp');
|
||||
const { loadRegistrationKey } = await import('@app/store/modules/registration');
|
||||
|
||||
// Set key file location as empty
|
||||
// This should only happen if the user doesn't have a key file
|
||||
store.dispatch(
|
||||
updateEmhttpState({
|
||||
field: StateFileKey.var,
|
||||
state: {
|
||||
regFile: '',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Load registration key into store
|
||||
await store.dispatch(loadRegistrationKey());
|
||||
|
||||
// Check if store has state files loaded
|
||||
const { status, keyFile } = store.getState().registration;
|
||||
expect(status).toBe(FileLoadStatus.LOADED);
|
||||
expect(keyFile).toBe('');
|
||||
});
|
||||
|
||||
test('Returns decoded key file if key location exists', async () => {
|
||||
const { loadRegistrationKey } = await import('@app/store/modules/registration');
|
||||
const { loadStateFiles } = await import('@app/store/modules/emhttp');
|
||||
|
||||
// Load state files into store
|
||||
await store.dispatch(loadStateFiles());
|
||||
|
||||
// Load registration key into store
|
||||
await store.dispatch(loadRegistrationKey());
|
||||
|
||||
// Check if store has state files loaded
|
||||
const { status, keyFile } = store.getState().registration;
|
||||
expect(status).toBe(FileLoadStatus.LOADED);
|
||||
expect(keyFile).toMatchInlineSnapshot(
|
||||
'"hVs1tLjvC9FiiQsIwIQ7G1KszAcexf0IneThhnmf22SB0dGs5WzRkqMiSMmt2DtR5HOXFUD32YyxuzGeUXmky3zKpSu6xhZNKVg5atGM1OfvkzHBMldI3SeBLuUFSgejLbpNUMdTrbk64JJdbzle4O8wiQgkIpAMIGxeYLwLBD4zHBcfyzq40QnxG--HcX6j25eE0xqa2zWj-j0b0rCAXahJV2a3ySCbPzr1MvfPRTVb0rr7KJ-25R592hYrz4H7Sc1B3p0lr6QUxHE6o7bcYrWKDRtIVoZ8SMPpd1_0gzYIcl5GsDFzFumTXUh8NEnl0Q8hwW1YE-tRc6Y_rrvd7w"'
|
||||
);
|
||||
});
|
||||
18
api/src/__test__/store/state-parsers/devices.test.ts
Normal file
18
api/src/__test__/store/state-parsers/devices.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { join } from 'path';
|
||||
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import type { DevicesIni } from '@app/store/state-parsers/devices';
|
||||
import { store } from '@app/store';
|
||||
|
||||
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('[]');
|
||||
});
|
||||
83
api/src/__test__/store/state-parsers/network.test.ts
Normal file
83
api/src/__test__/store/state-parsers/network.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { join } from 'path';
|
||||
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import type { NetworkIni } from '@app/store/state-parsers/network';
|
||||
import { store } from '@app/store';
|
||||
|
||||
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,
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
205
api/src/__test__/store/state-parsers/nfs.test.ts
Normal file
205
api/src/__test__/store/state-parsers/nfs.test.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { join } from 'path';
|
||||
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import type { NfsSharesIni } from '@app/store/state-parsers/nfs';
|
||||
import { store } from '@app/store';
|
||||
|
||||
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,16 +1,18 @@
|
||||
import { join } from 'path';
|
||||
|
||||
import { expect, test } from 'vitest';
|
||||
import { store } from '@app/store';
|
||||
|
||||
import type { NginxIni } from '@app/store/state-parsers/nginx';
|
||||
import { store } from '@app/store';
|
||||
|
||||
test('Returns parsed state file', async () => {
|
||||
const { parse } = await import('@app/store/state-parsers/nginx');
|
||||
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
|
||||
const { paths } = store.getState();
|
||||
const filePath = join(paths.states, 'nginx.ini');
|
||||
const stateFile = parseConfig<NginxIni>({
|
||||
filePath,
|
||||
type: 'ini',
|
||||
});
|
||||
expect(parse(stateFile)).toMatchSnapshot();
|
||||
});
|
||||
const { parse } = await import('@app/store/state-parsers/nginx');
|
||||
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
|
||||
const { paths } = store.getState();
|
||||
const filePath = join(paths.states, 'nginx.ini');
|
||||
const stateFile = parseConfig<NginxIni>({
|
||||
filePath,
|
||||
type: 'ini',
|
||||
});
|
||||
expect(parse(stateFile)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { join } from 'path';
|
||||
|
||||
import { expect, test } from 'vitest';
|
||||
import { store } from '@app/store';
|
||||
|
||||
import type { SharesIni } from '@app/store/state-parsers/shares';
|
||||
import { store } from '@app/store';
|
||||
|
||||
test('Returns parsed state file', async () => {
|
||||
const { parse } = await import('@app/store/state-parsers/shares');
|
||||
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
|
||||
const { paths } = store.getState();
|
||||
const filePath = join(paths.states, 'shares.ini');
|
||||
const stateFile = parseConfig<SharesIni>({
|
||||
filePath,
|
||||
type: 'ini',
|
||||
});
|
||||
expect(parse(stateFile)).toMatchInlineSnapshot(`
|
||||
const { parse } = await import('@app/store/state-parsers/shares');
|
||||
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
|
||||
const { paths } = store.getState();
|
||||
const filePath = join(paths.states, 'shares.ini');
|
||||
const stateFile = parseConfig<SharesIni>({
|
||||
filePath,
|
||||
type: 'ini',
|
||||
});
|
||||
expect(parse(stateFile)).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"allocator": "highwater",
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { join } from 'path';
|
||||
|
||||
import { expect, test } from 'vitest';
|
||||
import { store } from '@app/store';
|
||||
|
||||
import type { SlotsIni } from '@app/store/state-parsers/slots';
|
||||
import { store } from '@app/store';
|
||||
|
||||
test('Returns parsed state file', async () => {
|
||||
const { parse } = await import('@app/store/state-parsers/slots');
|
||||
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
|
||||
const { paths } = store.getState();
|
||||
const filePath = join(paths.states, 'disks.ini');
|
||||
const stateFile = parseConfig<SlotsIni>({
|
||||
filePath,
|
||||
type: 'ini',
|
||||
});
|
||||
expect(parse(stateFile)).toMatchInlineSnapshot(`
|
||||
const { parse } = await import('@app/store/state-parsers/slots');
|
||||
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
|
||||
const { paths } = store.getState();
|
||||
const filePath = join(paths.states, 'disks.ini');
|
||||
const stateFile = parseConfig<SlotsIni>({
|
||||
filePath,
|
||||
type: 'ini',
|
||||
});
|
||||
expect(parse(stateFile)).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"comment": null,
|
||||
|
||||
308
api/src/__test__/store/state-parsers/smb.test.ts
Normal file
308
api/src/__test__/store/state-parsers/smb.test.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
import { join } from 'path';
|
||||
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import type { SmbIni } from '@app/store/state-parsers/smb';
|
||||
import { store } from '@app/store';
|
||||
|
||||
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": [],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
42
api/src/__test__/store/state-parsers/users.test.ts
Normal file
42
api/src/__test__/store/state-parsers/users.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { join } from 'path';
|
||||
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import type { UsersIni } from '@app/store/state-parsers/users';
|
||||
import { store } from '@app/store';
|
||||
|
||||
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,19 +1,21 @@
|
||||
import { join } from 'path';
|
||||
|
||||
import { expect, test } from 'vitest';
|
||||
import { store } from '@app/store';
|
||||
|
||||
import type { VarIni } from '@app/store/state-parsers/var';
|
||||
import { store } from '@app/store';
|
||||
|
||||
test('Returns parsed state file', async () => {
|
||||
const { parse } = await import('@app/store/state-parsers/var');
|
||||
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
|
||||
const { paths } = store.getState();
|
||||
const filePath = join(paths.states, 'var.ini');
|
||||
const stateFile = parseConfig<VarIni>({
|
||||
filePath,
|
||||
type: 'ini',
|
||||
});
|
||||
const { parse } = await import('@app/store/state-parsers/var');
|
||||
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
|
||||
const { paths } = store.getState();
|
||||
const filePath = join(paths.states, 'var.ini');
|
||||
const stateFile = parseConfig<VarIni>({
|
||||
filePath,
|
||||
type: 'ini',
|
||||
});
|
||||
|
||||
expect(parse(stateFile)).toMatchInlineSnapshot(`
|
||||
expect(parse(stateFile)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"bindMgt": false,
|
||||
"cacheNumDevices": NaN,
|
||||
|
||||
30
api/src/__test__/store/sync/registration-sync.test.ts
Normal file
30
api/src/__test__/store/sync/registration-sync.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
vi.mock('@app/core/pubsub', () => ({
|
||||
pubsub: { publish: vi.fn() },
|
||||
}));
|
||||
|
||||
test('Creates a registration event', async () => {
|
||||
const { createRegistrationEvent } = await import('@app/store/sync/registration-sync');
|
||||
const { store } = await import('@app/store');
|
||||
const { loadStateFiles } = await import('@app/store/modules/emhttp');
|
||||
|
||||
// Load state files into store
|
||||
await store.dispatch(loadStateFiles());
|
||||
|
||||
const state = store.getState();
|
||||
const registrationEvent = createRegistrationEvent(state);
|
||||
expect(registrationEvent).toMatchInlineSnapshot(`
|
||||
{
|
||||
"registration": {
|
||||
"guid": "13FE-4200-C300-58C372A52B19",
|
||||
"keyFile": {
|
||||
"contents": null,
|
||||
"location": "/app/dev/Unraid.net/Pro.key",
|
||||
},
|
||||
"state": "PRO",
|
||||
"type": "PRO",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
20
api/src/__test__/upnp/helpers.test.ts
Normal file
20
api/src/__test__/upnp/helpers.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { type Mapping } from '@runonflux/nat-upnp';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import { getWanPortForUpnp } from '@app/upnp/helpers';
|
||||
|
||||
test('it successfully gets a wan port given no exclusions', () => {
|
||||
const port = getWanPortForUpnp(null, 36_000, 38_000);
|
||||
expect(port).toBeGreaterThan(35_999);
|
||||
expect(port).toBeLessThan(38_001);
|
||||
});
|
||||
|
||||
test('it fails to get a wan port given exclusions', () => {
|
||||
const port = getWanPortForUpnp([{ public: { port: 36_000 } }] as Mapping[], 36_000, 36_000);
|
||||
expect(port).toBeNull();
|
||||
});
|
||||
|
||||
test('it succeeds in getting a wan port given exclusions', () => {
|
||||
const port = getWanPortForUpnp([{ public: { port: 36_000 } }] as Mapping[], 30_000, 36_000);
|
||||
expect(port).not.toBeNull();
|
||||
});
|
||||
@@ -1,12 +1,11 @@
|
||||
import { getters, type RootState, store } from '@app/store';
|
||||
import uniq from 'lodash/uniq';
|
||||
import {
|
||||
getServerIps,
|
||||
getUrlForField,
|
||||
} from '@app/graphql/resolvers/subscription/network';
|
||||
import { FileLoadStatus } from '@app/store/types';
|
||||
import { logger } from '../core';
|
||||
|
||||
import type { RootState } from '@app/store';
|
||||
import { logger } from '@app/core';
|
||||
import { GRAPHQL_INTROSPECTION } from '@app/environment';
|
||||
import { getServerIps, getUrlForField } from '@app/graphql/resolvers/subscription/network';
|
||||
import { getters, store } from '@app/store';
|
||||
import { FileLoadStatus } from '@app/store/types';
|
||||
|
||||
const getAllowedSocks = (): string[] => [
|
||||
// Notifier bridge
|
||||
@@ -19,9 +18,7 @@ const getAllowedSocks = (): string[] => [
|
||||
'/var/run/unraid-cli.sock',
|
||||
];
|
||||
|
||||
const getLocalAccessUrlsForServer = (
|
||||
state: RootState = store.getState()
|
||||
): string[] => {
|
||||
const getLocalAccessUrlsForServer = (state: RootState = store.getState()): string[] => {
|
||||
const { emhttp } = state;
|
||||
if (emhttp.status !== FileLoadStatus.LOADED) {
|
||||
return [];
|
||||
@@ -40,22 +37,17 @@ const getLocalAccessUrlsForServer = (
|
||||
}).toString(),
|
||||
];
|
||||
} catch (error: unknown) {
|
||||
logger.debug(
|
||||
'Caught error in getLocalAccessUrlsForServer: \n%o',
|
||||
error
|
||||
);
|
||||
logger.debug('Caught error in getLocalAccessUrlsForServer: \n%o', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const getRemoteAccessUrlsForAllowedOrigins = (
|
||||
state: RootState = store.getState()
|
||||
): string[] => {
|
||||
const getRemoteAccessUrlsForAllowedOrigins = (state: RootState = store.getState()): string[] => {
|
||||
const { urls } = getServerIps(state);
|
||||
|
||||
if (urls) {
|
||||
return urls.reduce<string[]>((acc, curr) => {
|
||||
if (curr.ipv4 && curr.ipv6 || curr.ipv4) {
|
||||
if ((curr.ipv4 && curr.ipv6) || curr.ipv4) {
|
||||
acc.push(curr.ipv4.toString());
|
||||
} else if (curr.ipv6) {
|
||||
acc.push(curr.ipv6.toString());
|
||||
@@ -74,11 +66,7 @@ export const getExtraOrigins = (): string[] => {
|
||||
return extraOrigins
|
||||
.replaceAll(' ', '')
|
||||
.split(',')
|
||||
.filter(
|
||||
(origin) =>
|
||||
origin.startsWith('http://') ||
|
||||
origin.startsWith('https://')
|
||||
);
|
||||
.filter((origin) => origin.startsWith('http://') || origin.startsWith('https://'));
|
||||
}
|
||||
|
||||
return [];
|
||||
@@ -99,9 +87,7 @@ const getApolloSandbox = (): string[] => {
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getAllowedOrigins = (
|
||||
state: RootState = store.getState()
|
||||
): string[] =>
|
||||
export const getAllowedOrigins = (state: RootState = store.getState()): string[] =>
|
||||
uniq([
|
||||
...getAllowedSocks(),
|
||||
...getLocalAccessUrlsForServer(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { uptime } from 'os';
|
||||
|
||||
// Get uptime on boot and convert to date
|
||||
export const bootTimestamp = new Date(new Date().getTime() - (uptime() * 1_000));
|
||||
export const bootTimestamp = new Date(new Date().getTime() - uptime() * 1_000);
|
||||
|
||||
15
api/src/common/dashboard/get-unraid-version.ts
Normal file
15
api/src/common/dashboard/get-unraid-version.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { getters } from '@app/store';
|
||||
import { FileLoadStatus } from '@app/store/types';
|
||||
|
||||
/**
|
||||
* Unraid version string.
|
||||
* @returns The current version.
|
||||
*/
|
||||
export const getUnraidVersion = async (): Promise<string> => {
|
||||
const { status, var: emhttpVar } = getters.emhttp();
|
||||
if (status === FileLoadStatus.LOADED) {
|
||||
return emhttpVar.version;
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
};
|
||||
9
api/src/common/unraid-version-compare.ts
Normal file
9
api/src/common/unraid-version-compare.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { satisfies } from 'semver';
|
||||
|
||||
import { getters } from '@app/store';
|
||||
|
||||
/**
|
||||
* Compare version against the current unraid version.
|
||||
*/
|
||||
export const compareUnraidVersion = (range: string) =>
|
||||
satisfies(getters.emhttp().var.version, range, { includePrerelease: true });
|
||||
@@ -1,7 +1,9 @@
|
||||
import { PORT } from '@app/environment';
|
||||
import type { JSONWebKeySet } from 'jose';
|
||||
import { join } from 'path';
|
||||
|
||||
import type { JSONWebKeySet } from 'jose';
|
||||
|
||||
import { PORT } from '@app/environment';
|
||||
|
||||
export const getInternalApiAddress = (isHttp = true, nginxPort = 80) => {
|
||||
const envPort = PORT;
|
||||
const protocol = isHttp ? 'http' : 'ws';
|
||||
@@ -18,7 +20,6 @@ export const getInternalApiAddress = (isHttp = true, nginxPort = 80) => {
|
||||
|
||||
// Prod mode (user didn't change webgui port)
|
||||
return `${protocol}://127.0.0.1/graphql`;
|
||||
|
||||
};
|
||||
|
||||
// Milliseconds
|
||||
@@ -69,18 +70,15 @@ export const JWKS_LOCAL_PAYLOAD: JSONWebKeySet = {
|
||||
],
|
||||
};
|
||||
|
||||
export const OAUTH_BASE_URL =
|
||||
'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_btSkhlsEk';
|
||||
export const OAUTH_BASE_URL = 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_btSkhlsEk';
|
||||
export const OAUTH_CLIENT_ID = '53ci4o48gac8vq5jepubkjmo36';
|
||||
export const OAUTH_OPENID_CONFIGURATION_URL =
|
||||
OAUTH_BASE_URL + '/.well-known/openid-configuration';
|
||||
export const OAUTH_OPENID_CONFIGURATION_URL = OAUTH_BASE_URL + '/.well-known/openid-configuration';
|
||||
export const JWKS_REMOTE_LINK = OAUTH_BASE_URL + '/.well-known/jwks.json';
|
||||
export const RCD_SCRIPT = 'rc.unraid-api';
|
||||
export const KEYSERVER_VALIDATION_ENDPOINT =
|
||||
'https://keys.lime-technology.com/validate/apikey';
|
||||
export const KEYSERVER_VALIDATION_ENDPOINT = 'https://keys.lime-technology.com/validate/apikey';
|
||||
|
||||
/** Set the max retries for the GraphQL Client */
|
||||
export const MAX_RETRIES_FOR_LINEAR_BACKOFF = 100;
|
||||
|
||||
export const PM2_PATH = join(import.meta.dirname, '../../', 'node_modules', '.bin', 'pm2');
|
||||
export const ECOSYSTEM_PATH = join(import.meta.dirname, '../../', 'ecosystem.config.json');
|
||||
export const ECOSYSTEM_PATH = join(import.meta.dirname, '../../', 'ecosystem.config.json');
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { AppError } from '@app/core/errors/app-error';
|
||||
|
||||
/**
|
||||
* API key error.
|
||||
*/
|
||||
* API key error.
|
||||
*/
|
||||
export class ApiKeyError extends AppError {
|
||||
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,36 +2,36 @@
|
||||
* Generic application error.
|
||||
*/
|
||||
export class AppError extends Error {
|
||||
/** The HTTP status associated with this error. */
|
||||
public status: number;
|
||||
/** The HTTP status associated with this error. */
|
||||
public status: number;
|
||||
|
||||
/** Should we kill the application when thrown. */
|
||||
public fatal = false;
|
||||
/** Should we kill the application when thrown. */
|
||||
public fatal = false;
|
||||
|
||||
constructor(message: string, status?: number) {
|
||||
// Calling parent constructor of base Error class.
|
||||
super(message);
|
||||
constructor(message: string, status?: number) {
|
||||
// Calling parent constructor of base Error class.
|
||||
super(message);
|
||||
|
||||
// Saving class name in the property of our custom error as a shortcut.
|
||||
this.name = this.constructor.name;
|
||||
// Saving class name in the property of our custom error as a shortcut.
|
||||
this.name = this.constructor.name;
|
||||
|
||||
// Capturing stack trace, excluding constructor call from it.
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
// Capturing stack trace, excluding constructor call from it.
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
// We're using HTTP status codes with `500` as the default
|
||||
this.status = status ?? 500;
|
||||
}
|
||||
// We're using HTTP status codes with `500` as the default
|
||||
this.status = status ?? 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert error to JSON format.
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
error: {
|
||||
name: this.name,
|
||||
message: this.message,
|
||||
stacktrace: this.stack,
|
||||
},
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Convert error to JSON format.
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
error: {
|
||||
name: this.name,
|
||||
message: this.message,
|
||||
stacktrace: this.stack,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AppError } from '@app/core/errors/app-error';
|
||||
* The attempted operation can only be processed while the array is stopped.
|
||||
*/
|
||||
export class ArrayRunningError extends AppError {
|
||||
constructor() {
|
||||
super('Array needs to be stopped before any changes can occur.');
|
||||
}
|
||||
constructor() {
|
||||
super('Array needs to be stopped before any changes can occur.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@ import { FatalAppError } from '@app/core/errors/fatal-error';
|
||||
* Atomic write error
|
||||
*/
|
||||
export class AtomicWriteError extends FatalAppError {
|
||||
constructor(message: string, private readonly filePath: string, status = 500) {
|
||||
super(message, status);
|
||||
}
|
||||
constructor(
|
||||
message: string,
|
||||
private readonly filePath: string,
|
||||
status = 500
|
||||
) {
|
||||
super(message, status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import { FatalAppError } from '@app/core/errors/fatal-error';
|
||||
* Em cmd client error.
|
||||
*/
|
||||
export class EmCmdError extends FatalAppError {
|
||||
constructor(method: string, option: string, options: string[]) {
|
||||
const message = `Invalid option "${option}" for ${method}, allowed options ${JSON.stringify(options)}`;
|
||||
super(message);
|
||||
}
|
||||
constructor(method: string, option: string, options: string[]) {
|
||||
const message = `Invalid option "${option}" for ${method}, allowed options ${JSON.stringify(options)}`;
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ import { AppError } from '@app/core/errors/app-error';
|
||||
* Fatal application error.
|
||||
*/
|
||||
export class FatalAppError extends AppError {
|
||||
fatal = true;
|
||||
fatal = true;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import { AppError } from '@app/core/errors/app-error';
|
||||
* Module is missing a needed field
|
||||
*/
|
||||
export class FieldMissingError extends AppError {
|
||||
constructor(private readonly field: string) {
|
||||
// Overriding both message and status code.
|
||||
super(`Field missing: ${field}`, 400);
|
||||
}
|
||||
constructor(private readonly field: string) {
|
||||
// Overriding both message and status code.
|
||||
super(`Field missing: ${field}`, 400);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import { AppError } from '@app/core/errors/app-error';
|
||||
* The provided file is missing
|
||||
*/
|
||||
export class FileMissingError extends AppError {
|
||||
/**
|
||||
* @hideconstructor
|
||||
*/
|
||||
constructor(private readonly filePath: string) {
|
||||
// Overriding both message and status code.
|
||||
super('File missing: ' + filePath, 400);
|
||||
}
|
||||
/**
|
||||
* @hideconstructor
|
||||
*/
|
||||
constructor(private readonly filePath: string) {
|
||||
// Overriding both message and status code.
|
||||
super('File missing: ' + filePath, 400);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { AppError } from '@app/core/errors/app-error';
|
||||
* Sorry about that. 😔
|
||||
*/
|
||||
export class NotImplementedError extends AppError {
|
||||
constructor() {
|
||||
super('Not implemented!');
|
||||
}
|
||||
constructor() {
|
||||
super('Not implemented!');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { format } from 'util';
|
||||
|
||||
import { AppError } from '@app/core/errors/app-error';
|
||||
|
||||
/**
|
||||
* Invalid param provided to module
|
||||
*/
|
||||
export class ParamInvalidError extends AppError {
|
||||
constructor(parameterName: string, parameter: any) {
|
||||
// Overriding both message and status code.
|
||||
super(format('Param invalid: %s = %s', parameterName, parameter), 500);
|
||||
}
|
||||
constructor(parameterName: string, parameter: any) {
|
||||
// Overriding both message and status code.
|
||||
super(format('Param invalid: %s = %s', parameterName, parameter), 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import { AppError } from '@app/core/errors/app-error';
|
||||
* Required param is missing
|
||||
*/
|
||||
export class ParameterMissingError extends AppError {
|
||||
constructor(parameterName: string) {
|
||||
// Override both message and status code.
|
||||
super(`Param missing: ${parameterName}`, 500);
|
||||
}
|
||||
constructor(parameterName: string) {
|
||||
// Override both message and status code.
|
||||
super(`Param missing: ${parameterName}`, 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AppError } from '@app/core/errors/app-error';
|
||||
* Non fatal permission error
|
||||
*/
|
||||
export class PermissionError extends AppError {
|
||||
constructor(message: string) {
|
||||
super(message || 'Permission denied!');
|
||||
}
|
||||
constructor(message: string) {
|
||||
super(message || 'Permission denied!');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
import { pino } from 'pino';
|
||||
import { LOG_TYPE } from '@app/environment';
|
||||
|
||||
import pretty from 'pino-pretty';
|
||||
|
||||
export const levels = [
|
||||
'trace',
|
||||
'debug',
|
||||
'info',
|
||||
'warn',
|
||||
'error',
|
||||
'fatal',
|
||||
] as const;
|
||||
import { LOG_TYPE } from '@app/environment';
|
||||
|
||||
export type LogLevel = typeof levels[number];
|
||||
export const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'] as const;
|
||||
|
||||
export type LogLevel = (typeof levels)[number];
|
||||
|
||||
const level =
|
||||
levels[
|
||||
levels.indexOf(
|
||||
process.env.LOG_LEVEL?.toLowerCase() as (typeof levels)[number]
|
||||
)
|
||||
] ?? 'info';
|
||||
levels[levels.indexOf(process.env.LOG_LEVEL?.toLowerCase() as (typeof levels)[number])] ?? 'info';
|
||||
|
||||
export const logDestination = pino.destination({
|
||||
sync: true,
|
||||
|
||||
@@ -5,34 +5,34 @@ import { ensurePermission } from '@app/core/utils/permissions/ensure-permission'
|
||||
import { getters } from '@app/store';
|
||||
|
||||
export const addShare = async (context: CoreContext<unknown, { name: string }>): Promise<CoreResult> => {
|
||||
const { user, data } = context;
|
||||
const { user, data } = context;
|
||||
|
||||
if (!data?.name) {
|
||||
throw new AppError('No name provided');
|
||||
}
|
||||
if (!data?.name) {
|
||||
throw new AppError('No name provided');
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'share',
|
||||
action: 'create',
|
||||
possession: 'any',
|
||||
});
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'share',
|
||||
action: 'create',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
const { shares, disks } = getters.emhttp();
|
||||
const { shares, disks } = getters.emhttp();
|
||||
|
||||
const { name } = data;
|
||||
const userShares = shares.map(({ name }) => name);
|
||||
const diskShares = disks.filter(slot => slot.exportable).filter(({ name }) => name.startsWith('disk')).map(({ name }) => name);
|
||||
const { name } = data;
|
||||
const userShares = shares.map(({ name }) => name);
|
||||
const diskShares = disks
|
||||
.filter((slot) => slot.exportable)
|
||||
.filter(({ name }) => name.startsWith('disk'))
|
||||
.map(({ name }) => name);
|
||||
|
||||
// Existing share names
|
||||
const inUseNames = new Set([
|
||||
...userShares,
|
||||
...diskShares,
|
||||
]);
|
||||
// Existing share names
|
||||
const inUseNames = new Set([...userShares, ...diskShares]);
|
||||
|
||||
if (inUseNames.has(name)) {
|
||||
throw new AppError(`Share already exists with name: ${name}`, 400);
|
||||
}
|
||||
if (inUseNames.has(name)) {
|
||||
throw new AppError(`Share already exists with name: ${name}`, 400);
|
||||
}
|
||||
|
||||
throw new NotImplementedError();
|
||||
throw new NotImplementedError();
|
||||
};
|
||||
|
||||
@@ -1,83 +1,83 @@
|
||||
import type { CoreContext, CoreResult } from '@app/core/types';
|
||||
import { AppError } from '@app/core/errors/app-error';
|
||||
import { FieldMissingError } from '@app/core/errors/field-missing-error';
|
||||
import { pubsub } from '@app/core/pubsub';
|
||||
import { emcmd } from '@app/core/utils/clients/emcmd';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
import { hasFields } from '@app/core/utils/validation/has-fields';
|
||||
import { FieldMissingError } from '@app/core/errors/field-missing-error';
|
||||
import { emcmd } from '@app/core/utils/clients/emcmd';
|
||||
import { getters } from '@app/store';
|
||||
import { pubsub } from '@app/core/pubsub';
|
||||
|
||||
interface Context extends CoreContext {
|
||||
readonly data: {
|
||||
/** Display name. */
|
||||
readonly name: string;
|
||||
/** User's password. */
|
||||
readonly password: string;
|
||||
/** Friendly description. */
|
||||
readonly description: string;
|
||||
};
|
||||
readonly data: {
|
||||
/** Display name. */
|
||||
readonly name: string;
|
||||
/** User's password. */
|
||||
readonly password: string;
|
||||
/** Friendly description. */
|
||||
readonly description: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add user account.
|
||||
*/
|
||||
export const addUser = async (context: Context): Promise<CoreResult> => {
|
||||
const { data } = context;
|
||||
// Check permissions
|
||||
ensurePermission(context.user, {
|
||||
resource: 'user',
|
||||
action: 'create',
|
||||
possession: 'any',
|
||||
});
|
||||
const { data } = context;
|
||||
// Check permissions
|
||||
ensurePermission(context.user, {
|
||||
resource: 'user',
|
||||
action: 'create',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
// Validation
|
||||
const { name, description = '', password } = data;
|
||||
const missingFields = hasFields(data, ['name', 'password']);
|
||||
// Validation
|
||||
const { name, description = '', password } = data;
|
||||
const missingFields = hasFields(data, ['name', 'password']);
|
||||
|
||||
if (missingFields.length !== 0) {
|
||||
// Only log first error.
|
||||
throw new FieldMissingError(missingFields[0]);
|
||||
}
|
||||
if (missingFields.length !== 0) {
|
||||
// Only log first error.
|
||||
throw new FieldMissingError(missingFields[0]);
|
||||
}
|
||||
|
||||
// Check user name isn't taken
|
||||
if (getters.emhttp().users.find(user => user.name === name)) {
|
||||
throw new AppError('A user account with that name already exists.');
|
||||
}
|
||||
// Check user name isn't taken
|
||||
if (getters.emhttp().users.find((user) => user.name === name)) {
|
||||
throw new AppError('A user account with that name already exists.');
|
||||
}
|
||||
|
||||
// Create user
|
||||
await emcmd({
|
||||
userName: name,
|
||||
userDesc: description,
|
||||
userPassword: password,
|
||||
userPasswordConf: password,
|
||||
cmdUserEdit: 'Add',
|
||||
});
|
||||
// Create user
|
||||
await emcmd({
|
||||
userName: name,
|
||||
userDesc: description,
|
||||
userPassword: password,
|
||||
userPasswordConf: password,
|
||||
cmdUserEdit: 'Add',
|
||||
});
|
||||
|
||||
// Get fresh copy of Users with the new user
|
||||
const user = getters.emhttp().users.find(user => user.name === name);
|
||||
if (!user) {
|
||||
// User managed to disappear between us creating it and the lookup?
|
||||
throw new AppError('Internal Server Error!');
|
||||
}
|
||||
// Get fresh copy of Users with the new user
|
||||
const user = getters.emhttp().users.find((user) => user.name === name);
|
||||
if (!user) {
|
||||
// User managed to disappear between us creating it and the lookup?
|
||||
throw new AppError('Internal Server Error!');
|
||||
}
|
||||
|
||||
// Update users channel with new user
|
||||
pubsub.publish('users', {
|
||||
users: {
|
||||
mutation: 'CREATED',
|
||||
node: [user],
|
||||
},
|
||||
});
|
||||
// Update users channel with new user
|
||||
pubsub.publish('users', {
|
||||
users: {
|
||||
mutation: 'CREATED',
|
||||
node: [user],
|
||||
},
|
||||
});
|
||||
|
||||
// Update user channel with new user
|
||||
pubsub.publish('user', {
|
||||
user: {
|
||||
mutation: 'CREATED',
|
||||
node: user,
|
||||
},
|
||||
});
|
||||
// Update user channel with new user
|
||||
pubsub.publish('user', {
|
||||
user: {
|
||||
mutation: 'CREATED',
|
||||
node: user,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
text: `User created successfully. ${JSON.stringify(user, null, 2)}`,
|
||||
json: user,
|
||||
};
|
||||
return {
|
||||
text: `User created successfully. ${JSON.stringify(user, null, 2)}`,
|
||||
json: user,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
import { type CoreContext, type CoreResult } from '@app/core/types';
|
||||
import { ArrayRunningError } from '@app/core/errors/array-running-error';
|
||||
import { FieldMissingError } from '@app/core/errors/field-missing-error';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
import { hasFields } from '@app/core/utils/validation/has-fields';
|
||||
import { getArrayData } from '@app/core/modules/array/get-array-data';
|
||||
import { type CoreContext, type CoreResult } from '@app/core/types';
|
||||
import { arrayIsRunning } from '@app/core/utils/array/array-is-running';
|
||||
import { emcmd } from '@app/core/utils/clients/emcmd';
|
||||
import { ArrayRunningError } from '@app/core/errors/array-running-error';
|
||||
import { getArrayData } from '@app/core/modules/array/get-array-data';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
import { hasFields } from '@app/core/utils/validation/has-fields';
|
||||
|
||||
/**
|
||||
* Add a disk to the array.
|
||||
*/
|
||||
export const addDiskToArray = async function (context: CoreContext): Promise<CoreResult> {
|
||||
const { data = {}, user } = context;
|
||||
const { data = {}, user } = context;
|
||||
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'array',
|
||||
action: 'create',
|
||||
possession: 'any',
|
||||
});
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'array',
|
||||
action: 'create',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
const missingFields = hasFields(data, ['id']);
|
||||
if (missingFields.length !== 0) {
|
||||
// Just log first error
|
||||
throw new FieldMissingError(missingFields[0]);
|
||||
}
|
||||
const missingFields = hasFields(data, ['id']);
|
||||
if (missingFields.length !== 0) {
|
||||
// Just log first error
|
||||
throw new FieldMissingError(missingFields[0]);
|
||||
}
|
||||
|
||||
if (arrayIsRunning()) {
|
||||
throw new ArrayRunningError();
|
||||
}
|
||||
if (arrayIsRunning()) {
|
||||
throw new ArrayRunningError();
|
||||
}
|
||||
|
||||
const { id: diskId, slot: preferredSlot } = data;
|
||||
const slot = Number.parseInt(preferredSlot as string, 10);
|
||||
const { id: diskId, slot: preferredSlot } = data;
|
||||
const slot = Number.parseInt(preferredSlot as string, 10);
|
||||
|
||||
// Add disk
|
||||
await emcmd({
|
||||
changeDevice: 'apply',
|
||||
[`slotId.${slot}`]: diskId,
|
||||
});
|
||||
// Add disk
|
||||
await emcmd({
|
||||
changeDevice: 'apply',
|
||||
[`slotId.${slot}`]: diskId,
|
||||
});
|
||||
|
||||
const array = getArrayData()
|
||||
const array = getArrayData();
|
||||
|
||||
// Disk added successfully
|
||||
return {
|
||||
text: `Disk was added to the array in slot ${slot}.`,
|
||||
json: array,
|
||||
};
|
||||
// Disk added successfully
|
||||
return {
|
||||
text: `Disk was added to the array in slot ${slot}.`,
|
||||
json: array,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { getServerIdentifier } from '@app/core/utils/server-identifier';
|
||||
import {
|
||||
ArrayDiskType,
|
||||
type ArrayCapacity,
|
||||
type ArrayType,
|
||||
} from '@app/graphql/generated/api/types';
|
||||
import { store } from '@app/store/index';
|
||||
import { FileLoadStatus } from '@app/store/types';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import sum from 'lodash/sum';
|
||||
|
||||
import type { ArrayCapacity, ArrayType } from '@app/graphql/generated/api/types';
|
||||
import { getServerIdentifier } from '@app/core/utils/server-identifier';
|
||||
import { ArrayDiskType } from '@app/graphql/generated/api/types';
|
||||
import { store } from '@app/store/index';
|
||||
import { FileLoadStatus } from '@app/store/types';
|
||||
|
||||
export const getArrayData = (getState = store.getState): ArrayType => {
|
||||
// Var state isn't loaded
|
||||
const state = getState();
|
||||
@@ -17,9 +15,7 @@ export const getArrayData = (getState = store.getState): ArrayType => {
|
||||
state.emhttp.status !== FileLoadStatus.LOADED ||
|
||||
Object.keys(state.emhttp.var).length === 0
|
||||
) {
|
||||
throw new GraphQLError(
|
||||
'Attempt to get Array Data, but state was not loaded'
|
||||
);
|
||||
throw new GraphQLError('Attempt to get Array Data, but state was not loaded');
|
||||
}
|
||||
|
||||
const { emhttp } = state;
|
||||
@@ -29,9 +25,7 @@ export const getArrayData = (getState = store.getState): ArrayType => {
|
||||
|
||||
// Array boot/parities/disks/caches
|
||||
const boot = allDisks.find((disk) => disk.type === ArrayDiskType.FLASH);
|
||||
const parities = allDisks.filter(
|
||||
(disk) => disk.type === ArrayDiskType.PARITY
|
||||
);
|
||||
const parities = allDisks.filter((disk) => disk.type === ArrayDiskType.PARITY);
|
||||
const disks = allDisks.filter((disk) => disk.type === ArrayDiskType.DATA);
|
||||
const caches = allDisks.filter((disk) => disk.type === ArrayDiskType.CACHE);
|
||||
// Disk sizes
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { type CoreContext, type CoreResult } from '@app/core/types';
|
||||
import { ArrayRunningError } from '@app/core/errors/array-running-error';
|
||||
import { FieldMissingError } from '@app/core/errors/field-missing-error';
|
||||
import { getArrayData } from '@app/core/modules/array/get-array-data';
|
||||
import { type CoreContext, type CoreResult } from '@app/core/types';
|
||||
import { arrayIsRunning } from '@app/core/utils/array/array-is-running';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
import { hasFields } from '@app/core/utils/validation/has-fields';
|
||||
import { arrayIsRunning } from '@app/core/utils/array/array-is-running';
|
||||
import { ArrayRunningError } from '@app/core/errors/array-running-error';
|
||||
import { getArrayData } from '@app/core/modules/array/get-array-data';
|
||||
|
||||
interface Context extends CoreContext {
|
||||
data: {
|
||||
/** The slot the disk is in. */
|
||||
slot: string;
|
||||
};
|
||||
data: {
|
||||
/** The slot the disk is in. */
|
||||
slot: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -18,37 +18,37 @@ interface Context extends CoreContext {
|
||||
* @returns The updated array.
|
||||
*/
|
||||
export const removeDiskFromArray = async (context: Context): Promise<CoreResult> => {
|
||||
const { data, user } = context;
|
||||
const { data, user } = context;
|
||||
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'array',
|
||||
action: 'create',
|
||||
possession: 'any',
|
||||
});
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'array',
|
||||
action: 'create',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
const missingFields = hasFields(data, ['id']);
|
||||
const missingFields = hasFields(data, ['id']);
|
||||
|
||||
if (missingFields.length !== 0) {
|
||||
// Only log first error
|
||||
throw new FieldMissingError(missingFields[0]);
|
||||
}
|
||||
if (missingFields.length !== 0) {
|
||||
// Only log first error
|
||||
throw new FieldMissingError(missingFields[0]);
|
||||
}
|
||||
|
||||
if (arrayIsRunning()) {
|
||||
throw new ArrayRunningError();
|
||||
}
|
||||
if (arrayIsRunning()) {
|
||||
throw new ArrayRunningError();
|
||||
}
|
||||
|
||||
const { slot } = data;
|
||||
const { slot } = data;
|
||||
|
||||
// Error removing disk
|
||||
// if () {
|
||||
// }
|
||||
// Error removing disk
|
||||
// if () {
|
||||
// }
|
||||
|
||||
const array = getArrayData()
|
||||
const array = getArrayData();
|
||||
|
||||
// Disk removed successfully
|
||||
return {
|
||||
text: `Disk was removed from the array in slot ${slot}.`,
|
||||
json: array,
|
||||
};
|
||||
// Disk removed successfully
|
||||
return {
|
||||
text: `Disk was removed from the array in slot ${slot}.`,
|
||||
json: array,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { CoreContext, CoreResult } from '@app/core/types';
|
||||
import { AppError } from '@app/core/errors/app-error';
|
||||
import { FieldMissingError } from '@app/core/errors/field-missing-error';
|
||||
import { ParamInvalidError } from '@app/core/errors/param-invalid-error';
|
||||
import { getArrayData } from '@app/core/modules/array/get-array-data';
|
||||
import { arrayIsRunning } from '@app/core/utils/array/array-is-running';
|
||||
import { emcmd } from '@app/core/utils/clients/emcmd';
|
||||
import { uppercaseFirstChar } from '@app/core/utils/misc/uppercase-first-char';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
import { hasFields } from '@app/core/utils/validation/has-fields';
|
||||
import { arrayIsRunning } from '@app/core/utils/array/array-is-running';
|
||||
import { emcmd } from '@app/core/utils/clients/emcmd';
|
||||
import { FieldMissingError } from '@app/core/errors/field-missing-error';
|
||||
import { ParamInvalidError } from '@app/core/errors/param-invalid-error';
|
||||
import { AppError } from '@app/core/errors/app-error';
|
||||
import { getArrayData } from '@app/core/modules/array/get-array-data';
|
||||
|
||||
// @TODO: Fix this not working across node apps
|
||||
// each app has it's own lock since the var is scoped
|
||||
@@ -15,57 +15,57 @@ import { getArrayData } from '@app/core/modules/array/get-array-data';
|
||||
let locked = false;
|
||||
|
||||
export const updateArray = async (context: CoreContext): Promise<CoreResult> => {
|
||||
const { data = {}, user } = context;
|
||||
const { data = {}, user } = context;
|
||||
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'array',
|
||||
action: 'update',
|
||||
possession: 'any',
|
||||
});
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'array',
|
||||
action: 'update',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
const missingFields = hasFields(data, ['state']);
|
||||
const missingFields = hasFields(data, ['state']);
|
||||
|
||||
if (missingFields.length !== 0) {
|
||||
// Only log first error
|
||||
throw new FieldMissingError(missingFields[0]);
|
||||
}
|
||||
if (missingFields.length !== 0) {
|
||||
// Only log first error
|
||||
throw new FieldMissingError(missingFields[0]);
|
||||
}
|
||||
|
||||
const { state: nextState } = data;
|
||||
const startState = arrayIsRunning() ? 'started' : 'stopped';
|
||||
const pendingState = nextState === 'stop' ? 'stopping' : 'starting';
|
||||
const { state: nextState } = data;
|
||||
const startState = arrayIsRunning() ? 'started' : 'stopped';
|
||||
const pendingState = nextState === 'stop' ? 'stopping' : 'starting';
|
||||
|
||||
if (!['start', 'stop'].includes(nextState)) {
|
||||
throw new ParamInvalidError('state', nextState);
|
||||
}
|
||||
if (!['start', 'stop'].includes(nextState)) {
|
||||
throw new ParamInvalidError('state', nextState);
|
||||
}
|
||||
|
||||
// Prevent this running multiple times at once
|
||||
if (locked) {
|
||||
throw new AppError('Array state is still being updated.');
|
||||
}
|
||||
// Prevent this running multiple times at once
|
||||
if (locked) {
|
||||
throw new AppError('Array state is still being updated.');
|
||||
}
|
||||
|
||||
// Prevent starting/stopping array when it's already in the same state
|
||||
if ((arrayIsRunning() && nextState === 'start') || (!arrayIsRunning() && nextState === 'stop')) {
|
||||
throw new AppError(`The array is already ${startState}`);
|
||||
}
|
||||
// Prevent starting/stopping array when it's already in the same state
|
||||
if ((arrayIsRunning() && nextState === 'start') || (!arrayIsRunning() && nextState === 'stop')) {
|
||||
throw new AppError(`The array is already ${startState}`);
|
||||
}
|
||||
|
||||
// Set lock then start/stop array
|
||||
locked = true;
|
||||
const command = {
|
||||
[`cmd${uppercaseFirstChar(nextState)}`]: uppercaseFirstChar(nextState),
|
||||
startState: startState.toUpperCase(),
|
||||
};
|
||||
// Set lock then start/stop array
|
||||
locked = true;
|
||||
const command = {
|
||||
[`cmd${uppercaseFirstChar(nextState)}`]: uppercaseFirstChar(nextState),
|
||||
startState: startState.toUpperCase(),
|
||||
};
|
||||
|
||||
// `await` has to be used otherwise the catch
|
||||
// will finish after the return statement below
|
||||
await emcmd(command).finally(() => {
|
||||
locked = false;
|
||||
});
|
||||
// `await` has to be used otherwise the catch
|
||||
// will finish after the return statement below
|
||||
await emcmd(command).finally(() => {
|
||||
locked = false;
|
||||
});
|
||||
|
||||
// Get new array JSON
|
||||
const array = getArrayData()
|
||||
// Get new array JSON
|
||||
const array = getArrayData();
|
||||
|
||||
/**
|
||||
/**
|
||||
* Update array details
|
||||
*
|
||||
* @memberof Core
|
||||
@@ -76,13 +76,13 @@ export const updateArray = async (context: CoreContext): Promise<CoreResult> =>
|
||||
* @param {State~User} context.user The current user.
|
||||
* @returns {Core~Result} The updated array.
|
||||
*/
|
||||
return {
|
||||
text: `Array was ${startState}, ${pendingState}.`,
|
||||
json: {
|
||||
...array.json,
|
||||
state: nextState === 'start' ? 'started' : 'stopped',
|
||||
previousState: startState,
|
||||
pendingState,
|
||||
},
|
||||
};
|
||||
return {
|
||||
text: `Array was ${startState}, ${pendingState}.`,
|
||||
json: {
|
||||
...array.json,
|
||||
state: nextState === 'start' ? 'started' : 'stopped',
|
||||
previousState: startState,
|
||||
pendingState,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { CoreContext, CoreResult } from '@app/core/types';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
import { emcmd } from '@app/core/utils/clients/emcmd';
|
||||
import { FieldMissingError } from '@app/core/errors/field-missing-error';
|
||||
import { ParamInvalidError } from '@app/core/errors/param-invalid-error';
|
||||
import { emcmd } from '@app/core/utils/clients/emcmd';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
import { getters } from '@app/store';
|
||||
|
||||
type State = 'start' | 'cancel' | 'resume' | 'cancel';
|
||||
|
||||
interface Context extends CoreContext {
|
||||
data: {
|
||||
state?: State;
|
||||
correct?: boolean;
|
||||
};
|
||||
data: {
|
||||
state?: State;
|
||||
correct?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,60 +19,60 @@ interface Context extends CoreContext {
|
||||
* @returns The update array.
|
||||
*/
|
||||
export const updateParityCheck = async (context: Context): Promise<CoreResult> => {
|
||||
const { user, data } = context;
|
||||
const { user, data } = context;
|
||||
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'array',
|
||||
action: 'update',
|
||||
possession: 'any',
|
||||
});
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'array',
|
||||
action: 'update',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
// Validation
|
||||
if (!data.state) {
|
||||
throw new FieldMissingError('state');
|
||||
}
|
||||
// Validation
|
||||
if (!data.state) {
|
||||
throw new FieldMissingError('state');
|
||||
}
|
||||
|
||||
const { state: wantedState } = data;
|
||||
const emhttp = getters.emhttp();
|
||||
const running = emhttp.var.mdResync !== 0;
|
||||
const states = {
|
||||
pause: {
|
||||
cmdNoCheck: 'Pause',
|
||||
},
|
||||
resume: {
|
||||
cmdCheck: 'Resume',
|
||||
},
|
||||
cancel: {
|
||||
cmdNoCheck: 'Cancel',
|
||||
},
|
||||
start: {
|
||||
cmdCheck: 'Check',
|
||||
},
|
||||
};
|
||||
const { state: wantedState } = data;
|
||||
const emhttp = getters.emhttp();
|
||||
const running = emhttp.var.mdResync !== 0;
|
||||
const states = {
|
||||
pause: {
|
||||
cmdNoCheck: 'Pause',
|
||||
},
|
||||
resume: {
|
||||
cmdCheck: 'Resume',
|
||||
},
|
||||
cancel: {
|
||||
cmdNoCheck: 'Cancel',
|
||||
},
|
||||
start: {
|
||||
cmdCheck: 'Check',
|
||||
},
|
||||
};
|
||||
|
||||
let allowedStates = Object.keys(states);
|
||||
let allowedStates = Object.keys(states);
|
||||
|
||||
// Only allow starting a check if there isn't already one running
|
||||
if (running) {
|
||||
allowedStates = allowedStates.splice(allowedStates.indexOf('start'), 1);
|
||||
}
|
||||
// Only allow starting a check if there isn't already one running
|
||||
if (running) {
|
||||
allowedStates = allowedStates.splice(allowedStates.indexOf('start'), 1);
|
||||
}
|
||||
|
||||
// Only allow states from states object
|
||||
if (!allowedStates.includes(wantedState)) {
|
||||
throw new ParamInvalidError('state', wantedState);
|
||||
}
|
||||
// Only allow states from states object
|
||||
if (!allowedStates.includes(wantedState)) {
|
||||
throw new ParamInvalidError('state', wantedState);
|
||||
}
|
||||
|
||||
// Should we write correction to the parity during the check
|
||||
const writeCorrectionsToParity = wantedState === 'start' && data.correct;
|
||||
// Should we write correction to the parity during the check
|
||||
const writeCorrectionsToParity = wantedState === 'start' && data.correct;
|
||||
|
||||
await emcmd({
|
||||
startState: 'STARTED',
|
||||
...states[wantedState],
|
||||
...(writeCorrectionsToParity ? { optionCorrect: 'correct' } : {}),
|
||||
});
|
||||
await emcmd({
|
||||
startState: 'STARTED',
|
||||
...states[wantedState],
|
||||
...(writeCorrectionsToParity ? { optionCorrect: 'correct' } : {}),
|
||||
});
|
||||
|
||||
return {
|
||||
json: {},
|
||||
};
|
||||
return {
|
||||
json: {},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { type CoreContext, type CoreResult } from '@app/core/types';
|
||||
* Get internal context object.
|
||||
*/
|
||||
export const getContext = (context: CoreContext): CoreResult => ({
|
||||
text: `Context: ${JSON.stringify(context, null, 2)}`,
|
||||
json: context,
|
||||
html: `<h1>Context</h1>\n<pre>${JSON.stringify(context, null, 2)}</pre>`,
|
||||
text: `Context: ${JSON.stringify(context, null, 2)}`,
|
||||
json: context,
|
||||
html: `<h1>Context</h1>\n<pre>${JSON.stringify(context, null, 2)}</pre>`,
|
||||
});
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
import { type CoreContext, type CoreResult } from '@app/core/types';
|
||||
import { AppError } from '@app/core/errors/app-error';
|
||||
import { type CoreContext, type CoreResult } from '@app/core/types';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
|
||||
interface Context extends CoreContext {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single disk.
|
||||
*/
|
||||
export const getDisk = async (context: Context, Disks): Promise<CoreResult> => {
|
||||
const { params, user } = context;
|
||||
const { params, user } = context;
|
||||
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'disk',
|
||||
action: 'read',
|
||||
possession: 'any',
|
||||
});
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'disk',
|
||||
action: 'read',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
const { id } = params;
|
||||
const disk = await Disks.findOne({ id });
|
||||
const { id } = params;
|
||||
const disk = await Disks.findOne({ id });
|
||||
|
||||
if (!disk) {
|
||||
throw new AppError(`No disk found matching ${id}`, 404);
|
||||
}
|
||||
if (!disk) {
|
||||
throw new AppError(`No disk found matching ${id}`, 404);
|
||||
}
|
||||
|
||||
return {
|
||||
text: `Disk: ${JSON.stringify(disk, null, 2)}`,
|
||||
json: disk,
|
||||
};
|
||||
return {
|
||||
text: `Disk: ${JSON.stringify(disk, null, 2)}`,
|
||||
json: disk,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import fs from 'fs';
|
||||
import camelCaseKeys from 'camelcase-keys';
|
||||
import { catchHandlers } from '@app/core/utils/misc/catch-handlers';
|
||||
import { getters, store } from '@app/store';
|
||||
import { updateDockerState } from '@app/store/modules/docker'
|
||||
|
||||
import {
|
||||
type ContainerPort,
|
||||
ContainerPortType,
|
||||
type DockerContainer,
|
||||
ContainerState,
|
||||
} from '@app/graphql/generated/api/types';
|
||||
import camelCaseKeys from 'camelcase-keys';
|
||||
|
||||
import type { ContainerPort, DockerContainer } from '@app/graphql/generated/api/types';
|
||||
import { dockerLogger } from '@app/core/log';
|
||||
import { docker } from '@app/core/utils/clients/docker';
|
||||
import { catchHandlers } from '@app/core/utils/misc/catch-handlers';
|
||||
import { ContainerPortType, ContainerState } from '@app/graphql/generated/api/types';
|
||||
import { getters, store } from '@app/store';
|
||||
import { updateDockerState } from '@app/store/modules/docker';
|
||||
|
||||
/**
|
||||
* Get all Docker containers.
|
||||
@@ -21,7 +18,7 @@ import { docker } from '@app/core/utils/clients/docker';
|
||||
export const getDockerContainers = async (
|
||||
{ useCache } = { useCache: true }
|
||||
): Promise<Array<DockerContainer>> => {
|
||||
const dockerState = getters.docker()
|
||||
const dockerState = getters.docker();
|
||||
if (useCache && dockerState.containers) {
|
||||
dockerLogger.trace('Using docker container cache');
|
||||
return dockerState.containers;
|
||||
@@ -45,43 +42,36 @@ export const getDockerContainers = async (
|
||||
all: true,
|
||||
size: true,
|
||||
})
|
||||
.then((containers) =>
|
||||
containers.map((object) => camelCaseKeys(object, { deep: true }))
|
||||
)
|
||||
.then((containers) => containers.map((object) => camelCaseKeys(object, { deep: true })))
|
||||
// If docker throws an error return no containers
|
||||
.catch(catchHandlers.docker);
|
||||
|
||||
// Cleanup container object
|
||||
const containers: Array<DockerContainer> = rawContainers.map<DockerContainer>(
|
||||
(container) => {
|
||||
const names = container.names[0];
|
||||
const containerData: DockerContainer = {
|
||||
...container,
|
||||
labels: container.labels,
|
||||
// @ts-expect-error sizeRootFs is not on the dockerode type, but is fetched when size: true is set
|
||||
sizeRootFs: container.sizeRootFs ?? undefined,
|
||||
imageId: container.imageID,
|
||||
state:
|
||||
typeof container?.state === 'string'
|
||||
? ContainerState[container.state.toUpperCase()] ??
|
||||
ContainerState.EXITED
|
||||
: ContainerState.EXITED,
|
||||
autoStart: autoStarts.includes(names.split('/')[1]),
|
||||
ports: container.ports.map<ContainerPort>((port) => ({
|
||||
...port,
|
||||
type: ContainerPortType[port.type.toUpperCase()],
|
||||
})),
|
||||
};
|
||||
return containerData;
|
||||
}
|
||||
);
|
||||
const containers: Array<DockerContainer> = rawContainers.map<DockerContainer>((container) => {
|
||||
const names = container.names[0];
|
||||
const containerData: DockerContainer = {
|
||||
...container,
|
||||
labels: container.labels,
|
||||
// @ts-expect-error sizeRootFs is not on the dockerode type, but is fetched when size: true is set
|
||||
sizeRootFs: container.sizeRootFs ?? undefined,
|
||||
imageId: container.imageID,
|
||||
state:
|
||||
typeof container?.state === 'string'
|
||||
? (ContainerState[container.state.toUpperCase()] ?? ContainerState.EXITED)
|
||||
: ContainerState.EXITED,
|
||||
autoStart: autoStarts.includes(names.split('/')[1]),
|
||||
ports: container.ports.map<ContainerPort>((port) => ({
|
||||
...port,
|
||||
type: ContainerPortType[port.type.toUpperCase()],
|
||||
})),
|
||||
};
|
||||
return containerData;
|
||||
});
|
||||
|
||||
// Get all of the current containers
|
||||
const installed = containers.length;
|
||||
const running = containers.filter(
|
||||
(container) => container.state === ContainerState.RUNNING
|
||||
).length;
|
||||
const running = containers.filter((container) => container.state === ContainerState.RUNNING).length;
|
||||
|
||||
store.dispatch(updateDockerState({ containers, installed, running }))
|
||||
store.dispatch(updateDockerState({ containers, installed, running }));
|
||||
return containers;
|
||||
};
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
import camelCaseKeys from 'camelcase-keys';
|
||||
import { docker } from '@app/core/utils';
|
||||
|
||||
import { type CoreContext, type CoreResult } from '@app/core/types';
|
||||
import { docker } from '@app/core/utils';
|
||||
import { catchHandlers } from '@app/core/utils/misc/catch-handlers';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
|
||||
export const getDockerNetworks = async (context: CoreContext): Promise<CoreResult> => {
|
||||
const { user } = context;
|
||||
const { user } = context;
|
||||
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'docker/network',
|
||||
action: 'read',
|
||||
possession: 'any',
|
||||
});
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'docker/network',
|
||||
action: 'read',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
const networks = await docker.listNetworks()
|
||||
// If docker throws an error return no networks
|
||||
.catch(catchHandlers.docker)
|
||||
.then((networks = []) => networks.map(object => camelCaseKeys(object, { deep: true })));
|
||||
const networks = await docker
|
||||
.listNetworks()
|
||||
// If docker throws an error return no networks
|
||||
.catch(catchHandlers.docker)
|
||||
.then((networks = []) => networks.map((object) => camelCaseKeys(object, { deep: true })));
|
||||
|
||||
/**
|
||||
* Get all Docker networks
|
||||
*
|
||||
* @memberof Core
|
||||
* @module docker/get-networks
|
||||
* @param {Core~Context} context
|
||||
* @returns {Core~Result} All the in/active Docker networks on the system.
|
||||
*/
|
||||
return {
|
||||
text: `Networks: ${JSON.stringify(networks, null, 2)}`,
|
||||
json: networks,
|
||||
};
|
||||
/**
|
||||
* Get all Docker networks
|
||||
*
|
||||
* @memberof Core
|
||||
* @module docker/get-networks
|
||||
* @param {Core~Context} context
|
||||
* @returns {Core~Result} All the in/active Docker networks on the system.
|
||||
*/
|
||||
return {
|
||||
text: `Networks: ${JSON.stringify(networks, null, 2)}`,
|
||||
json: networks,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,10 +4,10 @@ import type { CoreResult } from '@app/core/types';
|
||||
* Get all apps.
|
||||
*/
|
||||
export const getApps = async (): Promise<CoreResult> => {
|
||||
const apps = [];
|
||||
const apps = [];
|
||||
|
||||
return {
|
||||
text: `Apps: ${JSON.stringify(apps, null, 2)}`,
|
||||
json: apps,
|
||||
};
|
||||
return {
|
||||
text: `Apps: ${JSON.stringify(apps, null, 2)}`,
|
||||
json: apps,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CoreResult, CoreContext } from '@app/core/types';
|
||||
import type { CoreContext, CoreResult } from '@app/core/types';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
|
||||
/**
|
||||
@@ -6,15 +6,15 @@ import { ensurePermission } from '@app/core/utils/permissions/ensure-permission'
|
||||
* @returns All currently connected devices.
|
||||
*/
|
||||
export const getDevices = async (context: CoreContext): Promise<CoreResult> => {
|
||||
const { user } = context;
|
||||
const { user } = context;
|
||||
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'device',
|
||||
action: 'read',
|
||||
possession: 'any',
|
||||
});
|
||||
/*
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'device',
|
||||
action: 'read',
|
||||
possession: 'any',
|
||||
});
|
||||
/*
|
||||
Const { devices } = getters.emhttp();
|
||||
|
||||
return {
|
||||
@@ -22,8 +22,8 @@ export const getDevices = async (context: CoreContext): Promise<CoreResult> => {
|
||||
json: devices,
|
||||
};
|
||||
*/
|
||||
return {
|
||||
text: 'Disabled Due To Bug With Devs Sub',
|
||||
json: {},
|
||||
};
|
||||
return {
|
||||
text: 'Disabled Due To Bug With Devs Sub',
|
||||
json: {},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
import type { Systeminformation } from 'systeminformation';
|
||||
import { execa } from 'execa';
|
||||
import {
|
||||
type Systeminformation,
|
||||
blockDevices,
|
||||
diskLayout,
|
||||
} from 'systeminformation';
|
||||
import {
|
||||
type Disk,
|
||||
DiskInterfaceType,
|
||||
DiskSmartStatus,
|
||||
DiskFsType,
|
||||
} from '@app/graphql/generated/api/types';
|
||||
import { graphqlLogger } from '@app/core/log';
|
||||
import { blockDevices, diskLayout } from 'systeminformation';
|
||||
|
||||
const getTemperature = async (
|
||||
disk: Systeminformation.DiskLayoutData
|
||||
): Promise<number> => {
|
||||
import type { Disk } from '@app/graphql/generated/api/types';
|
||||
import { graphqlLogger } from '@app/core/log';
|
||||
import { DiskFsType, DiskInterfaceType, DiskSmartStatus } from '@app/graphql/generated/api/types';
|
||||
|
||||
const getTemperature = async (disk: Systeminformation.DiskLayoutData): Promise<number> => {
|
||||
try {
|
||||
const stdout = await execa('smartctl', ['-A', disk.device])
|
||||
.then(({ stdout }) => stdout)
|
||||
@@ -23,9 +15,7 @@ const getTemperature = async (
|
||||
const header = lines.find((line) => line.startsWith('ID#')) ?? '';
|
||||
const fields = lines.splice(lines.indexOf(header) + 1, lines.length);
|
||||
const field = fields.find(
|
||||
(line) =>
|
||||
line.includes('Temperature_Celsius') ||
|
||||
line.includes('Airflow_Temperature_Cel')
|
||||
(line) => line.includes('Temperature_Celsius') || line.includes('Airflow_Temperature_Cel')
|
||||
);
|
||||
|
||||
if (!field) {
|
||||
@@ -33,10 +23,7 @@ const getTemperature = async (
|
||||
}
|
||||
|
||||
if (field.includes('Min/Max')) {
|
||||
return Number.parseInt(
|
||||
field.split(' - ')[1].trim().split(' ')[0],
|
||||
10
|
||||
);
|
||||
return Number.parseInt(field.split(' - ')[1].trim().split(' ')[0], 10);
|
||||
}
|
||||
|
||||
const line = field.split(' ');
|
||||
@@ -54,9 +41,7 @@ const parseDisk = async (
|
||||
): Promise<Disk> => {
|
||||
const partitions = partitionsToParse
|
||||
// Only get partitions from this disk
|
||||
.filter((partition) =>
|
||||
partition.name.startsWith(disk.device.split('/dev/')[1])
|
||||
)
|
||||
.filter((partition) => partition.name.startsWith(disk.device.split('/dev/')[1]))
|
||||
// Remove unneeded fields
|
||||
.map(({ name, fsType, size }) => ({
|
||||
name,
|
||||
@@ -82,18 +67,14 @@ const parseDisk = async (
|
||||
/**
|
||||
* Get all disks.
|
||||
*/
|
||||
export const getDisks = async (
|
||||
options?: { temperature: boolean }
|
||||
): Promise<Disk[]> => {
|
||||
export const getDisks = async (options?: { temperature: boolean }): Promise<Disk[]> => {
|
||||
// Return all fields but temperature
|
||||
if (options?.temperature === false) {
|
||||
const partitions = await blockDevices().then((devices) =>
|
||||
devices.filter((device) => device.type === 'part')
|
||||
);
|
||||
const diskLayoutData = await diskLayout();
|
||||
const disks = await Promise.all(
|
||||
diskLayoutData.map((disk) => parseDisk(disk, partitions))
|
||||
);
|
||||
const disks = await Promise.all(diskLayoutData.map((disk) => parseDisk(disk, partitions)));
|
||||
|
||||
return disks;
|
||||
}
|
||||
@@ -101,9 +82,7 @@ export const getDisks = async (
|
||||
const partitions = await blockDevices().then((devices) =>
|
||||
devices.filter((device) => device.type === 'part')
|
||||
);
|
||||
const disks = await asyncMap(await diskLayout(), async (disk) =>
|
||||
parseDisk(disk, partitions, true)
|
||||
);
|
||||
const disks = await asyncMap(await diskLayout(), async (disk) => parseDisk(disk, partitions, true));
|
||||
|
||||
return disks;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { promises as fs } from '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';
|
||||
|
||||
import Table from 'cli-table';
|
||||
|
||||
import { FileMissingError } from '@app/core/errors/file-missing-error';
|
||||
import { type CoreContext, type CoreResult } from '@app/core/types';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
import { getters } from '@app/store';
|
||||
|
||||
/**
|
||||
@@ -10,50 +12,50 @@ import { getters } from '@app/store';
|
||||
* @returns All parity checks with their respective date, duration, speed, status and errors.
|
||||
*/
|
||||
export const getParityHistory = async (context: CoreContext): Promise<CoreResult> => {
|
||||
const { user } = context;
|
||||
const { user } = context;
|
||||
|
||||
// Bail if the user doesn't have permission
|
||||
ensurePermission(user, {
|
||||
resource: 'parity-history',
|
||||
action: 'read',
|
||||
possession: 'any',
|
||||
});
|
||||
// Bail if the user doesn't have permission
|
||||
ensurePermission(user, {
|
||||
resource: 'parity-history',
|
||||
action: 'read',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
const historyFilePath = getters.paths()['parity-checks'];
|
||||
const history = await fs.readFile(historyFilePath).catch(() => {
|
||||
throw new FileMissingError(historyFilePath);
|
||||
});
|
||||
const historyFilePath = getters.paths()['parity-checks'];
|
||||
const history = await fs.readFile(historyFilePath).catch(() => {
|
||||
throw new FileMissingError(historyFilePath);
|
||||
});
|
||||
|
||||
// Convert checks into array of objects
|
||||
const lines = history.toString().trim().split('\n').reverse();
|
||||
const parityChecks = lines.map(line => {
|
||||
const [date, duration, speed, status, errors = '0'] = line.split('|');
|
||||
return {
|
||||
date,
|
||||
duration: Number.parseInt(duration, 10),
|
||||
speed,
|
||||
status,
|
||||
errors: Number.parseInt(errors, 10),
|
||||
};
|
||||
});
|
||||
// Convert checks into array of objects
|
||||
const lines = history.toString().trim().split('\n').reverse();
|
||||
const parityChecks = lines.map((line) => {
|
||||
const [date, duration, speed, status, errors = '0'] = line.split('|');
|
||||
return {
|
||||
date,
|
||||
duration: Number.parseInt(duration, 10),
|
||||
speed,
|
||||
status,
|
||||
errors: Number.parseInt(errors, 10),
|
||||
};
|
||||
});
|
||||
|
||||
// Create table for text output
|
||||
const table = new Table({
|
||||
head: ['Date', 'Duration', 'Speed', 'Status', 'Errors'],
|
||||
});
|
||||
// Update raw values with strings
|
||||
parityChecks.forEach(check => {
|
||||
const array = Object.values({
|
||||
...check,
|
||||
speed: check.speed ? check.speed : 'Unavailable',
|
||||
duration: check.duration >= 0 ? check.duration : 'Unavailable',
|
||||
status: check.status === '-4' ? 'Cancelled' : 'OK',
|
||||
});
|
||||
table.push(array);
|
||||
});
|
||||
// Create table for text output
|
||||
const table = new Table({
|
||||
head: ['Date', 'Duration', 'Speed', 'Status', 'Errors'],
|
||||
});
|
||||
// Update raw values with strings
|
||||
parityChecks.forEach((check) => {
|
||||
const array = Object.values({
|
||||
...check,
|
||||
speed: check.speed ? check.speed : 'Unavailable',
|
||||
duration: check.duration >= 0 ? check.duration : 'Unavailable',
|
||||
status: check.status === '-4' ? 'Cancelled' : 'OK',
|
||||
});
|
||||
table.push(array);
|
||||
});
|
||||
|
||||
return {
|
||||
text: table.toString(),
|
||||
json: parityChecks,
|
||||
};
|
||||
return {
|
||||
text: table.toString(),
|
||||
json: parityChecks,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getEmhttpdService } from '@app/core/modules/services/get-emhttpd';
|
||||
import type { CoreContext, CoreResult } from '@app/core/types';
|
||||
import { logger } from '@app/core/log';
|
||||
import type { CoreResult, CoreContext } from '@app/core/types';
|
||||
import { getEmhttpdService } from '@app/core/modules/services/get-emhttpd';
|
||||
import { getUnraidApiService } from '@app/core/modules/services/get-unraid-api';
|
||||
import { NODE_ENV } from '@app/environment';
|
||||
|
||||
|
||||
@@ -1,50 +1,53 @@
|
||||
import type { CoreContext, CoreResult } from '@app/core/types';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
import { AppError } from '@app/core/errors/app-error';
|
||||
import { getters } from '@app/store';
|
||||
import { type User } from '@app/core/types/states/user';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
import { getters } from '@app/store';
|
||||
|
||||
interface Context extends CoreContext {
|
||||
query: {
|
||||
/** Should all fields be returned? */
|
||||
slim: string;
|
||||
};
|
||||
query: {
|
||||
/** Should all fields be returned? */
|
||||
slim: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all users.
|
||||
*/
|
||||
export const getUsers = async (context: Context): Promise<CoreResult> => {
|
||||
const { query, user } = context;
|
||||
const { query, user } = context;
|
||||
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'user',
|
||||
action: 'read',
|
||||
possession: 'any',
|
||||
});
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'user',
|
||||
action: 'read',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
// Default to only showing limited fields
|
||||
const { slim = 'true' } = query;
|
||||
const { users } = getters.emhttp();
|
||||
// Default to only showing limited fields
|
||||
const { slim = 'true' } = query;
|
||||
const { users } = getters.emhttp();
|
||||
|
||||
if (users.length === 0) {
|
||||
// This is likely a new install or something went horribly wrong
|
||||
throw new AppError('No users found.', 404);
|
||||
}
|
||||
if (users.length === 0) {
|
||||
// This is likely a new install or something went horribly wrong
|
||||
throw new AppError('No users found.', 404);
|
||||
}
|
||||
|
||||
const result = slim === 'true' ? users.map((user: User) => {
|
||||
const { id, name, description, role } = user;
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
role,
|
||||
};
|
||||
}) : users;
|
||||
const result =
|
||||
slim === 'true'
|
||||
? users.map((user: User) => {
|
||||
const { id, name, description, role } = user;
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
role,
|
||||
};
|
||||
})
|
||||
: users;
|
||||
|
||||
return {
|
||||
text: `Users: ${JSON.stringify(result, null, 2)}`,
|
||||
json: result,
|
||||
};
|
||||
return {
|
||||
text: `Users: ${JSON.stringify(result, null, 2)}`,
|
||||
json: result,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CoreContext, CoreResult } from '@app/core/types';
|
||||
import { getUnraidVersion } from '@app/common/dashboard/get-unraid-version';
|
||||
import type { CoreResult, CoreContext } from '@app/core/types';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
|
||||
/**
|
||||
@@ -7,22 +7,22 @@ import { ensurePermission } from '@app/core/utils/permissions/ensure-permission'
|
||||
* @returns Welcomes a user.
|
||||
*/
|
||||
export const getWelcome = async (context: CoreContext): Promise<CoreResult> => {
|
||||
const { user } = context;
|
||||
const { user } = context;
|
||||
|
||||
// Bail if the user doesn't have permission
|
||||
ensurePermission(user, {
|
||||
resource: 'welcome',
|
||||
action: 'read',
|
||||
possession: 'any',
|
||||
});
|
||||
// Bail if the user doesn't have permission
|
||||
ensurePermission(user, {
|
||||
resource: 'welcome',
|
||||
action: 'read',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
const version = await getUnraidVersion();
|
||||
const message = `Welcome ${user.name} to this Unraid ${version} server`;
|
||||
const version = await getUnraidVersion();
|
||||
const message = `Welcome ${user.name} to this Unraid ${version} server`;
|
||||
|
||||
return {
|
||||
text: message,
|
||||
json: {
|
||||
message,
|
||||
},
|
||||
};
|
||||
return {
|
||||
text: message,
|
||||
json: {
|
||||
message,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { join } from 'node:path';
|
||||
|
||||
import type { FSWatcher } from 'chokidar';
|
||||
import { watch } from 'chokidar';
|
||||
|
||||
import { getters, store } from '@app/store/index';
|
||||
import { clearNotification, loadNotification } from '@app/store/modules/notifications';
|
||||
import { FileLoadStatus } from '@app/store/types';
|
||||
import { type FSWatcher, watch } from 'chokidar';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const handleNotificationAdd = (path: string) => {
|
||||
store.dispatch(loadNotification({ path }));
|
||||
@@ -18,7 +21,7 @@ export const setupNotificationWatch = async (): Promise<FSWatcher | null> => {
|
||||
const { notify, status } = getters.dynamix();
|
||||
if (status === FileLoadStatus.LOADED && notify?.path) {
|
||||
if (watcher) {
|
||||
await watcher.close()
|
||||
await watcher.close();
|
||||
}
|
||||
watcher = watch(join(notify.path, 'unread'), {})
|
||||
.on('add', (path) => {
|
||||
|
||||
@@ -1,41 +1,42 @@
|
||||
import { execa } from 'execa';
|
||||
|
||||
import { type CoreContext, type CoreResult } from '@app/core/types';
|
||||
import { cleanStdout } from '@app/core/utils/misc/clean-stdout';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
|
||||
interface Result extends CoreResult {
|
||||
json: {
|
||||
online: boolean;
|
||||
uptime: number;
|
||||
};
|
||||
json: {
|
||||
online: boolean;
|
||||
uptime: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get emhttpd service info.
|
||||
*/
|
||||
export const getEmhttpdService = async (context: CoreContext): Promise<Result> => {
|
||||
const { user } = context;
|
||||
const { user } = context;
|
||||
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'service/emhttpd',
|
||||
action: 'read',
|
||||
possession: 'any',
|
||||
});
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'service/emhttpd',
|
||||
action: 'read',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
// Only get uptime if process is online
|
||||
const uptime = await execa('ps', ['-C', 'emhttpd', '-o', 'etimes', '--no-headers'])
|
||||
.then(cleanStdout)
|
||||
.then(uptime => Number.parseInt(uptime, 10))
|
||||
.catch(() => -1);
|
||||
// Only get uptime if process is online
|
||||
const uptime = await execa('ps', ['-C', 'emhttpd', '-o', 'etimes', '--no-headers'])
|
||||
.then(cleanStdout)
|
||||
.then((uptime) => Number.parseInt(uptime, 10))
|
||||
.catch(() => -1);
|
||||
|
||||
const online = uptime >= 1;
|
||||
const online = uptime >= 1;
|
||||
|
||||
return {
|
||||
text: `Online: ${online}\n Uptime: ${uptime}`,
|
||||
json: {
|
||||
online,
|
||||
uptime,
|
||||
},
|
||||
};
|
||||
return {
|
||||
text: `Online: ${online}\n Uptime: ${uptime}`,
|
||||
json: {
|
||||
online,
|
||||
uptime,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
import type { CoreContext, CoreResult } from '@app/core/types';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
import { API_VERSION } from '@app/environment';
|
||||
|
||||
interface Result extends CoreResult {
|
||||
json: {
|
||||
name: string;
|
||||
online: boolean;
|
||||
uptime: {
|
||||
timestamp: string;
|
||||
seconds: number;
|
||||
};
|
||||
};
|
||||
json: {
|
||||
name: string;
|
||||
online: boolean;
|
||||
uptime: {
|
||||
timestamp: string;
|
||||
seconds: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// When this service started
|
||||
@@ -20,29 +20,29 @@ const startTimestamp = new Date();
|
||||
* Get Unraid api service info.
|
||||
*/
|
||||
export const getUnraidApiService = async (context: CoreContext): Promise<Result> => {
|
||||
// Check permissions
|
||||
ensurePermission(context.user, {
|
||||
resource: 'service/unraid-api',
|
||||
action: 'read',
|
||||
possession: 'any',
|
||||
});
|
||||
// Check permissions
|
||||
ensurePermission(context.user, {
|
||||
resource: 'service/unraid-api',
|
||||
action: 'read',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const uptimeTimestamp = startTimestamp.toISOString();
|
||||
const uptimeSeconds = (now.getTime() - startTimestamp.getTime());
|
||||
const now = new Date();
|
||||
const uptimeTimestamp = startTimestamp.toISOString();
|
||||
const uptimeSeconds = now.getTime() - startTimestamp.getTime();
|
||||
|
||||
const service = {
|
||||
name: 'unraid-api',
|
||||
online: true,
|
||||
uptime: {
|
||||
timestamp: uptimeTimestamp,
|
||||
seconds: uptimeSeconds,
|
||||
},
|
||||
version: API_VERSION,
|
||||
};
|
||||
const service = {
|
||||
name: 'unraid-api',
|
||||
online: true,
|
||||
uptime: {
|
||||
timestamp: uptimeTimestamp,
|
||||
seconds: uptimeSeconds,
|
||||
},
|
||||
version: API_VERSION,
|
||||
};
|
||||
|
||||
return {
|
||||
text: `Service: ${JSON.stringify(service, null, 2)}`,
|
||||
json: service,
|
||||
};
|
||||
return {
|
||||
text: `Service: ${JSON.stringify(service, null, 2)}`,
|
||||
json: service,
|
||||
};
|
||||
};
|
||||
|
||||
15
api/src/core/modules/services/nginx.ts
Normal file
15
api/src/core/modules/services/nginx.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { execa } from 'execa';
|
||||
|
||||
import { logger } from '@app/core/log';
|
||||
|
||||
export class NginxManager {
|
||||
public reloadNginx = async () => {
|
||||
try {
|
||||
await execa('/etc/rc.d/rc.nginx', ['reload']);
|
||||
return true;
|
||||
} catch (err: unknown) {
|
||||
logger.warn('Failed to restart Nginx with error: ', err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
15
api/src/core/modules/services/update-dns.ts
Normal file
15
api/src/core/modules/services/update-dns.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { execa } from 'execa';
|
||||
|
||||
import { logger } from '@app/core/log';
|
||||
|
||||
export class UpdateDNSManager {
|
||||
public updateDNS = async () => {
|
||||
try {
|
||||
await execa('/usr/bin/php', ['/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php']);
|
||||
return true;
|
||||
} catch (err: unknown) {
|
||||
logger.warn('Failed to call Update DNS with error: ', err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,150 +1,142 @@
|
||||
import { type CoreContext, type CoreResult } from '@app/core/types';
|
||||
import { EmCmdError } from '@app/core/errors/em-cmd-error';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
import { emcmd } from '@app/core/utils/clients/emcmd';
|
||||
import { type CoreContext, type CoreResult } from '@app/core/types';
|
||||
import { type Var } from '@app/core/types/states/var';
|
||||
import { emcmd } from '@app/core/utils/clients/emcmd';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
import { getters } from '@app/store';
|
||||
|
||||
interface Context extends CoreContext {
|
||||
data: Var;
|
||||
data: Var;
|
||||
}
|
||||
|
||||
interface Result extends CoreResult {
|
||||
json: {
|
||||
mdwriteMethod?: number;
|
||||
startArray?: boolean;
|
||||
spindownDelay?: number;
|
||||
defaultFormat?: any;
|
||||
defaultFsType?: any;
|
||||
};
|
||||
json: {
|
||||
mdwriteMethod?: number;
|
||||
startArray?: boolean;
|
||||
spindownDelay?: number;
|
||||
defaultFormat?: any;
|
||||
defaultFsType?: any;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update disk settings.
|
||||
*/
|
||||
export const updateDisk = async (context: Context): Promise<Result> => {
|
||||
const { data, user } = context;
|
||||
const { data, user } = context;
|
||||
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'disk/settings',
|
||||
action: 'update',
|
||||
possession: 'any',
|
||||
});
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'disk/settings',
|
||||
action: 'update',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
/**
|
||||
* Check context.data[property] is using an allowed value.
|
||||
*
|
||||
* @param property The property of data to check values against.
|
||||
* @param allowedValues Which values which are allowed.
|
||||
* @param optional If the value can also be undefined.
|
||||
*/
|
||||
const check = (property: string, allowedValues: Record<string, string> | string[], optional = true): void => {
|
||||
const value = data[property];
|
||||
/**
|
||||
* Check context.data[property] is using an allowed value.
|
||||
*
|
||||
* @param property The property of data to check values against.
|
||||
* @param allowedValues Which values which are allowed.
|
||||
* @param optional If the value can also be undefined.
|
||||
*/
|
||||
const check = (
|
||||
property: string,
|
||||
allowedValues: Record<string, string> | string[],
|
||||
optional = true
|
||||
): void => {
|
||||
const value = data[property];
|
||||
|
||||
// Skip checking if the value isn't needed and it's not set
|
||||
if (optional && value === undefined) {
|
||||
return;
|
||||
}
|
||||
// Skip checking if the value isn't needed and it's not set
|
||||
if (optional && value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// AllowedValues is an object
|
||||
if (!Array.isArray(allowedValues)) {
|
||||
allowedValues = Object.keys(allowedValues);
|
||||
}
|
||||
// AllowedValues is an object
|
||||
if (!Array.isArray(allowedValues)) {
|
||||
allowedValues = Object.keys(allowedValues);
|
||||
}
|
||||
|
||||
if (!allowedValues.includes(value)) {
|
||||
throw new EmCmdError(property, value, allowedValues);
|
||||
}
|
||||
};
|
||||
if (!allowedValues.includes(value)) {
|
||||
throw new EmCmdError(property, value, allowedValues);
|
||||
}
|
||||
};
|
||||
|
||||
// If set to 'Yes' then if the device configuration is correct upon server start - up, the array will be automatically started and shares exported.
|
||||
// If set to 'No' then you must start the array yourself.
|
||||
check('startArray', ['yes', 'no']);
|
||||
// If set to 'Yes' then if the device configuration is correct upon server start - up, the array will be automatically started and shares exported.
|
||||
// If set to 'No' then you must start the array yourself.
|
||||
check('startArray', ['yes', 'no']);
|
||||
|
||||
// Define the 'default' time-out for spinning hard drives down after a period of no I/O activity.
|
||||
// You may also override the default value for an individual disk.
|
||||
check('spindownDelay', {
|
||||
// Define the 'default' time-out for spinning hard drives down after a period of no I/O activity.
|
||||
// You may also override the default value for an individual disk.
|
||||
check('spindownDelay', {
|
||||
0: 'Never',
|
||||
15: '15 minutes',
|
||||
30: '30 minutes',
|
||||
45: '45 minutes',
|
||||
1: '1 hour',
|
||||
2: '2 hours',
|
||||
3: '3 hours',
|
||||
4: '4 hours',
|
||||
5: '5 hours',
|
||||
6: '6 hours',
|
||||
7: '7 hours',
|
||||
8: '8 hours',
|
||||
9: '9 hours',
|
||||
});
|
||||
|
||||
0: 'Never',
|
||||
15: '15 minutes',
|
||||
30: '30 minutes',
|
||||
45: '45 minutes',
|
||||
1: '1 hour',
|
||||
2: '2 hours',
|
||||
3: '3 hours',
|
||||
4: '4 hours',
|
||||
5: '5 hours',
|
||||
6: '6 hours',
|
||||
7: '7 hours',
|
||||
8: '8 hours',
|
||||
9: '9 hours',
|
||||
|
||||
});
|
||||
// Defines the type of partition layout to create when formatting hard drives 2TB in size and smaller **only**. (All devices larger then 2TB are always set up with GPT partition tables.)
|
||||
// **MBR: unaligned** setting will create MBR-style partition table, where the single partition 1 will start in the **63rd sector** from the start of the disk. This is the *traditional* setting for virtually all MBR-style partition tables.
|
||||
// **MBR: 4K-aligned** setting will create an MBR-style partition table, where the single partition 1 will start in the **64th sector** from the start of the disk. Since the sector size is 512 bytes, this will *align* the start of partition 1 on a 4K-byte boundary. This is required for proper support of so-called *Advanced Format* drives.
|
||||
// Unless you have a specific requirement do not change this setting from the default **MBR: 4K-aligned**.
|
||||
check('defaultFormat', {
|
||||
1: 'MBR: unaligned',
|
||||
2: 'MBR: 4K-aligned',
|
||||
});
|
||||
|
||||
// Defines the type of partition layout to create when formatting hard drives 2TB in size and smaller **only**. (All devices larger then 2TB are always set up with GPT partition tables.)
|
||||
// **MBR: unaligned** setting will create MBR-style partition table, where the single partition 1 will start in the **63rd sector** from the start of the disk. This is the *traditional* setting for virtually all MBR-style partition tables.
|
||||
// **MBR: 4K-aligned** setting will create an MBR-style partition table, where the single partition 1 will start in the **64th sector** from the start of the disk. Since the sector size is 512 bytes, this will *align* the start of partition 1 on a 4K-byte boundary. This is required for proper support of so-called *Advanced Format* drives.
|
||||
// Unless you have a specific requirement do not change this setting from the default **MBR: 4K-aligned**.
|
||||
check('defaultFormat', {
|
||||
// Selects the method to employ when writing to enabled disk in parity protected array.
|
||||
check('writeMethod', {
|
||||
auto: 'Auto - read/modify/write',
|
||||
|
||||
1: 'MBR: unaligned',
|
||||
2: 'MBR: 4K-aligned',
|
||||
|
||||
});
|
||||
0: 'read/modify/write',
|
||||
1: 'reconstruct write',
|
||||
});
|
||||
|
||||
// Selects the method to employ when writing to enabled disk in parity protected array.
|
||||
check('writeMethod', {
|
||||
auto: 'Auto - read/modify/write',
|
||||
// Defines the default file system type to create when an * unmountable * array device is formatted.
|
||||
// The default file system type for a single or multi - device cache is always Btrfs.
|
||||
check('defaultFsType', {
|
||||
xfs: 'xfs',
|
||||
btrfs: 'btrfs',
|
||||
reiserfs: 'reiserfs',
|
||||
|
||||
0: 'read/modify/write',
|
||||
1: 'reconstruct write',
|
||||
|
||||
});
|
||||
'luks:xfs': 'xfs - encrypted',
|
||||
'luks:btrfs': 'btrfs - encrypted',
|
||||
'luks:reiserfs': 'reiserfs - encrypted',
|
||||
});
|
||||
|
||||
// Defines the default file system type to create when an * unmountable * array device is formatted.
|
||||
// The default file system type for a single or multi - device cache is always Btrfs.
|
||||
check('defaultFsType', {
|
||||
xfs: 'xfs',
|
||||
btrfs: 'btrfs',
|
||||
reiserfs: 'reiserfs',
|
||||
const { startArray, spindownDelay, defaultFormat, defaultFsType, mdWriteMethod } = data;
|
||||
|
||||
'luks:xfs': 'xfs - encrypted',
|
||||
'luks:btrfs': 'btrfs - encrypted',
|
||||
'luks:reiserfs': 'reiserfs - encrypted',
|
||||
|
||||
});
|
||||
await emcmd({
|
||||
startArray,
|
||||
spindownDelay,
|
||||
defaultFormat,
|
||||
defaultFsType,
|
||||
|
||||
const {
|
||||
startArray,
|
||||
spindownDelay,
|
||||
defaultFormat,
|
||||
defaultFsType,
|
||||
mdWriteMethod,
|
||||
} = data;
|
||||
md_write_method: mdWriteMethod,
|
||||
changeDisk: 'Apply',
|
||||
});
|
||||
|
||||
await emcmd({
|
||||
startArray,
|
||||
spindownDelay,
|
||||
defaultFormat,
|
||||
defaultFsType,
|
||||
const emhttp = getters.emhttp();
|
||||
|
||||
md_write_method: mdWriteMethod,
|
||||
changeDisk: 'Apply',
|
||||
});
|
||||
// @todo: return all disk settings
|
||||
const result = {
|
||||
mdwriteMethod: emhttp.var.mdWriteMethod,
|
||||
startArray: emhttp.var.startArray,
|
||||
spindownDelay: emhttp.var.spindownDelay,
|
||||
defaultFormat: emhttp.var.defaultFormat,
|
||||
defaultFsType: emhttp.var.defaultFormat,
|
||||
};
|
||||
|
||||
const emhttp = getters.emhttp();
|
||||
|
||||
// @todo: return all disk settings
|
||||
const result = {
|
||||
mdwriteMethod: emhttp.var.mdWriteMethod,
|
||||
startArray: emhttp.var.startArray,
|
||||
spindownDelay: emhttp.var.spindownDelay,
|
||||
defaultFormat: emhttp.var.defaultFormat,
|
||||
defaultFsType: emhttp.var.defaultFormat,
|
||||
};
|
||||
|
||||
return {
|
||||
text: `Disk settings: ${JSON.stringify(result, null, 2)}`,
|
||||
json: result,
|
||||
};
|
||||
return {
|
||||
text: `Disk settings: ${JSON.stringify(result, null, 2)}`,
|
||||
json: result,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,48 +1,45 @@
|
||||
import type { CoreContext, CoreResult } from '@app/core/types/global';
|
||||
import type { UserShare, DiskShare } from '@app/core/types/states/share';
|
||||
import type { DiskShare, UserShare } from '@app/core/types/states/share';
|
||||
import { AppError } from '@app/core/errors/app-error';
|
||||
import { getShares } from '@app/core/utils';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
|
||||
interface Context extends CoreContext {
|
||||
params: {
|
||||
/** Name of the share */
|
||||
name: string;
|
||||
};
|
||||
params: {
|
||||
/** Name of the share */
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface Result extends CoreResult {
|
||||
json: UserShare | DiskShare;
|
||||
json: UserShare | DiskShare;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get single share.
|
||||
*/
|
||||
export const getShare = async function (context: Context): Promise<Result> {
|
||||
const { params, user } = context;
|
||||
const { name } = params;
|
||||
const { params, user } = context;
|
||||
const { name } = params;
|
||||
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'share',
|
||||
action: 'read',
|
||||
possession: 'any',
|
||||
});
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'share',
|
||||
action: 'read',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
const userShare = getShares('user', { name });
|
||||
const diskShare = getShares('disk', { name });
|
||||
const userShare = getShares('user', { name });
|
||||
const diskShare = getShares('disk', { name });
|
||||
|
||||
const share = [
|
||||
userShare,
|
||||
diskShare,
|
||||
].filter(_ => _)[0];
|
||||
const share = [userShare, diskShare].filter((_) => _)[0];
|
||||
|
||||
if (!share) {
|
||||
throw new AppError('No share found with that name.', 404);
|
||||
}
|
||||
if (!share) {
|
||||
throw new AppError('No share found with that name.', 404);
|
||||
}
|
||||
|
||||
return {
|
||||
text: `Share: ${JSON.stringify(share, null, 2)}`,
|
||||
json: share,
|
||||
};
|
||||
return {
|
||||
text: `Share: ${JSON.stringify(share, null, 2)}`,
|
||||
json: share,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
import { type CoreContext, type CoreResult } from '@app/core/types';
|
||||
import { AppError } from '@app/core/errors/app-error';
|
||||
import { FieldMissingError } from '@app/core/errors/field-missing-error';
|
||||
import { type CoreContext, type CoreResult } from '@app/core/types';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
import { hasFields } from '@app/core/utils/validation/has-fields';
|
||||
import { FieldMissingError } from '@app/core/errors/field-missing-error';
|
||||
import { getters } from '@app/store';
|
||||
|
||||
interface Context extends CoreContext {
|
||||
params: {
|
||||
/** Name of user to add the role to. */
|
||||
name: string;
|
||||
};
|
||||
params: {
|
||||
/** Name of user to add the role to. */
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add role to user.
|
||||
*/
|
||||
export const addRole = async (context: Context): Promise<CoreResult> => {
|
||||
const { user, params } = context;
|
||||
const { user, params } = context;
|
||||
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'user',
|
||||
action: 'update',
|
||||
possession: 'any',
|
||||
});
|
||||
// Check permissions
|
||||
ensurePermission(user, {
|
||||
resource: 'user',
|
||||
action: 'update',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
// Validation
|
||||
const { name } = params;
|
||||
const missingFields = hasFields(params, ['name']);
|
||||
// Validation
|
||||
const { name } = params;
|
||||
const missingFields = hasFields(params, ['name']);
|
||||
|
||||
if (missingFields.length !== 0) {
|
||||
throw new FieldMissingError(missingFields[0]);
|
||||
}
|
||||
if (missingFields.length !== 0) {
|
||||
throw new FieldMissingError(missingFields[0]);
|
||||
}
|
||||
|
||||
// Check user exists
|
||||
if (!getters.emhttp().users.find(user => user.name === name)) {
|
||||
throw new AppError('No user exists with this name.');
|
||||
}
|
||||
// Check user exists
|
||||
if (!getters.emhttp().users.find((user) => user.name === name)) {
|
||||
throw new AppError('No user exists with this name.');
|
||||
}
|
||||
|
||||
// @todo: add user role
|
||||
// @todo: add user role
|
||||
|
||||
return {
|
||||
text: 'User updated successfully.',
|
||||
};
|
||||
return {
|
||||
text: 'User updated successfully.',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
import type { CoreContext, CoreResult } from '@app/core/types';
|
||||
import { AppError } from '@app/core/errors/app-error';
|
||||
import { FieldMissingError } from '@app/core/errors/field-missing-error';
|
||||
import { emcmd } from '@app/core/utils/clients/emcmd';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
import { hasFields } from '@app/core/utils/validation/has-fields';
|
||||
import { emcmd } from '@app/core/utils/clients/emcmd';
|
||||
import { getters } from '@app/store';
|
||||
|
||||
interface Context extends CoreContext {
|
||||
params: {
|
||||
/** Name of user to delete. */
|
||||
name: string;
|
||||
};
|
||||
params: {
|
||||
/** Name of user to delete. */
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete user account.
|
||||
*/
|
||||
export const deleteUser = async (context: Context): Promise<CoreResult> => {
|
||||
// Check permissions
|
||||
ensurePermission(context.user, {
|
||||
resource: 'user',
|
||||
action: 'delete',
|
||||
possession: 'any',
|
||||
});
|
||||
// Check permissions
|
||||
ensurePermission(context.user, {
|
||||
resource: 'user',
|
||||
action: 'delete',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
const { params } = context;
|
||||
const { name } = params;
|
||||
const missingFields = hasFields(params, ['name']);
|
||||
const { params } = context;
|
||||
const { name } = params;
|
||||
const missingFields = hasFields(params, ['name']);
|
||||
|
||||
if (missingFields.length !== 0) {
|
||||
// Just throw the first error
|
||||
throw new FieldMissingError(missingFields[0]);
|
||||
}
|
||||
if (missingFields.length !== 0) {
|
||||
// Just throw the first error
|
||||
throw new FieldMissingError(missingFields[0]);
|
||||
}
|
||||
|
||||
// Check user exists
|
||||
if (!getters.emhttp().users.find(user => user.name === name)) {
|
||||
throw new AppError('No user exists with this name.');
|
||||
}
|
||||
// Check user exists
|
||||
if (!getters.emhttp().users.find((user) => user.name === name)) {
|
||||
throw new AppError('No user exists with this name.');
|
||||
}
|
||||
|
||||
// Delete user
|
||||
await emcmd({
|
||||
userName: name,
|
||||
confirmDelete: 'on',
|
||||
cmdUserEdit: 'Delete',
|
||||
});
|
||||
// Delete user
|
||||
await emcmd({
|
||||
userName: name,
|
||||
confirmDelete: 'on',
|
||||
cmdUserEdit: 'Delete',
|
||||
});
|
||||
|
||||
return {
|
||||
text: 'User deleted successfully.',
|
||||
};
|
||||
return {
|
||||
text: 'User deleted successfully.',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { CoreContext, CoreResult } from '@app/core/types';
|
||||
import { AppError } from '@app/core/errors/app-error';
|
||||
import { ensureParameter } from '@app/core/utils/validation/context';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
import { ensureParameter } from '@app/core/utils/validation/context';
|
||||
import { getters } from '@app/store';
|
||||
|
||||
interface Context extends CoreContext {
|
||||
params: {
|
||||
/** User ID */
|
||||
id: string;
|
||||
};
|
||||
params: {
|
||||
/** User ID */
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -16,29 +16,29 @@ interface Context extends CoreContext {
|
||||
* @returns The selected user.
|
||||
*/
|
||||
export const getUser = async (context: Context): Promise<CoreResult> => {
|
||||
// Check permissions
|
||||
ensurePermission(context.user, {
|
||||
resource: 'user',
|
||||
action: 'create',
|
||||
possession: 'any',
|
||||
});
|
||||
// Check permissions
|
||||
ensurePermission(context.user, {
|
||||
resource: 'user',
|
||||
action: 'create',
|
||||
possession: 'any',
|
||||
});
|
||||
|
||||
ensureParameter(context, 'id');
|
||||
ensureParameter(context, 'id');
|
||||
|
||||
const id = context?.params?.id;
|
||||
if (!id) {
|
||||
throw new AppError('No id passed.');
|
||||
}
|
||||
const id = context?.params?.id;
|
||||
if (!id) {
|
||||
throw new AppError('No id passed.');
|
||||
}
|
||||
|
||||
const user = getters.emhttp().users.find(user => user.id === id);
|
||||
const user = getters.emhttp().users.find((user) => user.id === id);
|
||||
|
||||
if (!user) {
|
||||
// This is likely a new install or something went horribly wrong
|
||||
throw new AppError(`No users found matching ${id}`, 404);
|
||||
}
|
||||
if (!user) {
|
||||
// This is likely a new install or something went horribly wrong
|
||||
throw new AppError(`No users found matching ${id}`, 404);
|
||||
}
|
||||
|
||||
return {
|
||||
text: `User: ${JSON.stringify(user, null, 2)}`,
|
||||
json: user,
|
||||
};
|
||||
return {
|
||||
text: `User: ${JSON.stringify(user, null, 2)}`,
|
||||
json: user,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
import { Notifier, type NotifierOptions, type NotifierSendOptions } from '@app/core/notifiers/notifier';
|
||||
import type { NotifierOptions, NotifierSendOptions } from '@app/core/notifiers/notifier';
|
||||
import { logger } from '@app/core/log';
|
||||
import { Notifier } from '@app/core/notifiers/notifier';
|
||||
|
||||
/**
|
||||
* Console notifier.
|
||||
*/
|
||||
export class ConsoleNotifier extends Notifier {
|
||||
private readonly log: typeof logger;
|
||||
private readonly log: typeof logger;
|
||||
|
||||
constructor(options: NotifierOptions = {}) {
|
||||
super(options);
|
||||
constructor(options: NotifierOptions = {}) {
|
||||
super(options);
|
||||
|
||||
this.level = options.level ?? 'info';
|
||||
this.helpers = options.helpers ?? {};
|
||||
this.template = options.template ?? '{{{ data }}}';
|
||||
this.log = logger;
|
||||
}
|
||||
this.level = options.level ?? 'info';
|
||||
this.helpers = options.helpers ?? {};
|
||||
this.template = options.template ?? '{{{ data }}}';
|
||||
this.log = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification.
|
||||
*/
|
||||
send(options: NotifierSendOptions) {
|
||||
const { title, data } = options;
|
||||
const { level, helpers } = this;
|
||||
// Render template
|
||||
const template = this.render({ ...data }, helpers);
|
||||
/**
|
||||
* Send notification.
|
||||
*/
|
||||
send(options: NotifierSendOptions) {
|
||||
const { title, data } = options;
|
||||
const { level, helpers } = this;
|
||||
// Render template
|
||||
const template = this.render({ ...data }, helpers);
|
||||
|
||||
this.log[level](title, template);
|
||||
}
|
||||
this.log[level](title, template);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { got } from 'got';
|
||||
import { Notifier, type NotifierOptions } from '@app/core/notifiers/notifier';
|
||||
|
||||
export type Options = NotifierOptions
|
||||
import type { NotifierOptions } from '@app/core/notifiers/notifier';
|
||||
import { Notifier } from '@app/core/notifiers/notifier';
|
||||
|
||||
export type Options = NotifierOptions;
|
||||
|
||||
/**
|
||||
* HTTP notifier.
|
||||
*/
|
||||
export class HttpNotifier extends Notifier {
|
||||
readonly $http = got;
|
||||
|
||||
constructor(options: Options) {
|
||||
super(options);
|
||||
}
|
||||
readonly $http = got;
|
||||
|
||||
constructor(options: Options) {
|
||||
super(options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
import Mustache from 'mustache';
|
||||
|
||||
import { type LooseObject } from '@app/core/types';
|
||||
import { type NotificationIni } from '../types/states/notification';
|
||||
import { type NotificationIni } from '@app/core/types/states/notification';
|
||||
|
||||
export type NotifierLevel = 'info' | 'warn' | 'error';
|
||||
|
||||
export type NotifierOptions = Partial<{
|
||||
level: NotifierLevel;
|
||||
importance?: NotificationIni['importance'];
|
||||
helpers?: Record<string, unknown>;
|
||||
template?: string;
|
||||
level: NotifierLevel;
|
||||
importance?: NotificationIni['importance'];
|
||||
helpers?: Record<string, unknown>;
|
||||
template?: string;
|
||||
}>;
|
||||
|
||||
export interface NotifierSendOptions {
|
||||
/** Which type of notification. */
|
||||
type?: string;
|
||||
/** The notification's title. */
|
||||
title: string;
|
||||
/** Static data passed for rendering. */
|
||||
data: LooseObject;
|
||||
/** Functions to generate dynamic data for rendering. */
|
||||
computed?: LooseObject;
|
||||
/** Which type of notification. */
|
||||
type?: string;
|
||||
/** The notification's title. */
|
||||
title: string;
|
||||
/** Static data passed for rendering. */
|
||||
data: LooseObject;
|
||||
/** Functions to generate dynamic data for rendering. */
|
||||
computed?: LooseObject;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,31 +31,31 @@ export interface NotifierSendOptions {
|
||||
* @private
|
||||
*/
|
||||
export class Notifier {
|
||||
template: string;
|
||||
helpers: LooseObject;
|
||||
level: string;
|
||||
template: string;
|
||||
helpers: LooseObject;
|
||||
level: string;
|
||||
|
||||
constructor(options: NotifierOptions) {
|
||||
this.template = options.template ?? '{{ data }}';
|
||||
this.helpers = options.helpers ?? {};
|
||||
this.level = options.level ?? 'info';
|
||||
}
|
||||
constructor(options: NotifierOptions) {
|
||||
this.template = options.template ?? '{{ data }}';
|
||||
this.helpers = options.helpers ?? {};
|
||||
this.level = options.level ?? 'info';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render template.
|
||||
* @param data Static data for template rendering.
|
||||
* @param helpers Functions for template rendering.
|
||||
* @param computed Functions to generate dynamic data for rendering.
|
||||
*/
|
||||
render(data: LooseObject): string {
|
||||
return Mustache.render(this.template, data);
|
||||
}
|
||||
/**
|
||||
* Render template.
|
||||
* @param data Static data for template rendering.
|
||||
* @param helpers Functions for template rendering.
|
||||
* @param computed Functions to generate dynamic data for rendering.
|
||||
*/
|
||||
render(data: LooseObject): string {
|
||||
return Mustache.render(this.template, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a mustache helper.
|
||||
* @param func Function to be wrapped.
|
||||
*/
|
||||
generateHelper(func: (text: string) => string) {
|
||||
return () => (text: string, render: (text: string) => string) => func(render(text));
|
||||
}
|
||||
/**
|
||||
* Generates a mustache helper.
|
||||
* @param func Function to be wrapped.
|
||||
*/
|
||||
generateHelper(func: (text: string) => string) {
|
||||
return () => (text: string, render: (text: string) => string) => func(render(text));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,52 @@
|
||||
import { logger } from '@app/core/log';
|
||||
import { Notifier, type NotifierSendOptions, type NotifierOptions } from '@app/core/notifiers/notifier';
|
||||
import { execa } from 'execa';
|
||||
|
||||
import type { NotifierOptions, NotifierSendOptions } from '@app/core/notifiers/notifier';
|
||||
import { logger } from '@app/core/log';
|
||||
import { Notifier } from '@app/core/notifiers/notifier';
|
||||
|
||||
type ValidLocalLevels = 'alert' | 'warning' | 'normal';
|
||||
|
||||
export class UnraidLocalNotifier extends Notifier {
|
||||
private convertNotifierLevel(level: NotifierOptions['level']): ValidLocalLevels {
|
||||
switch (level) {
|
||||
case 'error':
|
||||
return 'alert';
|
||||
case 'warn':
|
||||
return 'warning';
|
||||
case 'info':
|
||||
return 'normal';
|
||||
default:
|
||||
return 'normal';
|
||||
}
|
||||
}
|
||||
private convertNotifierLevel(level: NotifierOptions['level']): ValidLocalLevels {
|
||||
switch (level) {
|
||||
case 'error':
|
||||
return 'alert';
|
||||
case 'warn':
|
||||
return 'warning';
|
||||
case 'info':
|
||||
return 'normal';
|
||||
default:
|
||||
return 'normal';
|
||||
}
|
||||
}
|
||||
|
||||
constructor(options: NotifierOptions = {}) {
|
||||
super(options);
|
||||
constructor(options: NotifierOptions = {}) {
|
||||
super(options);
|
||||
|
||||
this.level = options.importance ?? this.convertNotifierLevel(options.level ?? 'info');
|
||||
this.template = options.template ?? '{{ message }}';
|
||||
}
|
||||
this.level = options.importance ?? this.convertNotifierLevel(options.level ?? 'info');
|
||||
this.template = options.template ?? '{{ message }}';
|
||||
}
|
||||
|
||||
async send(options: NotifierSendOptions) {
|
||||
const { title, data } = options;
|
||||
const { level } = this;
|
||||
async send(options: NotifierSendOptions) {
|
||||
const { title, data } = options;
|
||||
const { level } = this;
|
||||
|
||||
const template = this.render(data);
|
||||
try {
|
||||
await execa('/usr/local/emhttp/webGui/scripts/notify', ['-i', `${level}`, '-s', 'Unraid API', '-d', `${template}`, '-e', `${title}`]);
|
||||
} catch (error: unknown) {
|
||||
logger.warn(`Error sending unraid notification: ${error instanceof Error ? error.message : 'No Error Information'}`);
|
||||
}
|
||||
}
|
||||
const template = this.render(data);
|
||||
try {
|
||||
await execa('/usr/local/emhttp/webGui/scripts/notify', [
|
||||
'-i',
|
||||
`${level}`,
|
||||
'-s',
|
||||
'Unraid API',
|
||||
'-d',
|
||||
`${template}`,
|
||||
'-e',
|
||||
`${title}`,
|
||||
]);
|
||||
} catch (error: unknown) {
|
||||
logger.warn(
|
||||
`Error sending unraid notification: ${error instanceof Error ? error.message : 'No Error Information'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PubSub } from 'graphql-subscriptions';
|
||||
import EventEmitter from 'events';
|
||||
|
||||
import { PubSub } from 'graphql-subscriptions';
|
||||
|
||||
// Allow subscriptions to have 30 connections
|
||||
const eventEmitter = new EventEmitter();
|
||||
eventEmitter.setMaxListeners(30);
|
||||
|
||||
19
api/src/core/types/domain.ts
Normal file
19
api/src/core/types/domain.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// @todo: finish this
|
||||
export type DomainState = 'running' | 'stopped';
|
||||
|
||||
/**
|
||||
* Vm domain.
|
||||
*/
|
||||
export interface Domain {
|
||||
uuid: string;
|
||||
osType: string;
|
||||
autostart: string;
|
||||
maxMemory: string;
|
||||
schedulerType: string;
|
||||
schedulerParameters: string;
|
||||
securityLabel: string;
|
||||
name: string;
|
||||
state: string;
|
||||
vcpus?: string;
|
||||
memoryStats?: string;
|
||||
}
|
||||
36
api/src/core/types/global.ts
Normal file
36
api/src/core/types/global.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { type User } from '@app/core/types/states/user';
|
||||
|
||||
/**
|
||||
* Example: 1, 2, 3 or 1,2,3
|
||||
*/
|
||||
export type CommaSeparatedString = string;
|
||||
|
||||
export type LooseObject = Record<string, any>;
|
||||
|
||||
export type LooseStringObject = Record<string, string>;
|
||||
|
||||
/**
|
||||
* Context object
|
||||
* @property query Query params. e.g. { limit: 50 }
|
||||
* @property data Data object.
|
||||
* @property param Params object.
|
||||
*/
|
||||
export interface CoreContext<
|
||||
Query = Record<string, unknown>,
|
||||
Data = Record<string, unknown>,
|
||||
Params = Record<string, unknown>,
|
||||
> {
|
||||
readonly query?: Readonly<Query>;
|
||||
readonly data?: Readonly<Data>;
|
||||
readonly params?: Readonly<Params>;
|
||||
readonly user: Readonly<User>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result object
|
||||
*/
|
||||
export interface CoreResult<JSON = unknown> {
|
||||
json?: JSON;
|
||||
text?: string;
|
||||
html?: string;
|
||||
}
|
||||
@@ -11,9 +11,9 @@ interface Display {
|
||||
critical: string;
|
||||
custom: string;
|
||||
dashapps: string;
|
||||
/** a strftime format string */
|
||||
/** a strftime format string */
|
||||
date: string;
|
||||
/** a strftime format string */
|
||||
/** a strftime format string */
|
||||
time?: string;
|
||||
hot: string;
|
||||
max: string;
|
||||
|
||||
@@ -4,15 +4,15 @@ export type PciDeviceClass = 'vga' | 'audio' | 'gpu' | 'other';
|
||||
* PCI device
|
||||
*/
|
||||
export interface PciDevice {
|
||||
id: string;
|
||||
allowed: boolean;
|
||||
class: PciDeviceClass;
|
||||
vendorname: string;
|
||||
productname: string;
|
||||
typeid: string;
|
||||
serial: string;
|
||||
product: string;
|
||||
manufacturer: string;
|
||||
guid: string;
|
||||
name: string;
|
||||
id: string;
|
||||
allowed: boolean;
|
||||
class: PciDeviceClass;
|
||||
vendorname: string;
|
||||
productname: string;
|
||||
typeid: string;
|
||||
serial: string;
|
||||
product: string;
|
||||
manufacturer: string;
|
||||
guid: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
export type Network = {
|
||||
dhcpKeepresolv: boolean;
|
||||
dnsServer1: string;
|
||||
dnsServer2: string;
|
||||
dhcp6Keepresolv: boolean;
|
||||
bonding: boolean;
|
||||
bondname: string;
|
||||
bondnics: string[];
|
||||
bondingMode: string;
|
||||
bondingMiimon: string;
|
||||
bridging: boolean;
|
||||
brname: string;
|
||||
brnics: string;
|
||||
brstp: string;
|
||||
brfd: string;
|
||||
'description': string[];
|
||||
'protocol': string[];
|
||||
'useDhcp': boolean[];
|
||||
'ipaddr': string[];
|
||||
'netmask': string[];
|
||||
'gateway': string[];
|
||||
'metric': string[];
|
||||
'useDhcp6': boolean[];
|
||||
'ipaddr6': string[];
|
||||
'netmask6': string[];
|
||||
'gateway6': string[];
|
||||
'metric6': string[];
|
||||
'privacy6': string[];
|
||||
mtu: string[];
|
||||
type: string[];
|
||||
dhcpKeepresolv: boolean;
|
||||
dnsServer1: string;
|
||||
dnsServer2: string;
|
||||
dhcp6Keepresolv: boolean;
|
||||
bonding: boolean;
|
||||
bondname: string;
|
||||
bondnics: string[];
|
||||
bondingMode: string;
|
||||
bondingMiimon: string;
|
||||
bridging: boolean;
|
||||
brname: string;
|
||||
brnics: string;
|
||||
brstp: string;
|
||||
brfd: string;
|
||||
description: string[];
|
||||
protocol: string[];
|
||||
useDhcp: boolean[];
|
||||
ipaddr: string[];
|
||||
netmask: string[];
|
||||
gateway: string[];
|
||||
metric: string[];
|
||||
useDhcp6: boolean[];
|
||||
ipaddr6: string[];
|
||||
netmask6: string[];
|
||||
gateway6: string[];
|
||||
metric6: string[];
|
||||
privacy6: string[];
|
||||
mtu: string[];
|
||||
type: string[];
|
||||
};
|
||||
|
||||
export type Networks = Network[];
|
||||
|
||||
8
api/src/core/types/states/nfs.ts
Normal file
8
api/src/core/types/states/nfs.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type NfsShare = {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
writeList: string[];
|
||||
readList: string[];
|
||||
};
|
||||
|
||||
export type NfsShares = NfsShare[];
|
||||
@@ -1,23 +1,23 @@
|
||||
export interface FqdnEntry {
|
||||
interface: string;
|
||||
id: number | null;
|
||||
fqdn: string;
|
||||
isIpv6: boolean;
|
||||
interface: string;
|
||||
id: number | null;
|
||||
fqdn: string;
|
||||
isIpv6: boolean;
|
||||
}
|
||||
|
||||
export interface Nginx {
|
||||
certificateName: string;
|
||||
certificatePath: string;
|
||||
defaultUrl: string;
|
||||
httpPort: number;
|
||||
httpsPort: number;
|
||||
lanIp: string;
|
||||
lanIp6: string;
|
||||
lanMdns: string;
|
||||
lanName: string;
|
||||
sslEnabled: boolean;
|
||||
sslMode: 'yes' | 'no' | 'auto';
|
||||
wanAccessEnabled: boolean;
|
||||
wanIp: string;
|
||||
fqdnUrls: FqdnEntry[];
|
||||
certificateName: string;
|
||||
certificatePath: string;
|
||||
defaultUrl: string;
|
||||
httpPort: number;
|
||||
httpsPort: number;
|
||||
lanIp: string;
|
||||
lanIp6: string;
|
||||
lanMdns: string;
|
||||
lanName: string;
|
||||
sslEnabled: boolean;
|
||||
sslMode: 'yes' | 'no' | 'auto';
|
||||
wanAccessEnabled: boolean;
|
||||
wanIp: string;
|
||||
fqdnUrls: FqdnEntry[];
|
||||
}
|
||||
|
||||
7
api/src/core/types/states/sec.ts
Normal file
7
api/src/core/types/states/sec.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { type IniEnabled } from '@app/core/types/ini';
|
||||
|
||||
export interface SecIni {
|
||||
export: IniEnabled;
|
||||
writeList: string;
|
||||
readList: string;
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
export type Share = {
|
||||
/** Share name. */
|
||||
name: string;
|
||||
/** Free space in bytes. */
|
||||
free: number;
|
||||
/** Total space in bytes. */
|
||||
size: number;
|
||||
/** Which disks to include from the share. */
|
||||
include: string[];
|
||||
/** Which disks to exclude from the share. */
|
||||
exclude: string[];
|
||||
/** If the share should use the cache. */
|
||||
cache: boolean;
|
||||
/** Share name. */
|
||||
name: string;
|
||||
/** Free space in bytes. */
|
||||
free: number;
|
||||
/** Total space in bytes. */
|
||||
size: number;
|
||||
/** Which disks to include from the share. */
|
||||
include: string[];
|
||||
/** Which disks to exclude from the share. */
|
||||
exclude: string[];
|
||||
/** If the share should use the cache. */
|
||||
cache: boolean;
|
||||
};
|
||||
|
||||
export type Shares = Share[];
|
||||
@@ -19,14 +19,14 @@ export type Shares = Share[];
|
||||
* Disk share
|
||||
*/
|
||||
export interface DiskShare extends Share {
|
||||
type: 'disk';
|
||||
type: 'disk';
|
||||
}
|
||||
|
||||
/**
|
||||
* User share
|
||||
*/
|
||||
export interface UserShare extends Share {
|
||||
type: 'user';
|
||||
type: 'user';
|
||||
}
|
||||
|
||||
export type ShareType = 'user' | 'users' | 'disk' | 'disks';
|
||||
|
||||
35
api/src/core/types/states/smb.ts
Normal file
35
api/src/core/types/states/smb.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Security
|
||||
*/
|
||||
export type SmbSecurity =
|
||||
/**
|
||||
* When logged into the server as Guest, a macOS user can view and read/write all shares set as Public.
|
||||
* Files created or modified in the share will be owned by user nobody of the users group.
|
||||
* macOS users logged in with a user name/password previously created on the server can also view and read/write all shares set as Public.
|
||||
* In this case, files created or modified on the server will be owned by the logged in user.
|
||||
*/
|
||||
| 'Public'
|
||||
/**
|
||||
* When logged into the server as Guest, a macOS user can view and read(but not write) all shares set as Secure.
|
||||
* macOS users logged in with a user name/password previously created on the server can also view and read all shares set as Secure.
|
||||
* If their access right is set to read/write for the share on the server, they may also write the share.
|
||||
*/
|
||||
| 'Secure'
|
||||
/**
|
||||
* When logged onto the server as Guest, no Private shares are visible or accessible to any macOS user.
|
||||
* macOS users logged in with a user name/password previously created on the server may
|
||||
* have read or read/write(or have no access)according their access right for the share on the server. */
|
||||
| 'Private';
|
||||
|
||||
export type SmbShare = {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
security: SmbSecurity;
|
||||
writeList: string[];
|
||||
readList: string[];
|
||||
timemachine: {
|
||||
volsizelimit: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type SmbShares = SmbShare[];
|
||||
@@ -1,13 +1,13 @@
|
||||
export type User = {
|
||||
/** User's ID */
|
||||
id: string;
|
||||
/** Display name */
|
||||
name: string;
|
||||
description: string;
|
||||
/** If password is set. */
|
||||
password: boolean;
|
||||
/** The main {@link Permissions~Role | role} linked to this account. */
|
||||
role: string;
|
||||
/** User's ID */
|
||||
id: string;
|
||||
/** Display name */
|
||||
name: string;
|
||||
description: string;
|
||||
/** If password is set. */
|
||||
password: boolean;
|
||||
/** The main {@link Permissions~Role | role} linked to this account. */
|
||||
role: string;
|
||||
};
|
||||
|
||||
export type Users = User[];
|
||||
|
||||
@@ -1,196 +1,201 @@
|
||||
import { type registrationType, type ArrayState, type RegistrationState, type DiskFsType } from "@app/graphql/generated/api/types";
|
||||
import {
|
||||
type ArrayState,
|
||||
type DiskFsType,
|
||||
type RegistrationState,
|
||||
type registrationType,
|
||||
} from '@app/graphql/generated/api/types';
|
||||
|
||||
/**
|
||||
* Global vars
|
||||
*/
|
||||
export type Var = {
|
||||
bindMgt: boolean | null;
|
||||
cacheNumDevices: number;
|
||||
cacheSbNumDisks: number;
|
||||
/** Description of your server (displayed in the "webGui"). */
|
||||
comment: string;
|
||||
/** Is the array's config valid. */
|
||||
configValid: boolean;
|
||||
/** @internal used to hold the value for config.error */
|
||||
configState: string;
|
||||
/** Current CSRF token for HTTP requests with emhttpd. */
|
||||
csrfToken: string;
|
||||
defaultFormat: string;
|
||||
/** Default file system for data disks. */
|
||||
defaultFsType: DiskFsType.XFS;
|
||||
/** Amount of connected drives (license device count). */
|
||||
deviceCount: number;
|
||||
domain: string;
|
||||
domainLogin: string;
|
||||
domainShort: string;
|
||||
flashGuid: string;
|
||||
flashProduct: string;
|
||||
flashVendor: string;
|
||||
/** Current progress of the {@link ?content=mover | mover}. */
|
||||
fsCopyPrcnt: number;
|
||||
fsNumMounted: number;
|
||||
fsNumUnmountable: number;
|
||||
fsProgress: string;
|
||||
/** Current state of the array. */
|
||||
fsState: string;
|
||||
fsUnmountableMask: string;
|
||||
fuseDirectio: string;
|
||||
fuseDirectioDefault: string;
|
||||
fuseDirectioStatus: string;
|
||||
fuseRemember: string;
|
||||
fuseRememberDefault: string;
|
||||
fuseRememberStatus: string;
|
||||
hideDotFiles: boolean;
|
||||
// JoinStatus
|
||||
localMaster: boolean;
|
||||
/** The local tld to use e.g. `.local`. */
|
||||
localTld: string;
|
||||
/** Absolute file path to the data disks' luks key. */
|
||||
luksKeyfile: string;
|
||||
maxArraysz: number; /** Max amount of data drives allowed in the array. */
|
||||
maxCachesz: number; /** Max amount of cache drives allowed in the array. */
|
||||
mdColor: string;
|
||||
/** The amount of {@link ?content=array#disks-disabled | disabled disks} from the current array. */
|
||||
mdNumDisabled: number;
|
||||
mdNumDisks: number;
|
||||
mdNumErased: number;
|
||||
/** The amount of {@link ?content=array#disks-invalid | invalid disks} from the current array. */
|
||||
mdNumInvalid: number;
|
||||
/** The amount of {@link ?content=array#disks-missing | missing disks} from the current array. */
|
||||
mdNumMissing: number;
|
||||
mdNumNew: number;
|
||||
mdNumStripes: number;
|
||||
mdNumStripesDefault: number;
|
||||
mdNumStripesStatus: string;
|
||||
mdResync: number;
|
||||
mdResyncAction: string;
|
||||
mdResyncCorr: string;
|
||||
mdResyncDb: string;
|
||||
mdResyncDt: string;
|
||||
mdResyncPos: number;
|
||||
mdResyncSize: number;
|
||||
mdState: ArrayState;
|
||||
mdSyncThresh: number;
|
||||
mdSyncThreshDefault: number;
|
||||
mdSyncThreshStatus: string;
|
||||
mdSyncWindow: number;
|
||||
mdSyncWindowDefault: number;
|
||||
mdSyncWindowStatus: string;
|
||||
mdVersion: string;
|
||||
mdWriteMethod: number;
|
||||
mdWriteMethodDefault: string;
|
||||
mdWriteMethodStatus: string;
|
||||
/** Machine hostname. */
|
||||
name: string;
|
||||
// NrRequests
|
||||
nrRequests: number;
|
||||
// NrRequestsDefault
|
||||
nrRequestsDefault: number;
|
||||
// NrRequestsStatus
|
||||
/** NTP Server 1. */
|
||||
ntpServer1: string;
|
||||
/** NTP Server 2. */
|
||||
ntpServer2: string;
|
||||
/** NTP Server 3. */
|
||||
ntpServer3: string;
|
||||
/** NTP Server 4. */
|
||||
ntpServer4: string;
|
||||
pollAttributes: string;
|
||||
pollAttributesDefault: string;
|
||||
pollAttributesStatus: string;
|
||||
/** Port for the webui via HTTP. */
|
||||
port: number;
|
||||
/** Port for SSH daemon. */
|
||||
portssh: number;
|
||||
/** Port for the webui via HTTPS. */
|
||||
portssl: number;
|
||||
/** Port for telnet daemon. */
|
||||
porttelnet: number;
|
||||
queueDepth: string;
|
||||
regCheck: string;
|
||||
regState: RegistrationState;
|
||||
/** Where the registration key is stored. (e.g. "/boot/config/Pro.key") */
|
||||
regFile: string;
|
||||
regGen: string;
|
||||
regGuid: string;
|
||||
/** Registration time for key */
|
||||
regTm: string;
|
||||
/** Expiration of license for Trial Keys */
|
||||
regTm2: string;
|
||||
/** Expiration of Updates for non-legacy keys */
|
||||
regExp: string | null;
|
||||
/** Who the current Unraid key is registered to. */
|
||||
regTo: string;
|
||||
/** Which type of key this is. */
|
||||
regTy: registrationType;
|
||||
/** Is the server currently in safe mode. */
|
||||
safeMode: boolean;
|
||||
sbClean: boolean;
|
||||
sbEvents: number;
|
||||
sbName: string;
|
||||
sbNumDisks: number;
|
||||
sbState: string;
|
||||
sbSynced: number;
|
||||
sbSynced2: number;
|
||||
sbSyncErrs: number;
|
||||
sbSyncExit: string;
|
||||
sbUpdated: string;
|
||||
sbVersion: string;
|
||||
security: string;
|
||||
shareAvahiEnabled: boolean;
|
||||
shareAvahiSmbModel: string;
|
||||
shareAvahiSmbName: string;
|
||||
shareCacheEnabled: boolean;
|
||||
shareCacheFloor: string;
|
||||
/** Total number of disk/user shares. */
|
||||
shareCount: number;
|
||||
shareDisk: string;
|
||||
shareInitialGroup: string;
|
||||
shareInitialOwner: string;
|
||||
/** If the {@link ?content=mover | mover} is currently active. */
|
||||
shareMoverActive: boolean;
|
||||
shareMoverLogging: boolean;
|
||||
/** When the share mover script should run. Takes cron format time. */
|
||||
shareMoverSchedule: string;
|
||||
/** Total number of NFS shares. */
|
||||
shareNfsCount: number;
|
||||
shareNfsEnabled: boolean;
|
||||
/** Total number of SMB shares. */
|
||||
shareSmbCount: number;
|
||||
/** Is smb enabled */
|
||||
shareSmbEnabled: boolean;
|
||||
/** Which mode is smb running in? active-directory | workgroup */
|
||||
shareSmbMode: string;
|
||||
shareUser: string;
|
||||
// ShareUserExclude
|
||||
shutdownTimeout: number;
|
||||
/** How long until emhttpd should spin down the data drives in your array. */
|
||||
spindownDelay: number;
|
||||
spinupGroups: boolean;
|
||||
/** Should the array be started by default on boot. */
|
||||
startArray: boolean;
|
||||
/** The default start mode for the server. */
|
||||
startMode: string;
|
||||
/** Which page to start the webGui on. */
|
||||
startPage: string;
|
||||
sysArraySlots: number;
|
||||
sysCacheSlots: number;
|
||||
sysFlashSlots: number;
|
||||
sysModel: string;
|
||||
/** Current timezone. {@link https://en.wikipedia.org/wiki/List_of_tz_database_time_zones | Timezone list}. */
|
||||
timeZone: string;
|
||||
/** Should a NTP server be used for time sync. */
|
||||
useNtp: boolean;
|
||||
/** Should SSH be enabled. */
|
||||
useSsh: boolean;
|
||||
/** If HTTPS should be be enabled in the webui. */
|
||||
useSsl: boolean | null;
|
||||
/** Should telnet be enabled. */
|
||||
useTelnet: boolean;
|
||||
/** The current Unraid version. */
|
||||
version: string;
|
||||
/** The SMB workgroup. */
|
||||
workgroup: string;
|
||||
/** UPNP Setting */
|
||||
useUpnp: boolean;
|
||||
bindMgt: boolean | null;
|
||||
cacheNumDevices: number;
|
||||
cacheSbNumDisks: number;
|
||||
/** Description of your server (displayed in the "webGui"). */
|
||||
comment: string;
|
||||
/** Is the array's config valid. */
|
||||
configValid: boolean;
|
||||
/** @internal used to hold the value for config.error */
|
||||
configState: string;
|
||||
/** Current CSRF token for HTTP requests with emhttpd. */
|
||||
csrfToken: string;
|
||||
defaultFormat: string;
|
||||
/** Default file system for data disks. */
|
||||
defaultFsType: DiskFsType.XFS;
|
||||
/** Amount of connected drives (license device count). */
|
||||
deviceCount: number;
|
||||
domain: string;
|
||||
domainLogin: string;
|
||||
domainShort: string;
|
||||
flashGuid: string;
|
||||
flashProduct: string;
|
||||
flashVendor: string;
|
||||
/** Current progress of the {@link ?content=mover | mover}. */
|
||||
fsCopyPrcnt: number;
|
||||
fsNumMounted: number;
|
||||
fsNumUnmountable: number;
|
||||
fsProgress: string;
|
||||
/** Current state of the array. */
|
||||
fsState: string;
|
||||
fsUnmountableMask: string;
|
||||
fuseDirectio: string;
|
||||
fuseDirectioDefault: string;
|
||||
fuseDirectioStatus: string;
|
||||
fuseRemember: string;
|
||||
fuseRememberDefault: string;
|
||||
fuseRememberStatus: string;
|
||||
hideDotFiles: boolean;
|
||||
// JoinStatus
|
||||
localMaster: boolean;
|
||||
/** The local tld to use e.g. `.local`. */
|
||||
localTld: string;
|
||||
/** Absolute file path to the data disks' luks key. */
|
||||
luksKeyfile: string;
|
||||
maxArraysz: number /** Max amount of data drives allowed in the array. */;
|
||||
maxCachesz: number /** Max amount of cache drives allowed in the array. */;
|
||||
mdColor: string;
|
||||
/** The amount of {@link ?content=array#disks-disabled | disabled disks} from the current array. */
|
||||
mdNumDisabled: number;
|
||||
mdNumDisks: number;
|
||||
mdNumErased: number;
|
||||
/** The amount of {@link ?content=array#disks-invalid | invalid disks} from the current array. */
|
||||
mdNumInvalid: number;
|
||||
/** The amount of {@link ?content=array#disks-missing | missing disks} from the current array. */
|
||||
mdNumMissing: number;
|
||||
mdNumNew: number;
|
||||
mdNumStripes: number;
|
||||
mdNumStripesDefault: number;
|
||||
mdNumStripesStatus: string;
|
||||
mdResync: number;
|
||||
mdResyncAction: string;
|
||||
mdResyncCorr: string;
|
||||
mdResyncDb: string;
|
||||
mdResyncDt: string;
|
||||
mdResyncPos: number;
|
||||
mdResyncSize: number;
|
||||
mdState: ArrayState;
|
||||
mdSyncThresh: number;
|
||||
mdSyncThreshDefault: number;
|
||||
mdSyncThreshStatus: string;
|
||||
mdSyncWindow: number;
|
||||
mdSyncWindowDefault: number;
|
||||
mdSyncWindowStatus: string;
|
||||
mdVersion: string;
|
||||
mdWriteMethod: number;
|
||||
mdWriteMethodDefault: string;
|
||||
mdWriteMethodStatus: string;
|
||||
/** Machine hostname. */
|
||||
name: string;
|
||||
// NrRequests
|
||||
nrRequests: number;
|
||||
// NrRequestsDefault
|
||||
nrRequestsDefault: number;
|
||||
// NrRequestsStatus
|
||||
/** NTP Server 1. */
|
||||
ntpServer1: string;
|
||||
/** NTP Server 2. */
|
||||
ntpServer2: string;
|
||||
/** NTP Server 3. */
|
||||
ntpServer3: string;
|
||||
/** NTP Server 4. */
|
||||
ntpServer4: string;
|
||||
pollAttributes: string;
|
||||
pollAttributesDefault: string;
|
||||
pollAttributesStatus: string;
|
||||
/** Port for the webui via HTTP. */
|
||||
port: number;
|
||||
/** Port for SSH daemon. */
|
||||
portssh: number;
|
||||
/** Port for the webui via HTTPS. */
|
||||
portssl: number;
|
||||
/** Port for telnet daemon. */
|
||||
porttelnet: number;
|
||||
queueDepth: string;
|
||||
regCheck: string;
|
||||
regState: RegistrationState;
|
||||
/** Where the registration key is stored. (e.g. "/boot/config/Pro.key") */
|
||||
regFile: string;
|
||||
regGen: string;
|
||||
regGuid: string;
|
||||
/** Registration time for key */
|
||||
regTm: string;
|
||||
/** Expiration of license for Trial Keys */
|
||||
regTm2: string;
|
||||
/** Expiration of Updates for non-legacy keys */
|
||||
regExp: string | null;
|
||||
/** Who the current Unraid key is registered to. */
|
||||
regTo: string;
|
||||
/** Which type of key this is. */
|
||||
regTy: registrationType;
|
||||
/** Is the server currently in safe mode. */
|
||||
safeMode: boolean;
|
||||
sbClean: boolean;
|
||||
sbEvents: number;
|
||||
sbName: string;
|
||||
sbNumDisks: number;
|
||||
sbState: string;
|
||||
sbSynced: number;
|
||||
sbSynced2: number;
|
||||
sbSyncErrs: number;
|
||||
sbSyncExit: string;
|
||||
sbUpdated: string;
|
||||
sbVersion: string;
|
||||
security: string;
|
||||
shareAvahiEnabled: boolean;
|
||||
shareAvahiSmbModel: string;
|
||||
shareAvahiSmbName: string;
|
||||
shareCacheEnabled: boolean;
|
||||
shareCacheFloor: string;
|
||||
/** Total number of disk/user shares. */
|
||||
shareCount: number;
|
||||
shareDisk: string;
|
||||
shareInitialGroup: string;
|
||||
shareInitialOwner: string;
|
||||
/** If the {@link ?content=mover | mover} is currently active. */
|
||||
shareMoverActive: boolean;
|
||||
shareMoverLogging: boolean;
|
||||
/** When the share mover script should run. Takes cron format time. */
|
||||
shareMoverSchedule: string;
|
||||
/** Total number of NFS shares. */
|
||||
shareNfsCount: number;
|
||||
shareNfsEnabled: boolean;
|
||||
/** Total number of SMB shares. */
|
||||
shareSmbCount: number;
|
||||
/** Is smb enabled */
|
||||
shareSmbEnabled: boolean;
|
||||
/** Which mode is smb running in? active-directory | workgroup */
|
||||
shareSmbMode: string;
|
||||
shareUser: string;
|
||||
// ShareUserExclude
|
||||
shutdownTimeout: number;
|
||||
/** How long until emhttpd should spin down the data drives in your array. */
|
||||
spindownDelay: number;
|
||||
spinupGroups: boolean;
|
||||
/** Should the array be started by default on boot. */
|
||||
startArray: boolean;
|
||||
/** The default start mode for the server. */
|
||||
startMode: string;
|
||||
/** Which page to start the webGui on. */
|
||||
startPage: string;
|
||||
sysArraySlots: number;
|
||||
sysCacheSlots: number;
|
||||
sysFlashSlots: number;
|
||||
sysModel: string;
|
||||
/** Current timezone. {@link https://en.wikipedia.org/wiki/List_of_tz_database_time_zones | Timezone list}. */
|
||||
timeZone: string;
|
||||
/** Should a NTP server be used for time sync. */
|
||||
useNtp: boolean;
|
||||
/** Should SSH be enabled. */
|
||||
useSsh: boolean;
|
||||
/** If HTTPS should be be enabled in the webui. */
|
||||
useSsl: boolean | null;
|
||||
/** Should telnet be enabled. */
|
||||
useTelnet: boolean;
|
||||
/** The current Unraid version. */
|
||||
version: string;
|
||||
/** The SMB workgroup. */
|
||||
workgroup: string;
|
||||
/** UPNP Setting */
|
||||
useUpnp: boolean;
|
||||
};
|
||||
|
||||
@@ -5,6 +5,6 @@ import { getters } from '@app/store';
|
||||
* Is the array running?
|
||||
*/
|
||||
export const arrayIsRunning = () => {
|
||||
const emhttp = getters.emhttp();
|
||||
return emhttp.var.mdState === ArrayState.STARTED;
|
||||
const emhttp = getters.emhttp();
|
||||
return emhttp.var.mdState === ArrayState.STARTED;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { convert, type Data } from 'convert';
|
||||
import type { Data } from 'convert';
|
||||
import { convert } from 'convert';
|
||||
|
||||
// If it's "true", "yes" or "1" then it's true otherwise it's false
|
||||
export const toBoolean = (value: string): boolean =>
|
||||
|
||||
@@ -2,10 +2,10 @@ import Docker from 'dockerode';
|
||||
|
||||
const socketPath = '/var/run/docker.sock';
|
||||
const client = new Docker({
|
||||
socketPath,
|
||||
socketPath,
|
||||
});
|
||||
|
||||
/**
|
||||
* Docker client
|
||||
*/
|
||||
export const docker = client;
|
||||
export const docker = client;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user