Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions api/src/config/paths.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Injectable } from '@nestjs/common';
import { join, resolve as resolvePath } from 'path';

@Injectable()
export class PathsConfig {
private static instance: PathsConfig;

readonly core = import.meta.dirname;
readonly unraidApiBase = '/usr/local/unraid-api/';
readonly unraidData = resolvePath(
process.env.PATHS_UNRAID_DATA ?? '/boot/config/plugins/dynamix.my.servers/data/'
);
readonly dockerAutostart = '/var/lib/docker/unraid-autostart';
readonly dockerSocket = '/var/run/docker.sock';
readonly parityChecks = '/boot/config/parity-checks.log';
readonly htpasswd = '/etc/nginx/htpasswd';
readonly emhttpdSocket = '/var/run/emhttpd.socket';
readonly states = resolvePath(process.env.PATHS_STATES ?? '/usr/local/emhttp/state/');
readonly dynamixBase = resolvePath(
process.env.PATHS_DYNAMIX_BASE ?? '/boot/config/plugins/dynamix/'
);

/**
* Plugins have a default config and, optionally, a user-customized config.
* You have to merge them to resolve a the correct config.
*
* i.e. the plugin author can update or change defaults without breaking user configs
*
* Thus, we've described this plugin's config paths as a list. The order matters!
* Config data in earlier paths will be overwritten by configs from later paths.
*
* See [the original PHP implementation.](https://github.com/unraid/webgui/blob/95c6913c62e64314b985e08222feb3543113b2ec/emhttp/plugins/dynamix/include/Wrappers.php#L42)
*
* Here, the first path in the list is the default config.
* The second is the user-customized config.
*/
readonly dynamixConfig = [
resolvePath(
process.env.PATHS_DYNAMIX_CONFIG_DEFAULT ??

Check failure on line 39 in api/src/config/paths.config.ts

View workflow job for this annotation

GitHub Actions / Build API

Delete `⏎···············`

Check failure on line 39 in api/src/config/paths.config.ts

View workflow job for this annotation

GitHub Actions / Test API

Delete `⏎···············`
'/usr/local/emhttp/plugins/dynamix/default.cfg'
),
resolvePath(

Check failure on line 42 in api/src/config/paths.config.ts

View workflow job for this annotation

GitHub Actions / Build API

Replace `⏎············process.env.PATHS_DYNAMIX_CONFIG·??·'/boot/config/plugins/dynamix/dynamix.cfg'⏎········` with `process.env.PATHS_DYNAMIX_CONFIG·??·'/boot/config/plugins/dynamix/dynamix.cfg'`

Check failure on line 42 in api/src/config/paths.config.ts

View workflow job for this annotation

GitHub Actions / Test API

Replace `⏎············process.env.PATHS_DYNAMIX_CONFIG·??·'/boot/config/plugins/dynamix/dynamix.cfg'⏎········` with `process.env.PATHS_DYNAMIX_CONFIG·??·'/boot/config/plugins/dynamix/dynamix.cfg'`
process.env.PATHS_DYNAMIX_CONFIG ?? '/boot/config/plugins/dynamix/dynamix.cfg'
),
];

readonly myserversBase = '/boot/config/plugins/dynamix.my.servers/';
readonly myserversConfig = resolvePath(
process.env.PATHS_MY_SERVERS_CONFIG ??

Check failure on line 49 in api/src/config/paths.config.ts

View workflow job for this annotation

GitHub Actions / Build API

Delete `⏎···········`

Check failure on line 49 in api/src/config/paths.config.ts

View workflow job for this annotation

GitHub Actions / Test API

Delete `⏎···········`
'/boot/config/plugins/dynamix.my.servers/myservers.cfg'
);
readonly myserversConfigStates = join(
resolvePath(process.env.PATHS_STATES ?? '/usr/local/emhttp/state/'),
'myservers.cfg'
);
readonly myserversEnv = '/boot/config/plugins/dynamix.my.servers/env';
readonly myserversKeepalive =
process.env.PATHS_MY_SERVERS_FB ??

Check failure on line 58 in api/src/config/paths.config.ts

View workflow job for this annotation

GitHub Actions / Build API

Delete `⏎·······`

Check failure on line 58 in api/src/config/paths.config.ts

View workflow job for this annotation

GitHub Actions / Test API

Delete `⏎·······`
'/boot/config/plugins/dynamix.my.servers/fb_keepalive';
readonly keyfileBase = resolvePath(process.env.PATHS_KEYFILE_BASE ?? '/boot/config');
readonly machineId = resolvePath(process.env.PATHS_MACHINE_ID ?? '/var/lib/dbus/machine-id');
readonly logBase = resolvePath('/var/log/unraid-api/');
readonly unraidLogBase = resolvePath('/var/log/');
readonly varRun = '/var/run';
readonly authSessions = process.env.PATHS_AUTH_SESSIONS ?? '/var/lib/php';
readonly authKeys = resolvePath(
process.env.PATHS_AUTH_KEY ?? '/boot/config/plugins/dynamix.my.servers/keys'
);

// Singleton access
static getInstance(): PathsConfig {
if (!PathsConfig.instance) {
PathsConfig.instance = new PathsConfig();
}
return PathsConfig.instance;
}
}

