mirror of
https://github.com/meienberger/runtipi.git
synced 2024-08-17 08:00:26 +03:00
WIP - Install app
This commit is contained in:
parent
524a4e2e20
commit
a167cc4bc9
@ -8,6 +8,8 @@ packages:
|
||||
- base-devel
|
||||
- docker
|
||||
- avahi
|
||||
- nodejs
|
||||
- npm
|
||||
|
||||
username: nicolas
|
||||
|
||||
|
6
ansible/start.yml
Normal file
6
ansible/start.yml
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
- hosts: tipi
|
||||
become: yes
|
||||
|
||||
tasks:
|
||||
- import_tasks: ./tasks/common/system-api.yml
|
@ -70,4 +70,4 @@
|
||||
|
||||
- name: Enable UFW
|
||||
community.general.ufw:
|
||||
state: enabled
|
||||
state: enabled
|
||||
|
43
ansible/tasks/common/system-api.yml
Normal file
43
ansible/tasks/common/system-api.yml
Normal file
@ -0,0 +1,43 @@
|
||||
- name: Install "pm2" package globally.
|
||||
community.general.npm:
|
||||
name: yarn
|
||||
global: yes
|
||||
|
||||
- name: Run pm2 first time
|
||||
shell: pm2 list
|
||||
|
||||
- name: Enable pm2 as a service
|
||||
shell: sudo env PATH=$PATH:/usr/local/bin pm2 startup -u {{ username }}
|
||||
|
||||
- name: Create .env file in /system-api "{{ playbook_dir }}/../system-api/.env"
|
||||
copy:
|
||||
dest: "{{ playbook_dir }}/../system-api/.env"
|
||||
content: |
|
||||
ROOT_FOLDER={{ playbook_dir }}/../
|
||||
|
||||
- name: Install packages based on package.json.
|
||||
community.general.npm:
|
||||
path: "{{ playbook_dir }}/../system-api"
|
||||
|
||||
- name: Remove dist folder
|
||||
ansible.builtin.file:
|
||||
path: "{{ playbook_dir }}/../system-api/dist"
|
||||
state: absent
|
||||
|
||||
- name: Build system-api
|
||||
shell: cd {{ playbook_dir }}/../system-api && npm run build
|
||||
|
||||
- name: Check if app is already running
|
||||
become_user: "{{ username }}"
|
||||
shell: pm2 list
|
||||
register: pm2_result
|
||||
|
||||
- name: Start app
|
||||
become_user: "{{ username }}"
|
||||
shell: cd {{ playbook_dir }}/../system-api && pm2 start npm --name "system-api" -- start
|
||||
when: pm2_result.stdout.find("system-api") == -1
|
||||
|
||||
- name: Reload app
|
||||
become_user: "{{ username }}"
|
||||
shell: pm2 reload system-api
|
||||
when: pm2_result.stdout.find("system-api") != -1
|
22
apps/nextcloud/config.json
Normal file
22
apps/nextcloud/config.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Nextcloud",
|
||||
"description": "Nextcloud is a self-hosted, open source, and fully-featured cloud storage solution for your personal files, office documents, and photos.",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
@ -54,8 +54,8 @@ services:
|
||||
- MYSQL_PASSWORD=password
|
||||
- MYSQL_DATABASE=nextcloud
|
||||
- MYSQL_USER=nextcloud
|
||||
- NEXTCLOUD_ADMIN_USER=tipi
|
||||
- NEXTCLOUD_ADMIN_PASSWORD=password
|
||||
- NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER}
|
||||
- NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD}
|
||||
- NEXTCLOUD_TRUSTED_DOMAINS=tipi.local
|
||||
depends_on:
|
||||
- db-nextcloud
|
||||
@ -69,5 +69,3 @@ services:
|
||||
# traefik.http.routers.nextcloud.tls: true
|
||||
# traefik.http.routers.nextcloud.entrypoints: websecure
|
||||
# traefik.http.services.nextcloud.loadbalancer.server.port: 80
|
||||
|
||||
|
||||
|
@ -19,8 +19,6 @@ services:
|
||||
# networks:
|
||||
# - tipi_main_network
|
||||
|
||||
|
||||
|
||||
reverse-proxy:
|
||||
container_name: reverse-proxy
|
||||
image: traefik:v2.6
|
||||
|
@ -92,6 +92,7 @@ compose() {
|
||||
|
||||
docker-compose \
|
||||
--env-file "${env_file}" \
|
||||
--env-file "${app_dir}/.env" \
|
||||
--project-name "${app}" \
|
||||
--file "${app_compose_file}" \
|
||||
--file "${common_compose_file}" \
|
||||
@ -101,12 +102,6 @@ compose() {
|
||||
# Install new app
|
||||
if [[ "$command" = "install" ]]; then
|
||||
compose "${app}" pull
|
||||
|
||||
# # # Copy env file sample to .env
|
||||
# if [[ -f "${app_dir}/.env-sample" ]]; then
|
||||
# # Append to .env
|
||||
# echo "Copying .env-sample to .env for ${app} if not already done"
|
||||
# fi
|
||||
|
||||
compose "${app}" up -d
|
||||
exit
|
||||
|
@ -39,7 +39,7 @@ if ! command -v ansible-playbook > /dev/null; then
|
||||
sudo apt-get install -y ansible
|
||||
fi
|
||||
|
||||
ansible-playbook ansible/setup.yml -i ansible/hosts
|
||||
ansible-playbook ansible/setup.yml -i ansible/hosts -K
|
||||
|
||||
echo "Generating config files..."
|
||||
for template in "${ENV_FILE}"; do
|
||||
|
@ -23,6 +23,8 @@ if [[ ! -f "${STATE_FOLDER}/configured" ]]; then
|
||||
"${ROOT_FOLDER}/scripts/configure.sh"
|
||||
fi
|
||||
|
||||
ansible-playbook ansible/start.yml -i ansible/hosts -K
|
||||
|
||||
export DOCKER_CLIENT_TIMEOUT=240
|
||||
export COMPOSE_HTTP_TIMEOUT=240
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"installed": "nextcloud",
|
||||
"installed": "",
|
||||
"environment": {
|
||||
"anonaddy": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
136
system-api/.eslintrc.cjs
Normal file
136
system-api/.eslintrc.cjs
Normal file
@ -0,0 +1,136 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
],
|
||||
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,
|
||||
},
|
||||
};
|
3
system-api/.gitignore
vendored
3
system-api/.gitignore
vendored
@ -1 +1,2 @@
|
||||
node_modules
|
||||
node_modules/
|
||||
dist/
|
7
system-api/.prettierrc.cjs
Normal file
7
system-api/.prettierrc.cjs
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
singleQuote: true,
|
||||
semi: true,
|
||||
trailingComma: "all",
|
||||
arrowParens: "avoid",
|
||||
printWidth: 200,
|
||||
};
|
72
system-api/dist/server.js
vendored
72
system-api/dist/server.js
vendored
@ -1,72 +0,0 @@
|
||||
var __async = (__this, __arguments, generator) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
var fulfilled = (value) => {
|
||||
try {
|
||||
step(generator.next(value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
var rejected = (value) => {
|
||||
try {
|
||||
step(generator.throw(value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
||||
step((generator = generator.apply(__this, __arguments)).next());
|
||||
});
|
||||
};
|
||||
|
||||
// src/server.ts
|
||||
import express from "../node_modules/express/index.js";
|
||||
|
||||
// src/routes/system.routes.ts
|
||||
import { Router } from "../node_modules/express/index.js";
|
||||
|
||||
// src/controllers/system.controller.ts
|
||||
import si from "../node_modules/systeminformation/lib/index.js";
|
||||
var getCpuInfo = (req, res) => __async(void 0, null, function* () {
|
||||
const cpuLoad = yield si.currentLoad();
|
||||
res.status(200).send({ load: cpuLoad.currentLoad });
|
||||
});
|
||||
var getDiskInfo = (req, res) => __async(void 0, null, function* () {
|
||||
const disk = yield si.fsSize();
|
||||
const rootDisk = disk.find((item) => item.mount === "/");
|
||||
if (!rootDisk) {
|
||||
throw new Error("Could not find root disk");
|
||||
}
|
||||
const result = {
|
||||
size: rootDisk.size,
|
||||
used: rootDisk.used,
|
||||
available: rootDisk.available
|
||||
};
|
||||
res.status(200).send(result);
|
||||
});
|
||||
var getMemoryInfo = (req, res) => __async(void 0, null, function* () {
|
||||
const memory = yield si.mem();
|
||||
const result = {
|
||||
total: memory.total,
|
||||
free: memory.free,
|
||||
used: memory.used
|
||||
};
|
||||
res.status(200).json(result);
|
||||
});
|
||||
var system_controller_default = { getCpuInfo, getDiskInfo, getMemoryInfo };
|
||||
|
||||
// src/routes/system.routes.ts
|
||||
var router = Router();
|
||||
router.route("/cpu").get(system_controller_default.getCpuInfo);
|
||||
router.route("/disk").get(system_controller_default.getDiskInfo);
|
||||
router.route("/memory").get(system_controller_default.getMemoryInfo);
|
||||
var system_routes_default = router;
|
||||
|
||||
// src/server.ts
|
||||
var app = express();
|
||||
var port = 3001;
|
||||
app.use("/system", system_routes_default);
|
||||
app.listen(port, () => {
|
||||
console.log(`System API listening on port ${port}`);
|
||||
});
|
||||
//# sourceMappingURL=server.js.map
|
7
system-api/dist/server.js.map
vendored
7
system-api/dist/server.js.map
vendored
@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../src/server.ts", "../src/routes/system.routes.ts", "../src/controllers/system.controller.ts"],
|
||||
"sourcesContent": ["import express from \"express\";\nimport { systemRoutes } from \"./routes\";\n\nconst app = express();\nconst port = 3001;\n\napp.use(\"/system\", systemRoutes);\n\napp.listen(port, () => {\n console.log(`System API listening on port ${port}`);\n});\n", "import { Router } from \"express\";\nimport { SystemController } from \"../controllers\";\n\nconst router = Router();\n\nrouter.route(\"/cpu\").get(SystemController.getCpuInfo);\nrouter.route(\"/disk\").get(SystemController.getDiskInfo);\nrouter.route(\"/memory\").get(SystemController.getMemoryInfo);\n\nexport default router;\n", "import { Request, Response } from \"express\";\nimport si from \"systeminformation\";\n\ntype CpuData = {\n load: number;\n};\n\ntype DiskData = {\n size: number;\n used: number;\n available: number;\n};\n\ntype MemoryData = {\n total: number;\n free: number;\n used: number;\n};\n\n/**\n *\n * @param req\n * @param res\n */\nconst getCpuInfo = async (req: Request, res: Response<CpuData>) => {\n // const cpuInfo = await cpu.getCpuInfo();\n const cpuLoad = await si.currentLoad();\n\n res.status(200).send({ load: cpuLoad.currentLoad });\n};\n\n/**\n *\n * @param req\n * @param res\n */\nconst getDiskInfo = async (req: Request, res: Response<DiskData>) => {\n const disk = await si.fsSize();\n\n const rootDisk = disk.find((item) => item.mount === \"/\");\n\n if (!rootDisk) {\n throw new Error(\"Could not find root disk\");\n }\n\n const result: DiskData = {\n size: rootDisk.size,\n used: rootDisk.used,\n available: rootDisk.available,\n };\n\n res.status(200).send(result);\n};\n\n/**\n *\n * @param req\n * @param res\n */\nconst getMemoryInfo = async (req: Request, res: Response<MemoryData>) => {\n const memory = await si.mem();\n\n const result: MemoryData = {\n total: memory.total,\n free: memory.free,\n used: memory.used,\n };\n\n res.status(200).json(result);\n};\n\nexport default { getCpuInfo, getDiskInfo, getMemoryInfo };\n"],
|
||||
"mappings": ";;;;;;;;;;;;;;;;;;;;;;AAAA;;;ACAA;;;ACCA;AAuBA,IAAM,aAAa,CAAO,KAAc,QAA2B;AAEjE,QAAM,UAAU,MAAM,GAAG,YAAY;AAErC,MAAI,OAAO,GAAG,EAAE,KAAK,EAAE,MAAM,QAAQ,YAAY,CAAC;AACpD;AAOA,IAAM,cAAc,CAAO,KAAc,QAA4B;AACnE,QAAM,OAAO,MAAM,GAAG,OAAO;AAE7B,QAAM,WAAW,KAAK,KAAK,CAAC,SAAS,KAAK,UAAU,GAAG;AAEvD,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,MAAM,0BAA0B;AAAA,EAC5C;AAEA,QAAM,SAAmB;AAAA,IACvB,MAAM,SAAS;AAAA,IACf,MAAM,SAAS;AAAA,IACf,WAAW,SAAS;AAAA,EACtB;AAEA,MAAI,OAAO,GAAG,EAAE,KAAK,MAAM;AAC7B;AAOA,IAAM,gBAAgB,CAAO,KAAc,QAA8B;AACvE,QAAM,SAAS,MAAM,GAAG,IAAI;AAE5B,QAAM,SAAqB;AAAA,IACzB,OAAO,OAAO;AAAA,IACd,MAAM,OAAO;AAAA,IACb,MAAM,OAAO;AAAA,EACf;AAEA,MAAI,OAAO,GAAG,EAAE,KAAK,MAAM;AAC7B;AAEA,IAAO,4BAAQ,EAAE,YAAY,aAAa,cAAc;;;ADpExD,IAAM,SAAS,OAAO;AAEtB,OAAO,MAAM,MAAM,EAAE,IAAI,0BAAiB,UAAU;AACpD,OAAO,MAAM,OAAO,EAAE,IAAI,0BAAiB,WAAW;AACtD,OAAO,MAAM,SAAS,EAAE,IAAI,0BAAiB,aAAa;AAE1D,IAAO,wBAAQ;;;ADNf,IAAM,MAAM,QAAQ;AACpB,IAAM,OAAO;AAEb,IAAI,IAAI,WAAW,qBAAY;AAE/B,IAAI,OAAO,MAAM,MAAM;AACrB,UAAQ,IAAI,gCAAgC,MAAM;AACpD,CAAC;",
|
||||
"names": []
|
||||
}
|
4861
system-api/package-lock.json
generated
Normal file
4861
system-api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -9,17 +9,22 @@
|
||||
"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",
|
||||
"start:dev": "NODE_ENV=development nodemon --trace-deprecation --trace-warnings --watch dist dist/server.js",
|
||||
"dev": "concurrently \"yarn build:watch\" \"yarn start:dev\""
|
||||
"dev": "concurrently \"yarn build:watch\" \"yarn start:dev\"",
|
||||
"start": "NODE_ENV=production node dist/server.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"compression": "^1.7.4",
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.17.3",
|
||||
"helmet": "^5.0.2",
|
||||
"node-port-scanner": "^3.0.1",
|
||||
"public-ip": "^5.0.0",
|
||||
"systeminformation": "^5.11.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/express": "^4.17.13",
|
||||
"concurrently": "^7.1.0",
|
||||
"esbuild": "^0.14.32",
|
||||
|
25
system-api/src/config/config.ts
Normal file
25
system-api/src/config/config.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
interface IConfig {
|
||||
NODE_ENV: string;
|
||||
ROOT_FOLDER: string;
|
||||
}
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const { NODE_ENV = 'development', ROOT_FOLDER = '' } = process.env;
|
||||
|
||||
const missing = [];
|
||||
|
||||
if (!ROOT_FOLDER) missing.push('ROOT_FOLDER');
|
||||
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`Missing environment variables: ${missing.join(', ')}`);
|
||||
}
|
||||
|
||||
const config: IConfig = {
|
||||
NODE_ENV,
|
||||
ROOT_FOLDER,
|
||||
};
|
||||
|
||||
export default config;
|
1
system-api/src/config/index.ts
Normal file
1
system-api/src/config/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './config';
|
33
system-api/src/config/types.ts
Normal file
33
system-api/src/config/types.ts
Normal file
@ -0,0 +1,33 @@
|
||||
// "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;
|
||||
max?: number;
|
||||
min?: number;
|
||||
required?: boolean;
|
||||
env_variable?: string;
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
form_fields: Record<string, FormField[]>;
|
||||
}
|
5
system-api/src/constants/constants.ts
Normal file
5
system-api/src/constants/constants.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import config from '../config';
|
||||
|
||||
export const APP_DATA_FOLDER = 'app-data';
|
||||
export const APPS_FOLDER = 'apps';
|
||||
export const __prod__ = config.NODE_ENV === 'production';
|
184
system-api/src/controllers/app.controller.ts
Normal file
184
system-api/src/controllers/app.controller.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import { Request, Response } from 'express';
|
||||
import fs from 'fs';
|
||||
import process from 'child_process';
|
||||
import config from '../config';
|
||||
import { AppConfig } from '../config/types';
|
||||
|
||||
const appScript = `${config.ROOT_FOLDER}/scripts/app.sh`;
|
||||
|
||||
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 generateEnvFile = (appName: string, form: Record<string, string>) => {
|
||||
const appFolder = `${config.ROOT_FOLDER}/apps/${appName}`;
|
||||
const appDataFolder = `${config.ROOT_FOLDER}/app-data/${appName}`;
|
||||
|
||||
const appExists = fs.existsSync(appDataFolder);
|
||||
|
||||
if (!appExists) {
|
||||
throw new Error(`App ${appName} not installed`);
|
||||
}
|
||||
|
||||
const rawFile = fs.readFileSync(`${appFolder}/config.json`).toString();
|
||||
let configFile: AppConfig = JSON.parse(rawFile);
|
||||
let envFile = '';
|
||||
|
||||
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) {
|
||||
throw new Error(`Variable ${key} is required`);
|
||||
}
|
||||
});
|
||||
|
||||
fs.writeFileSync(`${appDataFolder}/.env`, envFile);
|
||||
};
|
||||
|
||||
const installApp = (req: Request, res: Response) => {
|
||||
try {
|
||||
const { appName, form } = req.body;
|
||||
|
||||
if (!appName) {
|
||||
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);
|
||||
|
||||
if (appExists) {
|
||||
throw new Error(`App ${appName} already installed`);
|
||||
}
|
||||
|
||||
// Create app folder
|
||||
fs.mkdirSync(appFolder);
|
||||
|
||||
// Copy default app files from app-data folder
|
||||
fs.copyFileSync(`${appFolder}/data`, `${appDataFolder}/data`);
|
||||
|
||||
generateEnvFile(appName, form);
|
||||
|
||||
const state = getStateFile();
|
||||
state.installed += ` ${appName}`;
|
||||
|
||||
fs.writeFileSync(`${config.ROOT_FOLDER}/state/apps.json`, JSON.stringify(state));
|
||||
|
||||
// Run script
|
||||
process.spawnSync(appScript, ['install', appName], {});
|
||||
|
||||
res.status(200).json({ message: 'App installed successfully' });
|
||||
} catch (e) {
|
||||
res.status(500).send(e);
|
||||
}
|
||||
};
|
||||
|
||||
const uninstallApp = (req: Request, res: Response) => {
|
||||
try {
|
||||
const { appName } = req.body;
|
||||
|
||||
if (!appName) {
|
||||
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);
|
||||
|
||||
if (!appExists) {
|
||||
throw new Error(`App ${appName} not installed`);
|
||||
}
|
||||
|
||||
// Delete app folder
|
||||
fs.rmdirSync(appFolder, { recursive: true });
|
||||
|
||||
// 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));
|
||||
|
||||
// Run script
|
||||
process.spawnSync(appScript, ['uninstall', appName], {});
|
||||
|
||||
res.status(200).json({ message: 'App uninstalled successfully' });
|
||||
} catch (e) {
|
||||
res.status(500).send(e);
|
||||
}
|
||||
};
|
||||
|
||||
const stopApp = (req: Request, res: Response) => {
|
||||
try {
|
||||
const { appName } = req.body;
|
||||
|
||||
if (!appName) {
|
||||
throw new Error('App name is required');
|
||||
}
|
||||
|
||||
const appDataFolder = `${config.ROOT_FOLDER}/app-data/${appName}`;
|
||||
|
||||
const appExists = fs.existsSync(appDataFolder);
|
||||
|
||||
if (!appExists) {
|
||||
throw new Error(`App ${appName} not installed`);
|
||||
}
|
||||
|
||||
// Run script
|
||||
process.spawnSync(appScript, ['stop', appName], {});
|
||||
|
||||
res.status(200).json({ message: 'App stopped successfully' });
|
||||
} catch (e) {
|
||||
res.status(500).send(e);
|
||||
}
|
||||
};
|
||||
|
||||
const updateAppConfig = (req: Request, res: Response) => {
|
||||
try {
|
||||
const { appName, form } = req.body;
|
||||
|
||||
if (!appName) {
|
||||
throw new Error('App name is required');
|
||||
}
|
||||
|
||||
const appDataFolder = `${config.ROOT_FOLDER}/app-data/${appName}`;
|
||||
|
||||
const appExists = fs.existsSync(appDataFolder);
|
||||
|
||||
if (!appExists) {
|
||||
throw new Error(`App ${appName} not installed`);
|
||||
}
|
||||
|
||||
generateEnvFile(appName, form);
|
||||
|
||||
// Run script
|
||||
process.spawnSync(appScript, ['stop', appName], {});
|
||||
process.spawnSync(appScript, ['start', appName], {});
|
||||
|
||||
res.status(200).json({ message: 'App updated successfully' });
|
||||
} catch (e) {
|
||||
res.status(500).send(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 appNames = apps.installed.split(' ');
|
||||
|
||||
res.status(200).json(appNames);
|
||||
} catch (e) {
|
||||
res.status(500).send(e);
|
||||
}
|
||||
};
|
||||
|
||||
export default { uninstallApp, installApp, stopApp, updateAppConfig, installedApps };
|
@ -1 +1,2 @@
|
||||
export { default as SystemController } from "./system.controller";
|
||||
export { default as SystemController } from './system.controller';
|
||||
export { default as AppController } from './app.controller';
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Request, Response } from "express";
|
||||
import si from "systeminformation";
|
||||
import { Request, Response } from 'express';
|
||||
import si from 'systeminformation';
|
||||
|
||||
type CpuData = {
|
||||
load: number;
|
||||
@ -37,10 +37,10 @@ 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");
|
||||
throw new Error('Could not find root disk');
|
||||
}
|
||||
|
||||
const result: DiskData = {
|
||||
|
10
system-api/src/routes/app.routes.ts
Normal file
10
system-api/src/routes/app.routes.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Router } from 'express';
|
||||
import { AppController } from '../controllers';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.route('/install').post(AppController.installApp);
|
||||
router.route('/uninstall').post(AppController.uninstallApp);
|
||||
router.route('/list').get(AppController.installedApps);
|
||||
|
||||
export default router;
|
@ -1 +1,2 @@
|
||||
export { default as systemRoutes } from "./system.routes";
|
||||
export { default as systemRoutes } from './system.routes';
|
||||
export { default as appRoutes } from './app.routes';
|
||||
|
@ -1,10 +1,19 @@
|
||||
import express from "express";
|
||||
import { systemRoutes } from "./routes";
|
||||
import express from 'express';
|
||||
import compression from 'compression';
|
||||
import helmet from 'helmet';
|
||||
import { __prod__ } from './constants/constants';
|
||||
import { appRoutes, systemRoutes } from './routes';
|
||||
|
||||
const app = express();
|
||||
const port = 3001;
|
||||
|
||||
app.use("/system", systemRoutes);
|
||||
if (__prod__) {
|
||||
app.use(compression());
|
||||
app.use(helmet());
|
||||
}
|
||||
|
||||
app.use('/system', systemRoutes);
|
||||
app.use('/app', appRoutes);
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`System API listening on port ${port}`);
|
||||
|
File diff suppressed because it is too large
Load Diff
1623
system-api/yarn.lock
1623
system-api/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user