add fs helpers

This commit is contained in:
Nicolas Meienberger 2022-04-09 15:08:55 +02:00
parent 1283457268
commit 4e5bf34ece
21 changed files with 19568 additions and 234 deletions

3
system-api/.eslintignore Normal file
View File

@ -0,0 +1,3 @@
node_modules/
dist/
*.cjs

View File

@ -1,136 +1,16 @@
module.exports = {
root: true,
env: {
node: true,
},
env: { node: true },
extends: ['airbnb-base', 'eslint:recommended', 'plugin:import/typescript'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: ['./tsconfig.json'],
ecmaVersion: 'latest',
sourceType: 'module',
},
extends: ['plugin:prettier/recommended', 'airbnb-typescript', 'plugin:sonarjs/recommended', 'plugin:@typescript-eslint/recommended', 'plugin:unicorn/recommended', 'hardcore'],
plugins: ['prettier', '@typescript-eslint', 'no-loops', 'sonarjs', 'deprecate', 'no-secrets', 'jest', 'react'],
overrides: [
{
files: ['**/*.test.ts', '**/*.test.tsx', 'jest.setup.ts', 'jest.config.js'],
rules: {
'import/unambiguous': 0,
'unicorn/consistent-function-scoping': 0,
},
env: {
jest: true,
},
},
{
files: ['**/*.d.ts'],
rules: {
'import/unambiguous': 0,
},
},
],
plugins: ['@typescript-eslint', 'import'],
rules: {
'max-statements': 0,
camelcase: 0,
'unicorn/prefer-node-protocol': 0,
'newline-per-chained-call': 0,
'new-cap': 0,
'security/detect-non-literal-regexp': 0,
'promise/avoid-new': 0,
'import/no-commonjs': 0,
'unicorn/prefer-module': 0,
'@typescript-eslint/no-var-requires': 0,
'security/detect-unsafe-regex': 0,
'unicorn/no-unsafe-regex': 0,
'no-param-reassign': ['error', { props: true, ignorePropertyModificationsFor: ['^draft'] }],
'unicorn/no-array-callback-reference': 0,
'import/no-namespace': 0,
'unicorn/no-null': 0,
'unicorn/no-useless-undefined': 0,
'import/max-dependencies': 0,
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': ['error'],
'unicorn/no-array-for-each': 0,
'unicorn/prevent-abbreviations': 0,
'import/order': 0,
'import/extensions': 0,
'ext/lines-between-object-properties': 0,
'putout/putout': 0,
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
'deprecate/function': 1,
'deprecate/member-expression': 1,
'deprecate/import': 1,
'no-secrets/no-secrets': 'error',
'arrow-body-style': 0,
semi: 0,
'@typescript-eslint/semi': 0,
'@typescript-eslint/indent': 0,
'implicit-arrow-linebreak': 0,
'function-paren-newline': 0,
'operator-linebreak': 0,
'import/no-unused-modules': [1, { unusedExports: true }],
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: true,
},
],
quotes: ['warn', 'single'],
'no-restricted-syntax': 2,
'no-await-in-loop': 2,
'object-curly-newline': 0,
'no-constant-condition': 2,
'no-mixed-operators': 1,
'no-console': ['error', { allow: ['warn', 'error'] }],
'no-underscore-dangle': 0,
'no-global-assign': 2,
'prefer-const': [
'error',
{
destructuring: 'any',
ignoreReadBeforeAssign: false,
},
],
'import/prefer-default-export': 0,
'import/no-named-as-default': 0,
'max-lines': [
'error',
{
max: 200,
skipBlankLines: true,
skipComments: true,
},
],
'max-len': [
2,
200,
{
ignoreComments: true,
ignoreTemplateLiterals: true,
ignoreStrings: true,
},
],
curly: 0,
'arrow-parens': 0,
'no-return-assign': 2,
'comma-dangle': 0,
'no-multi-str': 0,
'newline-before-return': 2,
'newline-after-var': 2,
'newline-per-chained-call': 2,
'import/newline-after-import': 2,
'no-loops/no-loops': 2,
'jest/no-disabled-tests': 'warn',
'jest/no-focused-tests': 'error',
'jest/no-identical-title': 'error',
'jest/prefer-to-have-length': 'warn',
'jest/valid-expect': 'error',
'id-length': 0,
'no-magic-numbers': 0,
'unicorn/prefer-type-error': 0,
'unicorn/no-array-method-this-argument': 0,
'no-shadow': 'off',
'@typescript-eslint/no-shadow': ['error'],
},
globals: {
JSX: true,
'no-restricted-exports': 0,
'max-len': [{ code: 200 }],
'import/extensions': ['error', 'ignorePackages', { js: 'never', jsx: 'never', ts: 'never', tsx: 'never' }],
},
};