Check failure on line 77 in api/src/config/paths.config.ts

View workflow job for this annotation

GitHub Actions / Build API

Replace `·` with `⏎`

Check failure on line 77 in api/src/config/paths.config.ts

View workflow job for this annotation

GitHub Actions / Build API

Newline required at end of file but not found

Check failure on line 77 in api/src/config/paths.config.ts

View workflow job for this annotation

GitHub Actions / Test API

Replace `·` with `⏎`

Check failure on line 77 in api/src/config/paths.config.ts

View workflow job for this annotation

GitHub Actions / Test API

Newline required at end of file but not found
9 changes: 9 additions & 0 deletions api/src/config/paths.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { PathsConfig } from './paths.config.js';

Check failure on line 2 in api/src/config/paths.module.ts

View workflow job for this annotation

GitHub Actions / Build API

Insert `⏎`

Check failure on line 2 in api/src/config/paths.module.ts

View workflow job for this annotation

GitHub Actions / Build API

import statements should have an absolute path

Check failure on line 2 in api/src/config/paths.module.ts

View workflow job for this annotation

GitHub Actions / Test API

Insert `⏎`

Check failure on line 2 in api/src/config/paths.module.ts

View workflow job for this annotation

GitHub Actions / Test API

import statements should have an absolute path

@Global()
@Module({
providers: [PathsConfig],
exports: [PathsConfig],
})
export class PathsModule {}

Check failure on line 9 in api/src/config/paths.module.ts

View workflow job for this annotation

GitHub Actions / Build API

Replace `·` with `⏎`

Check failure on line 9 in api/src/config/paths.module.ts

View workflow job for this annotation

GitHub Actions / Build API

Newline required at end of file but not found

Check failure on line 9 in api/src/config/paths.module.ts

View workflow job for this annotation

GitHub Actions / Test API

Replace `·` with `⏎`

Check failure on line 9 in api/src/config/paths.module.ts

View workflow job for this annotation

GitHub Actions / Test API

Newline required at end of file but not found
83 changes: 28 additions & 55 deletions api/src/core/modules/docker/get-docker-containers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import fs from 'fs';
import { promises as fs } from 'fs';

import camelCaseKeys from 'camelcase-keys';

Expand All @@ -9,6 +9,7 @@ import { catchHandlers } from '@app/core/utils/misc/catch-handlers.js';
import { ContainerPortType, ContainerState } from '@app/graphql/generated/api/types.js';
import { getters, store } from '@app/store/index.js';
import { updateDockerState } from '@app/store/modules/docker.js';
import { PathsConfig } from '../../../config/paths.config.js';

export interface ContainerListingOptions {
useCache?: boolean;
Expand All @@ -18,9 +19,7 @@ export interface ContainerListingOptions {
* Get all Docker containers.
* @returns All the in/active Docker containers on the system.
*/
export const getDockerContainers = async (
{ useCache }: ContainerListingOptions = { useCache: true }
): Promise<Array<DockerContainer>> => {
export const getDockerContainers = async ({ useCache = true }: ContainerListingOptions = {}): Promise<DockerContainer[]> => {
const dockerState = getters.docker();
if (useCache && dockerState.containers) {
dockerLogger.trace('Using docker container cache');
Expand All @@ -29,57 +28,31 @@ export const getDockerContainers = async (

dockerLogger.trace('Skipping docker container cache');

/**
* Docker auto start file
*
* @note Doesn't exist if array is offline.
* @see https://github.com/limetech/webgui/issues/502#issue-480992547
*/
const autoStartFile = await fs.promises
.readFile(getters.paths()['docker-autostart'], 'utf8')
.then((file) => file.toString())
.catch(() => '');
const autoStarts = autoStartFile.split('\n');
const rawContainers = await docker
.listContainers({
all: true,
size: true,
})
// If docker throws an error return no containers
.catch(catchHandlers.docker);

// Cleanup container object
const containers: Array<DockerContainer> = rawContainers.map((container) => {
const names = container.Names[0];
const containerData: DockerContainer = camelCaseKeys<DockerContainer>(
{
labels: container.Labels ?? {},
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()],
})),
command: container.Command,
created: container.Created,
mounts: container.Mounts,
networkSettings: container.NetworkSettings,
hostConfig: {
networkMode: container.HostConfig.NetworkMode,
},
id: container.Id,
image: container.Image,
status: container.Status,
},
{ deep: true }
);
return containerData;
});
const paths = PathsConfig.getInstance();
const autostartFile = await fs.readFile(paths.dockerAutostart, 'utf8').catch(() => '');
const autoStarts = autostartFile.split('\n');
const rawContainers = await docker.listContainers({ all: true }).catch(catchHandlers.docker);

const containers: DockerContainer[] = rawContainers.map((container) => ({
id: container.Id,
image: container.Image,
imageId: container.ImageID,
command: container.Command,
created: container.Created,
state: ContainerState[container.State.toUpperCase() as keyof typeof ContainerState],
status: container.Status,
ports: container.Ports.map((port) => ({
...port,
type: ContainerPortType[port.Type.toUpperCase() as keyof typeof ContainerPortType],
})) as ContainerPort[],
autoStart: autoStarts.includes(container.Names[0].split('/')[1]),
labels: container.Labels ?? {},
mounts: container.Mounts,
networkSettings: container.NetworkSettings,
hostConfig: {
networkMode: container.HostConfig.NetworkMode,
},
}));

// Get all of the current containers
const installed = containers.length;
Expand Down
4 changes: 3 additions & 1 deletion api/src/core/modules/get-parity-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { FileMissingError } from '@app/core/errors/file-missing-error.js';
import { type CoreContext, type CoreResult } from '@app/core/types/index.js';
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission.js';
import { getters } from '@app/store/index.js';
import { PathsConfig } from '../../config/paths.config.js';

/**
* Get parity history.
Expand All @@ -21,7 +22,8 @@ export const getParityHistory = async (context: CoreContext): Promise<CoreResult
possession: 'any',
});

const historyFilePath = getters.paths()['parity-checks'];
const paths = PathsConfig.getInstance();
const historyFilePath = paths.parityChecks;
const history = await fs.readFile(historyFilePath).catch(() => {
throw new FileMissingError(historyFilePath);
});
Expand Down
18 changes: 18 additions & 0 deletions api/src/core/types/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export enum StateFileKey {
DISPLAY = 'display',
DISKS = 'disks',
DOCKER = 'docker',
EMHTTP = 'emhttp',
IDENT = 'ident',
SHARES = 'shares',
SLOTS = 'slots',
USERS = 'users',
DEVICES = 'devices',
NETWORK = 'network',
NFS = 'nfs',
NGINX = 'nginx',
SMB = 'smb',
VAR = 'var',
SEC = 'sec',
NOTIFICATION = 'notification',
}
15 changes: 10 additions & 5 deletions api/src/core/utils/clients/docker.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import Docker from 'dockerode';
import { PathsConfig } from '../../../config/paths.config.js';

const socketPath = '/var/run/docker.sock';
const client = new Docker({
socketPath,
});
const createDockerClient = () => {
const paths = PathsConfig.getInstance();
const socketPath = paths.dockerSocket;
return new Docker({
socketPath,
});
};

/**
* Docker client
*/
export const docker = client;
export const docker = createDockerClient();
export { createDockerClient };
10 changes: 9 additions & 1 deletion api/src/core/utils/clients/emcmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import { logger } from '@app/core/log.js';
import { type LooseObject } from '@app/core/types/index.js';
import { DRY_RUN } from '@app/environment.js';
import { getters } from '@app/store/index.js';
import { PathsConfig } from '../../../config/paths.config.js';

/**
* Run a command with emcmd.
*/
export const emcmd = async (commands: LooseObject) => {
const socketPath = getters.paths()['emhttpd-socket'];
const paths = PathsConfig.getInstance();
const socketPath = paths.emhttpdSocket;
const { csrfToken } = getters.emhttp().var;

const url = `http://unix:${socketPath}:/update.htm`;
Expand Down Expand Up @@ -39,3 +41,9 @@ export const emcmd = async (commands: LooseObject) => {
throw error;
});
};

export const createEmcmdClient = () => {
const paths = PathsConfig.getInstance();
const socketPath = paths.emhttpdSocket;
// Rest of implementation
};
Comment on lines +45 to +49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Complete the implementation of createEmcmdClient

The function appears to be incomplete with just a comment indicating "Rest of implementation". Complete the function implementation or consider removing it if it's not needed yet.


🏁 Script executed:

#!/bin/bash
# Check if this function is called elsewhere in the codebase
echo "Checking for references to createEmcmdClient..."
rg "createEmcmdClient" --type ts

Length of output: 214


Action Required: Implement or remove createEmcmdClient