View File

@ -1,7 +1,6 @@
module.exports = {
singleQuote: true,
semi: true,
trailingComma: "all",
arrowParens: "avoid",
trailingComma: 'all',
printWidth: 200,
};

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@
"main": "src/server.ts",
"type": "module",
"scripts": {
"lint": "eslint . --ext .ts",
"test": "echo \"Error: no test specified\" && exit 1",
"build": "esbuild --bundle src/server.ts --outdir=dist --allow-overwrite --sourcemap --platform=node --minify --analyze=verbose --external:./node_modules/* --format=esm",
"build:watch": "esbuild --bundle src/server.ts --outdir=dist --allow-overwrite --sourcemap --platform=node --external:./node_modules/* --format=esm --watch",
@ -26,8 +27,17 @@
"devDependencies": {
"@types/compression": "^1.7.2",
"@types/express": "^4.17.13",
"@types/validator": "^13.7.2",
"concurrently": "^7.1.0",
"esbuild": "^0.14.32",
"nodemon": "^2.0.15"
"eslint": "^8.13.0",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-hardcore": "^24.5.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-unicorn": "^42.0.0",
"nodemon": "^2.0.15",
"prettier": "2.6.2"
}
}

View File

@ -1,21 +1,3 @@
// "form_fields": {
// "username": {
// "type": "text",
// "label": "Username",
// "max": 50,
// "min": 3,
// "required": true,
// "env_variable": "NEXTCLOUD_USERNAME"
// },
// "password": {
// "type": "password",
// "label": "Password",
// "max": 50,
// "min": 3,
// "required": true,
// "env_variable": "NEXTCLOUD_PASSWORD"
// }
interface FormField {
type: string;
label: string;
@ -29,5 +11,5 @@ export interface AppConfig {
name: string;
description: string;
version: string;
form_fields: Record<string, FormField[]>;
form_fields: Record<string, FormField>;
}

View File

@ -2,4 +2,4 @@ import config from '../config';
export const APP_DATA_FOLDER = 'app-data';
export const APPS_FOLDER = 'apps';
export const __prod__ = config.NODE_ENV === 'production';
export const isProd = config.NODE_ENV === 'production';

View File

@ -1,2 +0,0 @@
export { default as SystemController } from './system.controller';
export { default as AppController } from './app.controller';

View File

@ -1,15 +0,0 @@
import { Request, Response } from "express";
import publicIp from "public-ip";
import portScanner from "node-port-scanner";
const isPortOpen = async (req: Request, res: Response<boolean>) => {
const port = req.params.port;
const host = await publicIp.v4();
const isOpen = await portScanner(host, [port]);
console.log(port);
res.status(200).send(isOpen);
};

View File

@ -0,0 +1,3 @@
const objectKeys = <T>(obj: T): (keyof T)[] => Object.keys(obj) as (keyof T)[];
export default { objectKeys };

View File

@ -1,45 +1,37 @@
import { Request, Response } from 'express';
import fs from 'fs';
import process from 'child_process';
import config from '../config';
import { AppConfig } from '../config/types';
import config from '../../config';
import { AppConfig } from '../../config/types';
import { createFolder, fileExists, readJsonFile, writeFile, copyFile, runScript, deleteFolder } from '../fs/fs.helpers';
const appScript = `${config.ROOT_FOLDER}/scripts/app.sh`;
type AppsState = { installed: string };
const getAppFolder = (appName: string) => `${config.ROOT_FOLDER}/apps/${appName}`;
const getDataFolder = (appName: string) => `${config.ROOT_FOLDER}/app-data/${appName}`;
const getStateFile = () => {
// Add app to apps.json
const rawFile = fs.readFileSync(`${config.ROOT_FOLDER}/state/apps.json`).toString();
let apps = JSON.parse(rawFile);
return apps;
const getStateFile = (): AppsState => {
return readJsonFile('/state/apps.json');
};
const generateEnvFile = (appName: string, form: Record<string, string>) => {
const appExists = fs.existsSync(getDataFolder(appName));
const appExists = fileExists(`/app-data/${appName}`);
if (!appExists) {
throw new Error(`App ${appName} not installed`);
}
const rawFile = fs.readFileSync(`${getAppFolder(appName)}/config.json`).toString();
let configFile: AppConfig = JSON.parse(rawFile);
const configFile: AppConfig = readJsonFile(`/apps/${appName}/config.json`);
let envFile = '';
Object.keys(configFile.form_fields).forEach(key => {
Object.keys(configFile.form_fields).forEach((key) => {
const value = form[key];
if (value) {
const envVar = configFile.form_fields[key].env_variable;
envFile += `${envVar}=${value}\n`;
} else if (configFile[key].required) {
} else if (configFile.form_fields[key].required) {
throw new Error(`Variable ${key} is required`);
}
});
fs.writeFileSync(`${getDataFolder(appName)}/.env`, envFile);
writeFile(`/app-data/${appName}/.env`, envFile);
};
const installApp = (req: Request, res: Response) => {
@ -50,30 +42,25 @@ const installApp = (req: Request, res: Response) => {
throw new Error('App name is required');
}
const appDataFolder = `${config.ROOT_FOLDER}/app-data/${appName}`;
const appFolder = `${config.ROOT_FOLDER}/apps/${appName}`;
const appExists = fs.existsSync(appDataFolder);
const appExists = fileExists(`/app-data/${appName}`);
if (appExists) {
throw new Error(`App ${appName} already installed`);
}
// Create app folder
fs.mkdirSync(appFolder);
createFolder(`/app-data/${appName}`);
// Copy default app files from app-data folder
fs.copyFileSync(`${appFolder}/data`, `${appDataFolder}/data`);
copyFile(`/apps/${appName}/data`, `/app-data/${appName}/data`);
// Create env file
generateEnvFile(appName, form);
const state = getStateFile();
state.installed += ` ${appName}`;
fs.writeFileSync(`${config.ROOT_FOLDER}/state/apps.json`, JSON.stringify(state));
writeFile('/state/apps.json', JSON.stringify(state));
// Run script
process.spawnSync(appScript, ['install', appName], {});
runScript('/scripts/app.sh', ['install', appName]);
res.status(200).json({ message: 'App installed successfully' });
} catch (e) {
@ -88,22 +75,23 @@ const uninstallApp = (req: Request, res: Response) => {
if (!appName) {
throw new Error('App name is required');
}
const appExists = fs.existsSync(getDataFolder(appName));
const appExists = fileExists(`/app-data/${appName}`);
if (!appExists) {
throw new Error(`App ${appName} not installed`);
}
// Delete app folder
fs.rmdirSync(getAppFolder(appName), { recursive: true });
deleteFolder(`/app-data/${appName}`);
// Remove app from apps.json
const state = getStateFile();
state.installed = state.installed.replace(` ${appName}`, '');
fs.writeFileSync(`${config.ROOT_FOLDER}/state/apps.json`, JSON.stringify(state));
writeFile('/state/apps.json', JSON.stringify(state));
// Run script
process.spawnSync(appScript, ['uninstall', appName], {});
runScript('/scripts/app.sh', ['uninstall', appName]);
res.status(200).json({ message: 'App uninstalled successfully' });
} catch (e) {
@ -119,18 +107,18 @@ const stopApp = (req: Request, res: Response) => {
throw new Error('App name is required');
}
const appExists = fs.existsSync(getDataFolder(appName));
const appExists = fileExists(`/app-data/${appName}`);
if (!appExists) {
throw new Error(`App ${appName} not installed`);
}
// Run script
process.spawnSync(appScript, ['stop', appName], {});
runScript('/scripts/app.sh', ['stop', appName]);
res.status(200).json({ message: 'App stopped successfully' });
} catch (e) {
res.status(500).send(e);
res.status(500).end(e);
}
};
@ -142,7 +130,7 @@ const updateAppConfig = (req: Request, res: Response) => {
throw new Error('App name is required');
}
const appExists = fs.existsSync(getDataFolder(appName));
const appExists = fileExists(`/app-data/${appName}`);
if (!appExists) {
throw new Error(`App ${appName} not installed`);
@ -151,26 +139,49 @@ const updateAppConfig = (req: Request, res: Response) => {
generateEnvFile(appName, form);
// Run script
process.spawnSync(appScript, ['stop', appName], {});
process.spawnSync(appScript, ['start', appName], {});
runScript('/scripts/app.sh', ['stop', appName]);
runScript('/scripts/app.sh', ['start', appName]);
res.status(200).json({ message: 'App updated successfully' });
} catch (e) {
res.status(500).send(e);
res.status(500).end(e);
}
};
const installedApps = (req: Request, res: Response) => {
try {
const rawFile = fs.readFileSync(`${config.ROOT_FOLDER}/state/apps.json`).toString();
const apps = JSON.parse(rawFile);
const apps = readJsonFile('/state/apps.json');
const appNames = apps.installed.split(' ');
res.status(200).json(appNames);
} catch (e) {
res.status(500).send(e);
res.status(500).end(e);
}
};
export default { uninstallApp, installApp, stopApp, updateAppConfig, installedApps };
const getAppInfo = (req: Request, res: Response<AppConfig>) => {
try {
const { appName } = req.body;
if (!appName) {
throw new Error('App name is required');
}
const configFile: AppConfig = readJsonFile(`/apps/${appName}/config.json`);
res.status(200).json(configFile);
} catch (e) {
res.status(500).end(e);
}
};
const AppController = {
uninstallApp,
installApp,
stopApp,
updateAppConfig,
installedApps,
getAppInfo,
};
export default AppController;

View File

@ -1,5 +1,5 @@
import { Router } from 'express';
import { AppController } from '../controllers';
import AppController from './apps.controller';
const router = Router();

View File

@ -0,0 +1,21 @@
import fs from 'fs';
import childProcess from 'child_process';
import config from '../../config';
export const getAbsolutePath = (path: string) => `${config.ROOT_FOLDER}${path}`;
export const readJsonFile = (path: string): any => {
const rawFile = fs.readFileSync(getAbsolutePath(path)).toString();
return JSON.parse(rawFile);
};
export const fileExists = (path: string): boolean => fs.existsSync(getAbsolutePath(path));
export const writeFile = (path: string, data: any) => fs.writeFileSync(getAbsolutePath(path), data);
export const createFolder = (path: string) => fs.mkdirSync(getAbsolutePath(path));
export const deleteFolder = (path: string) => fs.rmSync(getAbsolutePath(path), { recursive: true });
export const copyFile = (source: string, destination: string) => fs.copyFileSync(getAbsolutePath(source), getAbsolutePath(destination));
export const runScript = (path: string, args: string[]) => childProcess.spawnSync(getAbsolutePath(path), args, {});

View File

@ -0,0 +1,19 @@
import { Request, Response } from 'express';
import publicIp from 'public-ip';
import portScanner from 'node-port-scanner';
const isPortOpen = async (req: Request, res: Response<boolean>) => {
const { port } = req.params;
const host = await publicIp.v4();
const isOpen = await portScanner(host, [port]);
res.status(200).send(isOpen);
};
const NetworkController = {
isPortOpen,
};
export default NetworkController;

View File

@ -37,7 +37,7 @@ const getCpuInfo = async (req: Request, res: Response<CpuData>) => {
const getDiskInfo = async (req: Request, res: Response<DiskData>) => {
const disk = await si.fsSize();
const rootDisk = disk.find(item => item.mount === '/');
const rootDisk = disk.find((item) => item.mount === '/');
if (!rootDisk) {
throw new Error('Could not find root disk');

View File

@ -0,0 +1,10 @@
import { Router } from 'express';
import SystemController from './system.controller';
const router = Router();
router.route('/cpu').get(SystemController.getCpuInfo);
router.route('/disk').get(SystemController.getDiskInfo);
router.route('/memory').get(SystemController.getMemoryInfo);
export default router;

View File

@ -1,2 +0,0 @@
export { default as systemRoutes } from './system.routes';
export { default as appRoutes } from './app.routes';

View File

@ -1,10 +0,0 @@
import { Router } from "express";
import { SystemController } from "../controllers";
const router = Router();
router.route("/cpu").get(SystemController.getCpuInfo);
router.route("/disk").get(SystemController.getDiskInfo);
router.route("/memory").get(SystemController.getMemoryInfo);
export default router;

View File

@ -1,19 +1,20 @@
import express from 'express';
import compression from 'compression';
import helmet from 'helmet';
import { __prod__ } from './constants/constants';
import { appRoutes, systemRoutes } from './routes';
import { isProd } from './constants/constants';
import appsRoutes from './modules/apps/apps.routes';
import systemRoutes from './modules/system/system.routes';
const app = express();
const port = 3001;
if (__prod__) {
if (isProd) {
app.use(compression());
app.use(helmet());
}
app.use('/system', systemRoutes);
app.use('/app', appRoutes);
app.use('/app', appsRoutes);
app.listen(port, () => {
console.log(`System API listening on port ${port}`);

View File

@ -15,6 +15,6 @@
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs"],
"exclude": ["node_modules"]
}