  • The function currently contains only a placeholder comment ("// Rest of implementation") and lacks a complete implementation.
  • Our search shows no external references to createEmcmdClient, suggesting it might not be used elsewhere.
  • Please either complete the function implementation if it’s needed or remove it to avoid unnecessary code clutter.

7 changes: 7 additions & 0 deletions api/src/core/utils/clients/emhttpd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { PathsConfig } from '../../../config/paths.config.js';

export const createEmhttpdClient = () => {
const paths = PathsConfig.getInstance();
const socketPath = paths.emhttpdSocket;
// Rest of implementation
};
Comment on lines +3 to +7
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Complete the implementation

The function contains a placeholder comment but lacks actual implementation.

Please complete the implementation of this client function.

7 changes: 7 additions & 0 deletions api/src/core/utils/clients/ssh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { PathsConfig } from '../../../config/paths.config.js';

export const createSshClient = () => {
const paths = PathsConfig.getInstance();
const keyPath = paths.keyfileBase;
// Rest of implementation
};
Comment on lines +3 to +7
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Complete the implementation

The function contains a placeholder comment but lacks actual implementation.

Please complete the implementation of this SSH client function.

10 changes: 9 additions & 1 deletion api/src/core/utils/misc/catch-handlers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AppError } from '@app/core/errors/app-error.js';
import { getters } from '@app/store/index.js';
import { PathsConfig } from '../../../config/paths.config.js';

interface DockerError extends NodeJS.ErrnoException {
address: string;
Expand All @@ -10,7 +11,8 @@ interface DockerError extends NodeJS.ErrnoException {
*/
export const catchHandlers = {
docker(error: DockerError) {
const socketPath = getters.paths()['docker-socket'];
const paths = PathsConfig.getInstance();
const socketPath = paths.dockerSocket;

// Throw custom error for docker socket missing
if (error.code === 'ENOENT' && error.address === socketPath) {
Expand All @@ -27,3 +29,9 @@ export const catchHandlers = {
throw error;
},
};

export const handleDockerError = (error: Error) => {
const paths = PathsConfig.getInstance();
const socketPath = paths.dockerSocket;
// Rest of implementation
};
Comment on lines +33 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Incomplete implementation for handleDockerError.

The function appears to be a stub with "Rest of implementation" as a comment. This should be completed before merging.

 export const handleDockerError = (error: Error) => {
     const paths = PathsConfig.getInstance();
     const socketPath = paths.dockerSocket;
-    // Rest of implementation
+    // Apply similar logic as in catchHandlers.docker
+    if (error instanceof Error && 'code' in error && 'address' in error) {
+        const dockerError = error as unknown as DockerError;
+        if (dockerError.code === 'ENOENT' && dockerError.address === socketPath) {
+            throw new AppError('Docker socket unavailable.');
+        }
+    }
+    throw error;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const handleDockerError = (error: Error) => {
const paths = PathsConfig.getInstance();
const socketPath = paths.dockerSocket;
// Rest of implementation
};
export const handleDockerError = (error: Error) => {
const paths = PathsConfig.getInstance();
const socketPath = paths.dockerSocket;
// Apply similar logic as in catchHandlers.docker
if (error instanceof Error && 'code' in error && 'address' in error) {
const dockerError = error as unknown as DockerError;
if (dockerError.code === 'ENOENT' && dockerError.address === socketPath) {
throw new AppError('Docker socket unavailable.');
}
}
throw error;
};

4 changes: 3 additions & 1 deletion api/src/core/utils/misc/get-machine-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { readFile } from 'fs/promises';

import { FileMissingError } from '@app/core/errors/file-missing-error.js';
import { getters } from '@app/store/index.js';
import { PathsConfig } from '../../../config/paths.config.js';

let machineId: string | null = null;

export const getMachineId = async (): Promise<string> => {
const path = getters.paths()['machine-id'];
const paths = PathsConfig.getInstance();
const path = paths.machineId;

if (machineId) {
return machineId;
Expand Down
7 changes: 7 additions & 0 deletions api/src/core/utils/misc/parse-state-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { promises as fs } from 'fs';
import { parse } from 'ini';

export const parseStateFile = async (path: string) => {
const content = await fs.readFile(path, 'utf8');
return parse(content);
};
6 changes: 3 additions & 3 deletions api/src/store/actions/load-dynamix-config-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { type DynamixConfig } from '@app/core/types/ini.js';
import { parseConfig } from '@app/core/utils/misc/parse-config.js';
import { type RecursiveNullable, type RecursivePartial } from '@app/types/index.js';
import { batchProcess } from '@app/utils.js';
import { PathsConfig } from '../../config/paths.config.js';

/**
* Loads a configuration file from disk, parses it to a RecursivePartial of the provided type, and returns it.
Expand Down Expand Up @@ -40,9 +41,8 @@ export const loadDynamixConfigFile = createAsyncThunk<
if (filePath) {
return loadConfigFile<DynamixConfig>(filePath);
}
const store = await import('@app/store/index.js');
const paths = store.getters.paths()['dynamix-config'];
const { data: configs } = await batchProcess(paths, (path) => loadConfigFile<DynamixConfig>(path));
const paths = PathsConfig.getInstance();
const { data: configs } = await batchProcess(paths.dynamixConfig, (path) => loadConfigFile<DynamixConfig>(path));
const [defaultConfig = {}, customConfig = {}] = configs;
return { ...defaultConfig, ...customConfig };
});
3 changes: 0 additions & 3 deletions api/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { dynamicRemoteAccessReducer } from '@app/store/modules/dynamic-remote-ac
import { dynamix } from '@app/store/modules/dynamix.js';
import { emhttp } from '@app/store/modules/emhttp.js';
import { mothership } from '@app/store/modules/minigraph.js';
import { paths } from '@app/store/modules/paths.js';
import { registration } from '@app/store/modules/registration.js';
import { remoteGraphQLReducer } from '@app/store/modules/remote-graphql.js';
import { upnp } from '@app/store/modules/upnp.js';
Expand All @@ -18,7 +17,6 @@ export const store = configureStore({
config: configReducer,
dynamicRemoteAccess: dynamicRemoteAccessReducer,
minigraph: mothership.reducer,
paths: paths.reducer,
emhttp: emhttp.reducer,
registration: registration.reducer,
remoteGraphQL: remoteGraphQLReducer,
Expand All @@ -45,7 +43,6 @@ export const getters = {
dynamix: () => store.getState().dynamix,
emhttp: () => store.getState().emhttp,
minigraph: () => store.getState().minigraph,
paths: () => store.getState().paths,
registration: () => store.getState().registration,
remoteGraphQL: () => store.getState().remoteGraphQL,
upnp: () => store.getState().upnp,
Expand Down
Loading