Bläddra i källkod

refactor: replace usage of config with new runtime config

wip: make script executable from everywhere
Nicolas Meienberger 2 år sedan
förälder
incheckning
78cb3c36ad
32 ändrade filer med 272 tillägg och 297 borttagningar
  1. 1 1
      .github/workflows/ci.yml
  2. 1 1
      docker-compose.dev.yml
  3. 1 1
      docker-compose.rc.yml
  4. 1 1
      docker-compose.yml
  5. 3 2
      packages/dashboard/next.config.js
  6. 1 1
      packages/dashboard/src/core/helpers/url-helpers.ts
  7. 9 9
      packages/dashboard/src/hooks/useCachedRessources.ts
  8. 0 58
      packages/system-api/src/config/config.ts
  9. 0 1
      packages/system-api/src/config/index.ts
  10. 9 7
      packages/system-api/src/config/logger/logger.ts
  11. 2 2
      packages/system-api/src/constants/constants.ts
  12. 20 10
      packages/system-api/src/core/config/TipiConfig.ts
  13. 2 2
      packages/system-api/src/core/jobs/jobs.ts
  14. 2 2
      packages/system-api/src/core/middlewares/sessionMiddleware.ts
  15. 9 8
      packages/system-api/src/core/updates/__tests__/v040.test.ts
  16. 2 2
      packages/system-api/src/core/updates/v040.ts
  17. 10 10
      packages/system-api/src/modules/apps/__tests__/apps.factory.ts
  18. 4 4
      packages/system-api/src/modules/apps/__tests__/apps.helpers.test.ts
  19. 24 24
      packages/system-api/src/modules/apps/__tests__/apps.service.test.ts
  20. 13 13
      packages/system-api/src/modules/apps/apps.helpers.ts
  21. 3 3
      packages/system-api/src/modules/apps/apps.service.ts
  22. 21 21
      packages/system-api/src/modules/fs/__tests__/fs.helpers.test.ts
  23. 3 3
      packages/system-api/src/modules/fs/fs.helpers.ts
  24. 3 3
      packages/system-api/src/modules/system/system.service.ts
  25. 22 8
      packages/system-api/src/server.ts
  26. 18 65
      scripts/app.sh
  27. 0 12
      scripts/configure.sh
  28. 8 6
      scripts/git.sh
  29. 26 1
      scripts/start.sh
  30. 6 10
      scripts/stop.sh
  31. 13 6
      scripts/system-info.sh
  32. 35 0
      scripts/utils.sh

+ 1 - 1
.github/workflows/ci.yml

@@ -3,7 +3,7 @@ on:
   push:
   push:
 
 
 env:
 env:
-  ROOT_FOLDER: /test
+  ROOT_FOLDER: /runtipi
   JWT_SECRET: "secret"
   JWT_SECRET: "secret"
   ROOT_FOLDER_HOST: /tipi
   ROOT_FOLDER_HOST: /tipi
   APPS_REPO_ID: repo-id
   APPS_REPO_ID: repo-id

+ 1 - 1
docker-compose.dev.yml

@@ -53,7 +53,7 @@ services:
     volumes:
     volumes:
       ## Docker sock
       ## Docker sock
       - /var/run/docker.sock:/var/run/docker.sock:ro
       - /var/run/docker.sock:/var/run/docker.sock:ro
-      - ${PWD}:/tipi
+      - ${PWD}:/runtipi
       - ${PWD}/packages/system-api/src:/api/src
       - ${PWD}/packages/system-api/src:/api/src
       # - /api/node_modules
       # - /api/node_modules
     environment:
     environment:

+ 1 - 1
docker-compose.rc.yml

@@ -46,7 +46,7 @@ services:
     volumes:
     volumes:
       ## Docker sock
       ## Docker sock
       - /var/run/docker.sock:/var/run/docker.sock:ro
       - /var/run/docker.sock:/var/run/docker.sock:ro
-      - ${PWD}:/tipi
+      - ${PWD}:/runtipi
     environment:
     environment:
       INTERNAL_IP: ${INTERNAL_IP}
       INTERNAL_IP: ${INTERNAL_IP}
       TIPI_VERSION: ${TIPI_VERSION}
       TIPI_VERSION: ${TIPI_VERSION}

+ 1 - 1
docker-compose.yml

@@ -46,7 +46,7 @@ services:
     volumes:
     volumes:
       ## Docker sock
       ## Docker sock
       - /var/run/docker.sock:/var/run/docker.sock:ro
       - /var/run/docker.sock:/var/run/docker.sock:ro
-      - ${PWD}:/tipi
+      - ${PWD}:/runtipi
     environment:
     environment:
       INTERNAL_IP: ${INTERNAL_IP}
       INTERNAL_IP: ${INTERNAL_IP}
       TIPI_VERSION: ${TIPI_VERSION}
       TIPI_VERSION: ${TIPI_VERSION}

+ 3 - 2
packages/dashboard/next.config.js

@@ -1,5 +1,5 @@
 /** @type {import('next').NextConfig} */
 /** @type {import('next').NextConfig} */
-const { INTERNAL_IP, DOMAIN } = process.env;
+const { INTERNAL_IP, DOMAIN, NGINX_PORT } = process.env;
 
 
 const nextConfig = {
 const nextConfig = {
   webpackDevMiddleware: (config) => {
   webpackDevMiddleware: (config) => {
@@ -11,8 +11,9 @@ const nextConfig = {
   },
   },
   reactStrictMode: true,
   reactStrictMode: true,
   env: {
   env: {
-    INTERNAL_IP: INTERNAL_IP,
+    NEXT_PUBLIC_INTERNAL_IP: INTERNAL_IP,
     NEXT_PUBLIC_DOMAIN: DOMAIN,
     NEXT_PUBLIC_DOMAIN: DOMAIN,
+    NEXT_PUBLIC_PORT: NGINX_PORT,
   },
   },
   basePath: '/dashboard',
   basePath: '/dashboard',
 };
 };

+ 1 - 1
packages/dashboard/src/core/helpers/url-helpers.ts

@@ -2,8 +2,8 @@ export const getUrl = (url: string) => {
   const domain = process.env.NEXT_PUBLIC_DOMAIN;
   const domain = process.env.NEXT_PUBLIC_DOMAIN;
   let prefix = '';
   let prefix = '';
 
 
+  prefix = 'dashboard';
   if (domain !== 'tipi.localhost') {
   if (domain !== 'tipi.localhost') {
-    prefix = 'dashboard';
   }
   }
 
 
   return `/${prefix}/${url}`;
   return `/${prefix}/${url}`;

+ 9 - 9
packages/dashboard/src/hooks/useCachedRessources.ts

@@ -1,22 +1,23 @@
 import { useEffect, useState } from 'react';
 import { useEffect, useState } from 'react';
 import { ApolloClient } from '@apollo/client';
 import { ApolloClient } from '@apollo/client';
-import axios from 'axios';
-import useSWR, { BareFetcher } from 'swr';
 import { createApolloClient } from '../core/apollo/client';
 import { createApolloClient } from '../core/apollo/client';
 import { useSytemStore } from '../state/systemStore';
 import { useSytemStore } from '../state/systemStore';
-import { getUrl } from '../core/helpers/url-helpers';
 
 
 interface IReturnProps {
 interface IReturnProps {
   client?: ApolloClient<unknown>;
   client?: ApolloClient<unknown>;
   isLoadingComplete?: boolean;
   isLoadingComplete?: boolean;
 }
 }
 
 
-const fetcher: BareFetcher<any> = (url: string) => {
-  return axios.get(getUrl(url)).then((res) => res.data);
-};
+// const fetcher: BareFetcher<any> = (url: string) => {
+//   return axios.get(getUrl(url)).then((res) => res.data);
+// };
 
 
 export default function useCachedResources(): IReturnProps {
 export default function useCachedResources(): IReturnProps {
-  const { data } = useSWR<{ ip: string; domain: string; port: string }>('api/ip', fetcher);
+  const ip = process.env.NEXT_PUBLIC_INTERNAL_IP;
+  const domain = process.env.NEXT_PUBLIC_DOMAIN;
+  const port = process.env.NEXT_PUBLIC_PORT;
+
+  // const { data } = useSWR<{ ip: string; domain: string; port: string }>('api/ip', fetcher);
   const { baseUrl, setBaseUrl, setInternalIp, setDomain } = useSytemStore();
   const { baseUrl, setBaseUrl, setInternalIp, setDomain } = useSytemStore();
   const [isLoadingComplete, setLoadingComplete] = useState(false);
   const [isLoadingComplete, setLoadingComplete] = useState(false);
   const [client, setClient] = useState<ApolloClient<unknown>>();
   const [client, setClient] = useState<ApolloClient<unknown>>();
@@ -35,7 +36,6 @@ export default function useCachedResources(): IReturnProps {
   }
   }
 
 
   useEffect(() => {
   useEffect(() => {
-    const { ip, domain, port } = data || {};
     if (ip && !baseUrl) {
     if (ip && !baseUrl) {
       setInternalIp(ip);
       setInternalIp(ip);
       setDomain(domain);
       setDomain(domain);
@@ -50,7 +50,7 @@ export default function useCachedResources(): IReturnProps {
         setBaseUrl(`https://${domain}/api`);
         setBaseUrl(`https://${domain}/api`);
       }
       }
     }
     }
-  }, [baseUrl, setBaseUrl, data, setInternalIp, setDomain]);
+  }, [baseUrl, setBaseUrl, setInternalIp, setDomain]);
 
 
   useEffect(() => {
   useEffect(() => {
     if (baseUrl) {
     if (baseUrl) {

+ 0 - 58
packages/system-api/src/config/config.ts

@@ -1,58 +0,0 @@
-import * as dotenv from 'dotenv';
-
-interface IConfig {
-  logs: {
-    LOGS_FOLDER: string;
-    LOGS_APP: string;
-    LOGS_ERROR: string;
-  };
-  NODE_ENV: string;
-  ROOT_FOLDER: string;
-  JWT_SECRET: string;
-  CLIENT_URLS: string[];
-  VERSION: string;
-  ROOT_FOLDER_HOST: string;
-  APPS_REPO_ID: string;
-  APPS_REPO_URL: string;
-  INTERNAL_IP: string;
-}
-
-if (process.env.NODE_ENV !== 'production') {
-  dotenv.config({ path: '.env.dev' });
-} else {
-  dotenv.config({ path: '.env' });
-}
-
-const {
-  LOGS_FOLDER = 'logs',
-  LOGS_APP = 'app.log',
-  LOGS_ERROR = 'error.log',
-  NODE_ENV = 'development',
-  JWT_SECRET = '',
-  INTERNAL_IP = '',
-  TIPI_VERSION = '',
-  ROOT_FOLDER_HOST = '',
-  NGINX_PORT = '80',
-  APPS_REPO_ID = '',
-  APPS_REPO_URL = '',
-  DOMAIN = '',
-} = process.env;
-
-const config: IConfig = {
-  logs: {
-    LOGS_FOLDER,
-    LOGS_APP,
-    LOGS_ERROR,
-  },
-  NODE_ENV,
-  ROOT_FOLDER: '/tipi',
-  JWT_SECRET,
-  CLIENT_URLS: ['http://localhost:3000', `http://${INTERNAL_IP}`, `http://${INTERNAL_IP}:${NGINX_PORT}`, `http://${INTERNAL_IP}:3000`, `https://${DOMAIN}`],
-  VERSION: TIPI_VERSION,
-  ROOT_FOLDER_HOST,
-  APPS_REPO_ID,
-  APPS_REPO_URL,
-  INTERNAL_IP,
-};
-
-export default config;

+ 0 - 1
packages/system-api/src/config/index.ts

@@ -1 +0,0 @@
-export { default } from './config';

+ 9 - 7
packages/system-api/src/config/logger/logger.ts

@@ -1,13 +1,15 @@
 import fs from 'fs-extra';
 import fs from 'fs-extra';
 import path from 'path';
 import path from 'path';
 import { createLogger, format, transports } from 'winston';
 import { createLogger, format, transports } from 'winston';
-import config from '..';
+import { getConfig } from '../../core/config/TipiConfig';
+
+const { logs, NODE_ENV } = getConfig();
 
 
 const { align, printf, timestamp, combine, colorize } = format;
 const { align, printf, timestamp, combine, colorize } = format;
 
 
 // Create the logs directory if it does not exist
 // Create the logs directory if it does not exist
-if (!fs.existsSync(config.logs.LOGS_FOLDER)) {
-  fs.mkdirSync(config.logs.LOGS_FOLDER);
+if (!fs.existsSync(logs.LOGS_FOLDER)) {
+  fs.mkdirSync(logs.LOGS_FOLDER);
 }
 }
 
 
 /**
 /**
@@ -36,14 +38,14 @@ const Logger = createLogger({
     // - Write all logs error (and below) to `error.log`.
     // - Write all logs error (and below) to `error.log`.
     //
     //
     new transports.File({
     new transports.File({
-      filename: path.join(config.logs.LOGS_FOLDER, config.logs.LOGS_ERROR),
+      filename: path.join(logs.LOGS_FOLDER, logs.LOGS_ERROR),
       level: 'error',
       level: 'error',
     }),
     }),
     new transports.File({
     new transports.File({
-      filename: path.join(config.logs.LOGS_FOLDER, config.logs.LOGS_APP),
+      filename: path.join(logs.LOGS_FOLDER, logs.LOGS_APP),
     }),
     }),
   ],
   ],
-  exceptionHandlers: [new transports.File({ filename: path.join(config.logs.LOGS_FOLDER, config.logs.LOGS_ERROR) })],
+  exceptionHandlers: [new transports.File({ filename: path.join(logs.LOGS_FOLDER, logs.LOGS_ERROR) })],
 });
 });
 
 
 //
 //
@@ -59,4 +61,4 @@ const LoggerDev = createLogger({
   ],
   ],
 });
 });
 
 
-export default config.NODE_ENV === 'production' ? Logger : LoggerDev;
+export default NODE_ENV === 'production' ? Logger : LoggerDev;

+ 2 - 2
packages/system-api/src/constants/constants.ts

@@ -1,5 +1,5 @@
-import config from '../config';
+import { getConfig } from '../core/config/TipiConfig';
 
 
 export const APP_DATA_FOLDER = 'app-data';
 export const APP_DATA_FOLDER = 'app-data';
 export const APPS_FOLDER = 'apps';
 export const APPS_FOLDER = 'apps';
-export const isProd = config.NODE_ENV === 'production';
+export const isProd = getConfig().NODE_ENV === 'production';

+ 20 - 10
packages/system-api/src/core/config/TipiConfig.ts

@@ -1,7 +1,6 @@
 import { z } from 'zod';
 import { z } from 'zod';
 import * as dotenv from 'dotenv';
 import * as dotenv from 'dotenv';
 import fs from 'fs-extra';
 import fs from 'fs-extra';
-import config from '../../config';
 import { readJsonFile } from '../../modules/fs/fs.helpers';
 import { readJsonFile } from '../../modules/fs/fs.helpers';
 
 
 if (process.env.NODE_ENV !== 'production') {
 if (process.env.NODE_ENV !== 'production') {
@@ -24,13 +23,13 @@ const {
 } = process.env;
 } = process.env;
 
 
 const configSchema = z.object({
 const configSchema = z.object({
-  NODE_ENV: z.string(),
-  repo: z.string(),
+  NODE_ENV: z.union([z.literal('development'), z.literal('production'), z.literal('test')]),
   logs: z.object({
   logs: z.object({
     LOGS_FOLDER: z.string(),
     LOGS_FOLDER: z.string(),
     LOGS_APP: z.string(),
     LOGS_APP: z.string(),
     LOGS_ERROR: z.string(),
     LOGS_ERROR: z.string(),
   }),
   }),
+  dnsIp: z.string(),
   rootFolder: z.string(),
   rootFolder: z.string(),
   internalIp: z.string(),
   internalIp: z.string(),
   version: z.string(),
   version: z.string(),
@@ -47,28 +46,26 @@ class Config {
   private config: z.infer<typeof configSchema>;
   private config: z.infer<typeof configSchema>;
 
 
   constructor() {
   constructor() {
-    const fileConfig = readJsonFile('/tipi/state/settings.json');
     const envConfig: z.infer<typeof configSchema> = {
     const envConfig: z.infer<typeof configSchema> = {
       logs: {
       logs: {
         LOGS_FOLDER,
         LOGS_FOLDER,
         LOGS_APP,
         LOGS_APP,
         LOGS_ERROR,
         LOGS_ERROR,
       },
       },
-      NODE_ENV,
-      repo: APPS_REPO_URL,
-      rootFolder: '/tipi',
+      NODE_ENV: NODE_ENV as z.infer<typeof configSchema>['NODE_ENV'],
+      rootFolder: '/runtipi',
       internalIp: INTERNAL_IP,
       internalIp: INTERNAL_IP,
       version: TIPI_VERSION,
       version: TIPI_VERSION,
       jwtSecret: JWT_SECRET,
       jwtSecret: JWT_SECRET,
-      clientUrls: ['http://localhost:3000', `http://${INTERNAL_IP}`, `http://${INTERNAL_IP}:${NGINX_PORT}`, `http://${INTERNAL_IP}:3000`, `https://${DOMAIN}`],
+      clientUrls: ['http://localhost:3000', `http://${INTERNAL_IP}`, `http://${INTERNAL_IP}:${NGINX_PORT}`, `http://${INTERNAL_IP}:3000`, DOMAIN && `https://${DOMAIN}`].filter(Boolean),
       appsRepoId: APPS_REPO_ID,
       appsRepoId: APPS_REPO_ID,
       appsRepoUrl: APPS_REPO_URL,
       appsRepoUrl: APPS_REPO_URL,
       domain: DOMAIN,
       domain: DOMAIN,
+      dnsIp: '9.9.9.9',
     };
     };
 
 
     const parsed = configSchema.parse({
     const parsed = configSchema.parse({
       ...envConfig,
       ...envConfig,
-      ...fileConfig,
     });
     });
 
 
     this.config = parsed;
     this.config = parsed;
@@ -85,13 +82,24 @@ class Config {
     return this.config;
     return this.config;
   }
   }
 
 
+  public applyJsonConfig() {
+    const fileConfig = readJsonFile('/state/settings.json');
+
+    const parsed = configSchema.parse({
+      ...this.config,
+      ...fileConfig,
+    });
+
+    this.config = parsed;
+  }
+
   public setConfig(key: keyof typeof configSchema.shape, value: any) {
   public setConfig(key: keyof typeof configSchema.shape, value: any) {
     const newConf = { ...this.getConfig() };
     const newConf = { ...this.getConfig() };
     newConf[key] = value;
     newConf[key] = value;
 
 
     this.config = configSchema.parse(newConf);
     this.config = configSchema.parse(newConf);
 
 
-    fs.writeFileSync(`${config.ROOT_FOLDER}/state/settings.json`, JSON.stringify(newConf));
+    fs.writeFileSync(`${this.config.rootFolder}/state/settings.json`, JSON.stringify(newConf));
   }
   }
 }
 }
 
 
@@ -100,3 +108,5 @@ export const setConfig = (key: keyof typeof configSchema.shape, value: any) => {
 };
 };
 
 
 export const getConfig = () => Config.getInstance().getConfig();
 export const getConfig = () => Config.getInstance().getConfig();
+
+export const applyJsonConfig = () => Config.getInstance().applyJsonConfig();

+ 2 - 2
packages/system-api/src/core/jobs/jobs.ts

@@ -1,14 +1,14 @@
 import cron from 'node-cron';
 import cron from 'node-cron';
-import config from '../../config';
 import logger from '../../config/logger/logger';
 import logger from '../../config/logger/logger';
 import { updateRepo } from '../../helpers/repo-helpers';
 import { updateRepo } from '../../helpers/repo-helpers';
+import { getConfig } from '../../core/config/TipiConfig';
 
 
 const startJobs = () => {
 const startJobs = () => {
   logger.info('Starting cron jobs...');
   logger.info('Starting cron jobs...');
 
 
   cron.schedule('0 * * * *', () => {
   cron.schedule('0 * * * *', () => {
     logger.info('Cloning apps repo...');
     logger.info('Cloning apps repo...');
-    updateRepo(config.APPS_REPO_URL);
+    updateRepo(getConfig().appsRepoUrl);
   });
   });
 };
 };
 
 

+ 2 - 2
packages/system-api/src/core/middlewares/sessionMiddleware.ts

@@ -1,7 +1,7 @@
 import session from 'express-session';
 import session from 'express-session';
-import config from '../../config';
 import SessionFileStore from 'session-file-store';
 import SessionFileStore from 'session-file-store';
 import { COOKIE_MAX_AGE, __prod__ } from '../../config/constants/constants';
 import { COOKIE_MAX_AGE, __prod__ } from '../../config/constants/constants';
+import { getConfig } from '../config/TipiConfig';
 
 
 const getSessionMiddleware = () => {
 const getSessionMiddleware = () => {
   const FileStore = SessionFileStore(session);
   const FileStore = SessionFileStore(session);
@@ -12,7 +12,7 @@ const getSessionMiddleware = () => {
     name: 'qid',
     name: 'qid',
     store: new FileStore(),
     store: new FileStore(),
     cookie: { maxAge: COOKIE_MAX_AGE, secure: false, sameSite, httpOnly: true },
     cookie: { maxAge: COOKIE_MAX_AGE, secure: false, sameSite, httpOnly: true },
-    secret: config.JWT_SECRET,
+    secret: getConfig().jwtSecret,
     resave: false,
     resave: false,
     saveUninitialized: false,
     saveUninitialized: false,
   });
   });

+ 9 - 8
packages/system-api/src/core/updates/__tests__/v040.test.ts

@@ -6,6 +6,7 @@ import { AppInfo, AppStatusEnum } from '../../../modules/apps/apps.types';
 import { createApp } from '../../../modules/apps/__tests__/apps.factory';
 import { createApp } from '../../../modules/apps/__tests__/apps.factory';
 import Update, { UpdateStatusEnum } from '../../../modules/system/update.entity';
 import Update, { UpdateStatusEnum } from '../../../modules/system/update.entity';
 import { setupConnection, teardownConnection } from '../../../test/connection';
 import { setupConnection, teardownConnection } from '../../../test/connection';
+import { getConfig } from '../../config/TipiConfig';
 import { updateV040 } from '../v040';
 import { updateV040 } from '../v040';
 
 
 jest.mock('fs');
 jest.mock('fs');
@@ -61,7 +62,7 @@ describe('No state/apps.json', () => {
 describe('State/apps.json exists with no installed app', () => {
 describe('State/apps.json exists with no installed app', () => {
   beforeEach(async () => {
   beforeEach(async () => {
     const { MockFiles } = await createApp({});
     const { MockFiles } = await createApp({});
-    MockFiles['/tipi/state/apps.json'] = createState([]);
+    MockFiles[`${getConfig().rootFolder}/state/apps.json`] = createState([]);
     // @ts-ignore
     // @ts-ignore
     fs.__createMockFiles(MockFiles);
     fs.__createMockFiles(MockFiles);
   });
   });
@@ -79,7 +80,7 @@ describe('State/apps.json exists with no installed app', () => {
 
 
   it('Should delete state file after update', async () => {
   it('Should delete state file after update', async () => {
     await updateV040();
     await updateV040();
-    expect(fs.existsSync('/tipi/state/apps.json')).toBe(false);
+    expect(fs.existsSync(`${getConfig().rootFolder}/state/apps.json`)).toBe(false);
   });
   });
 });
 });
 
 
@@ -88,9 +89,9 @@ describe('State/apps.json exists with one installed app', () => {
   beforeEach(async () => {
   beforeEach(async () => {
     const { MockFiles, appInfo } = await createApp({});
     const { MockFiles, appInfo } = await createApp({});
     app1 = appInfo;
     app1 = appInfo;
-    MockFiles['/tipi/state/apps.json'] = createState([appInfo.id]);
-    MockFiles[`/tipi/app-data/${appInfo.id}`] = '';
-    MockFiles[`/tipi/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
+    MockFiles[`${getConfig().rootFolder}/state/apps.json`] = createState([appInfo.id]);
+    MockFiles[`${getConfig().rootFolder}/app-data/${appInfo.id}`] = '';
+    MockFiles[`${getConfig().rootFolder}/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
     // @ts-ignore
     // @ts-ignore
     fs.__createMockFiles(MockFiles);
     fs.__createMockFiles(MockFiles);
   });
   });
@@ -117,9 +118,9 @@ describe('State/apps.json exists with one installed app', () => {
   it('Should not try to migrate app if it already exists', async () => {
   it('Should not try to migrate app if it already exists', async () => {
     const { MockFiles, appInfo } = await createApp({ installed: true });
     const { MockFiles, appInfo } = await createApp({ installed: true });
     app1 = appInfo;
     app1 = appInfo;
-    MockFiles['/tipi/state/apps.json'] = createState([appInfo.id]);
-    MockFiles[`/tipi/app-data/${appInfo.id}`] = '';
-    MockFiles[`/tipi/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
+    MockFiles[`${getConfig().rootFolder}/state/apps.json`] = createState([appInfo.id]);
+    MockFiles[`${getConfig().rootFolder}/app-data/${appInfo.id}`] = '';
+    MockFiles[`${getConfig().rootFolder}/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
     // @ts-ignore
     // @ts-ignore
     fs.__createMockFiles(MockFiles);
     fs.__createMockFiles(MockFiles);
 
 

+ 2 - 2
packages/system-api/src/core/updates/v040.ts

@@ -1,10 +1,10 @@
-import config from '../../config';
 import logger from '../../config/logger/logger';
 import logger from '../../config/logger/logger';
 import App from '../../modules/apps/app.entity';
 import App from '../../modules/apps/app.entity';
 import { AppInfo, AppStatusEnum } from '../../modules/apps/apps.types';
 import { AppInfo, AppStatusEnum } from '../../modules/apps/apps.types';
 import User from '../../modules/auth/user.entity';
 import User from '../../modules/auth/user.entity';
 import { deleteFolder, fileExists, readFile, readJsonFile } from '../../modules/fs/fs.helpers';
 import { deleteFolder, fileExists, readFile, readJsonFile } from '../../modules/fs/fs.helpers';
 import Update, { UpdateStatusEnum } from '../../modules/system/update.entity';
 import Update, { UpdateStatusEnum } from '../../modules/system/update.entity';
+import { getConfig } from '../config/TipiConfig';
 
 
 type AppsState = { installed: string };
 type AppsState = { installed: string };
 
 
@@ -39,7 +39,7 @@ export const updateV040 = async (): Promise<void> => {
 
 
           const form: Record<string, string> = {};
           const form: Record<string, string> = {};
 
 
-          const configFile: AppInfo | null = readJsonFile(`/repos/${config.APPS_REPO_ID}/apps/${appId}/config.json`);
+          const configFile: AppInfo | null = readJsonFile(`/repos/${getConfig().appsRepoId}/apps/${appId}/config.json`);
           configFile?.form_fields?.forEach((field) => {
           configFile?.form_fields?.forEach((field) => {
             const envVar = field.env_variable;
             const envVar = field.env_variable;
             const envVarValue = envVarsMap.get(envVar);
             const envVarValue = envVarsMap.get(envVar);

+ 10 - 10
packages/system-api/src/modules/apps/__tests__/apps.factory.ts

@@ -1,7 +1,7 @@
 import { faker } from '@faker-js/faker';
 import { faker } from '@faker-js/faker';
 import { AppCategoriesEnum, AppInfo, AppStatusEnum, FieldTypes } from '../apps.types';
 import { AppCategoriesEnum, AppInfo, AppStatusEnum, FieldTypes } from '../apps.types';
-import config from '../../../config';
 import App from '../app.entity';
 import App from '../app.entity';
+import { getConfig } from '../../../core/config/TipiConfig';
 
 
 interface IProps {
 interface IProps {
   installed?: boolean;
   installed?: boolean;
@@ -55,11 +55,11 @@ const createApp = async (props: IProps) => {
   }
   }
 
 
   let MockFiles: any = {};
   let MockFiles: any = {};
-  MockFiles[`${config.ROOT_FOLDER}/.env`] = 'TEST=test';
-  MockFiles[`${config.ROOT_FOLDER}/repos/repo-id`] = '';
-  MockFiles[`${config.ROOT_FOLDER}/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
-  MockFiles[`${config.ROOT_FOLDER}/repos/repo-id/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
-  MockFiles[`${config.ROOT_FOLDER}/repos/repo-id/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
+  MockFiles[`${getConfig().rootFolder}/.env`] = 'TEST=test';
+  MockFiles[`${getConfig().rootFolder}/repos/repo-id`] = '';
+  MockFiles[`${getConfig().rootFolder}/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
+  MockFiles[`${getConfig().rootFolder}/repos/repo-id/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
+  MockFiles[`${getConfig().rootFolder}/repos/repo-id/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
 
 
   let appEntity = new App();
   let appEntity = new App();
   if (installed) {
   if (installed) {
@@ -71,10 +71,10 @@ const createApp = async (props: IProps) => {
       domain,
       domain,
     }).save();
     }).save();
 
 
-    MockFiles[`${config.ROOT_FOLDER}/app-data/${appInfo.id}`] = '';
-    MockFiles[`${config.ROOT_FOLDER}/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
-    MockFiles[`${config.ROOT_FOLDER}/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
-    MockFiles[`${config.ROOT_FOLDER}/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
+    MockFiles[`${getConfig().rootFolder}/app-data/${appInfo.id}`] = '';
+    MockFiles[`${getConfig().rootFolder}/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
+    MockFiles[`${getConfig().rootFolder}/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
+    MockFiles[`${getConfig().rootFolder}/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
   }
   }
 
 
   return { appInfo, MockFiles, appEntity };
   return { appInfo, MockFiles, appEntity };

+ 4 - 4
packages/system-api/src/modules/apps/__tests__/apps.helpers.test.ts

@@ -1,7 +1,7 @@
 import { faker } from '@faker-js/faker';
 import { faker } from '@faker-js/faker';
 import fs from 'fs-extra';
 import fs from 'fs-extra';
 import { DataSource } from 'typeorm';
 import { DataSource } from 'typeorm';
-import config from '../../../config';
+import { getConfig } from '../../../core/config/TipiConfig';
 import { setupConnection, teardownConnection } from '../../../test/connection';
 import { setupConnection, teardownConnection } from '../../../test/connection';
 import App from '../app.entity';
 import App from '../app.entity';
 import { checkAppRequirements, checkEnvFile, generateEnvFile, getAppInfo, getAvailableApps, getEnvMap, getUpdateInfo, runAppScript } from '../apps.helpers';
 import { checkAppRequirements, checkEnvFile, generateEnvFile, getAppInfo, getAvailableApps, getEnvMap, getUpdateInfo, runAppScript } from '../apps.helpers';
@@ -95,7 +95,7 @@ describe('checkEnvFile', () => {
 
 
   it('Should throw if a required field is missing', () => {
   it('Should throw if a required field is missing', () => {
     const newAppEnv = 'APP_PORT=test\n';
     const newAppEnv = 'APP_PORT=test\n';
-    fs.writeFileSync(`${config.ROOT_FOLDER}/app-data/${app1.id}/app.env`, newAppEnv);
+    fs.writeFileSync(`${getConfig().rootFolder}/app-data/${app1.id}/app.env`, newAppEnv);
 
 
     try {
     try {
       checkEnvFile(app1.id);
       checkEnvFile(app1.id);
@@ -167,7 +167,7 @@ describe('generateEnvFile', () => {
 
 
     const randomField = faker.random.alphaNumeric(32);
     const randomField = faker.random.alphaNumeric(32);
 
 
-    fs.writeFileSync(`${config.ROOT_FOLDER}/app-data/${appInfo.id}/app.env`, `RANDOM_FIELD=${randomField}`);
+    fs.writeFileSync(`${getConfig().rootFolder}/app-data/${appInfo.id}/app.env`, `RANDOM_FIELD=${randomField}`);
 
 
     generateEnvFile(appEntity);
     generateEnvFile(appEntity);
 
 
@@ -271,7 +271,7 @@ describe('getAppInfo', () => {
     // @ts-ignore
     // @ts-ignore
     fs.__createMockFiles(MockFiles);
     fs.__createMockFiles(MockFiles);
 
 
-    fs.writeFileSync(`${config.ROOT_FOLDER}/repos/repo-id/apps/${app1.id}/config.json`, '{}');
+    fs.writeFileSync(`${getConfig().rootFolder}/repos/repo-id/apps/${app1.id}/config.json`, '{}');
 
 
     const app = await getAppInfo(appInfo.id);
     const app = await getAppInfo(appInfo.id);
 
 

+ 24 - 24
packages/system-api/src/modules/apps/__tests__/apps.service.test.ts

@@ -1,6 +1,5 @@
 import AppsService from '../apps.service';
 import AppsService from '../apps.service';
 import fs from 'fs-extra';
 import fs from 'fs-extra';
-import config from '../../../config';
 import childProcess from 'child_process';
 import childProcess from 'child_process';
 import { AppInfo, AppStatusEnum } from '../apps.types';
 import { AppInfo, AppStatusEnum } from '../apps.types';
 import App from '../app.entity';
 import App from '../app.entity';
@@ -8,6 +7,7 @@ import { createApp } from './apps.factory';
 import { setupConnection, teardownConnection } from '../../../test/connection';
 import { setupConnection, teardownConnection } from '../../../test/connection';
 import { DataSource } from 'typeorm';
 import { DataSource } from 'typeorm';
 import { getEnvMap } from '../apps.helpers';
 import { getEnvMap } from '../apps.helpers';
+import { getConfig } from '../../../core/config/TipiConfig';
 
 
 jest.mock('fs-extra');
 jest.mock('fs-extra');
 jest.mock('child_process');
 jest.mock('child_process');
@@ -43,7 +43,7 @@ describe('Install app', () => {
 
 
   it('Should correctly generate env file for app', async () => {
   it('Should correctly generate env file for app', async () => {
     await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
     await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
-    const envFile = fs.readFileSync(`${config.ROOT_FOLDER}/app-data/${app1.id}/app.env`).toString();
+    const envFile = fs.readFileSync(`${getConfig().rootFolder}/app-data/${app1.id}/app.env`).toString();
 
 
     expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${app1.port}\nTEST_FIELD=test\nAPP_DOMAIN=192.168.1.10:${app1.port}`);
     expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${app1.port}\nTEST_FIELD=test\nAPP_DOMAIN=192.168.1.10:${app1.port}`);
   });
   });
@@ -63,7 +63,7 @@ describe('Install app', () => {
     const spy = jest.spyOn(childProcess, 'execFile');
     const spy = jest.spyOn(childProcess, 'execFile');
     await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
     await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
 
 
-    expect(spy.mock.lastCall).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['install', app1.id], {}, expect.any(Function)]);
+    expect(spy.mock.lastCall).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['install', app1.id], {}, expect.any(Function)]);
     spy.mockRestore();
     spy.mockRestore();
   });
   });
 
 
@@ -74,8 +74,8 @@ describe('Install app', () => {
     await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
     await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
 
 
     expect(spy.mock.calls.length).toBe(2);
     expect(spy.mock.calls.length).toBe(2);
-    expect(spy.mock.calls[0]).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['install', app1.id], {}, expect.any(Function)]);
-    expect(spy.mock.calls[1]).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['start', app1.id], {}, expect.any(Function)]);
+    expect(spy.mock.calls[0]).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['install', app1.id], {}, expect.any(Function)]);
+    expect(spy.mock.calls[1]).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['start', app1.id], {}, expect.any(Function)]);
 
 
     spy.mockRestore();
     spy.mockRestore();
   });
   });
@@ -112,7 +112,7 @@ describe('Install app', () => {
 
 
   it('Should correctly copy app from repos to apps folder', async () => {
   it('Should correctly copy app from repos to apps folder', async () => {
     await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
     await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
-    const appFolder = fs.readdirSync(`${config.ROOT_FOLDER}/apps/${app1.id}`);
+    const appFolder = fs.readdirSync(`${getConfig().rootFolder}/apps/${app1.id}`);
 
 
     expect(appFolder).toBeDefined();
     expect(appFolder).toBeDefined();
     expect(appFolder.indexOf('docker-compose.yml')).toBeGreaterThanOrEqual(0);
     expect(appFolder.indexOf('docker-compose.yml')).toBeGreaterThanOrEqual(0);
@@ -121,19 +121,19 @@ describe('Install app', () => {
   it('Should cleanup any app folder existing before install', async () => {
   it('Should cleanup any app folder existing before install', async () => {
     const { MockFiles, appInfo } = await createApp({});
     const { MockFiles, appInfo } = await createApp({});
     app1 = appInfo;
     app1 = appInfo;
-    MockFiles[`/tipi/apps/${appInfo.id}/docker-compose.yml`] = 'test';
-    MockFiles[`/tipi/apps/${appInfo.id}/test.yml`] = 'test';
-    MockFiles[`/tipi/apps/${appInfo.id}`] = ['test.yml', 'docker-compose.yml'];
+    MockFiles[`${getConfig().rootFolder}/apps/${appInfo.id}/docker-compose.yml`] = 'test';
+    MockFiles[`${getConfig().rootFolder}/apps/${appInfo.id}/test.yml`] = 'test';
+    MockFiles[`${getConfig().rootFolder}/apps/${appInfo.id}`] = ['test.yml', 'docker-compose.yml'];
 
 
     // @ts-ignore
     // @ts-ignore
     fs.__createMockFiles(MockFiles);
     fs.__createMockFiles(MockFiles);
 
 
-    expect(fs.existsSync(`${config.ROOT_FOLDER}/apps/${app1.id}/test.yml`)).toBe(true);
+    expect(fs.existsSync(`${getConfig().rootFolder}/apps/${app1.id}/test.yml`)).toBe(true);
 
 
     await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
     await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
 
 
-    expect(fs.existsSync(`${config.ROOT_FOLDER}/apps/${app1.id}/test.yml`)).toBe(false);
-    expect(fs.existsSync(`${config.ROOT_FOLDER}/apps/${app1.id}/docker-compose.yml`)).toBe(true);
+    expect(fs.existsSync(`${getConfig().rootFolder}/apps/${app1.id}/test.yml`)).toBe(false);
+    expect(fs.existsSync(`${getConfig().rootFolder}/apps/${app1.id}/docker-compose.yml`)).toBe(true);
   });
   });
 
 
   it('Should throw if app is exposed and domain is not provided', async () => {
   it('Should throw if app is exposed and domain is not provided', async () => {
@@ -194,7 +194,7 @@ describe('Uninstall app', () => {
 
 
     await AppsService.uninstallApp(app1.id);
     await AppsService.uninstallApp(app1.id);
 
 
-    expect(spy.mock.lastCall).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['uninstall', app1.id], {}, expect.any(Function)]);
+    expect(spy.mock.lastCall).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['uninstall', app1.id], {}, expect.any(Function)]);
 
 
     spy.mockRestore();
     spy.mockRestore();
   });
   });
@@ -205,8 +205,8 @@ describe('Uninstall app', () => {
     await AppsService.uninstallApp(app1.id);
     await AppsService.uninstallApp(app1.id);
 
 
     expect(spy.mock.calls.length).toBe(2);
     expect(spy.mock.calls.length).toBe(2);
-    expect(spy.mock.calls[0]).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['stop', app1.id], {}, expect.any(Function)]);
-    expect(spy.mock.calls[1]).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['uninstall', app1.id], {}, expect.any(Function)]);
+    expect(spy.mock.calls[0]).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['stop', app1.id], {}, expect.any(Function)]);
+    expect(spy.mock.calls[1]).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['uninstall', app1.id], {}, expect.any(Function)]);
 
 
     spy.mockRestore();
     spy.mockRestore();
   });
   });
@@ -245,7 +245,7 @@ describe('Start app', () => {
 
 
     await AppsService.startApp(app1.id);
     await AppsService.startApp(app1.id);
 
 
-    expect(spy.mock.lastCall).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['start', app1.id], {}, expect.any(Function)]);
+    expect(spy.mock.lastCall).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['start', app1.id], {}, expect.any(Function)]);
 
 
     spy.mockRestore();
     spy.mockRestore();
   });
   });
@@ -266,11 +266,11 @@ describe('Start app', () => {
   });
   });
 
 
   it('Regenerate env file', async () => {
   it('Regenerate env file', async () => {
-    fs.writeFile(`${config.ROOT_FOLDER}/app-data/${app1.id}/app.env`, 'TEST=test\nAPP_PORT=3000', () => {});
+    fs.writeFile(`${getConfig().rootFolder}/app-data/${app1.id}/app.env`, 'TEST=test\nAPP_PORT=3000', () => {});
 
 
     await AppsService.startApp(app1.id);
     await AppsService.startApp(app1.id);
 
 
-    const envFile = fs.readFileSync(`${config.ROOT_FOLDER}/app-data/${app1.id}/app.env`).toString();
+    const envFile = fs.readFileSync(`${getConfig().rootFolder}/app-data/${app1.id}/app.env`).toString();
 
 
     expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${app1.port}\nTEST_FIELD=test\nAPP_DOMAIN=192.168.1.10:${app1.port}`);
     expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${app1.port}\nTEST_FIELD=test\nAPP_DOMAIN=192.168.1.10:${app1.port}`);
   });
   });
@@ -302,7 +302,7 @@ describe('Stop app', () => {
 
 
     await AppsService.stopApp(app1.id);
     await AppsService.stopApp(app1.id);
 
 
-    expect(spy.mock.lastCall).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['stop', app1.id], {}, expect.any(Function)]);
+    expect(spy.mock.lastCall).toEqual([`${getConfig().rootFolder}/scripts/app.sh`, ['stop', app1.id], {}, expect.any(Function)]);
   });
   });
 
 
   it('Should throw if app is not installed', async () => {
   it('Should throw if app is not installed', async () => {
@@ -334,7 +334,7 @@ describe('Update app config', () => {
   it('Should correctly update app config', async () => {
   it('Should correctly update app config', async () => {
     await AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' });
     await AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' });
 
 
-    const envFile = fs.readFileSync(`${config.ROOT_FOLDER}/app-data/${app1.id}/app.env`).toString();
+    const envFile = fs.readFileSync(`${getConfig().rootFolder}/app-data/${app1.id}/app.env`).toString();
 
 
     expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${app1.port}\nTEST_FIELD=test\nAPP_DOMAIN=192.168.1.10:${app1.port}`);
     expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${app1.port}\nTEST_FIELD=test\nAPP_DOMAIN=192.168.1.10:${app1.port}`);
   });
   });
@@ -352,8 +352,8 @@ describe('Update app config', () => {
     // @ts-ignore
     // @ts-ignore
     fs.__createMockFiles(MockFiles);
     fs.__createMockFiles(MockFiles);
 
 
-    const envFile = fs.readFileSync(`${config.ROOT_FOLDER}/app-data/${appInfo.id}/app.env`).toString();
-    fs.writeFileSync(`${config.ROOT_FOLDER}/app-data/${appInfo.id}/app.env`, `${envFile}\nRANDOM_FIELD=test`);
+    const envFile = fs.readFileSync(`${getConfig().rootFolder}/app-data/${appInfo.id}/app.env`).toString();
+    fs.writeFileSync(`${getConfig().rootFolder}/app-data/${appInfo.id}/app.env`, `${envFile}\nRANDOM_FIELD=test`);
 
 
     await AppsService.updateAppConfig(appInfo.id, { TEST_FIELD: 'test' });
     await AppsService.updateAppConfig(appInfo.id, { TEST_FIELD: 'test' });
 
 
@@ -470,8 +470,8 @@ describe('Start all apps', () => {
 
 
     expect(spy.mock.calls.length).toBe(2);
     expect(spy.mock.calls.length).toBe(2);
     expect(spy.mock.calls).toEqual([
     expect(spy.mock.calls).toEqual([
-      [`${config.ROOT_FOLDER}/scripts/app.sh`, ['start', app1.id], {}, expect.any(Function)],
-      [`${config.ROOT_FOLDER}/scripts/app.sh`, ['start', app2.id], {}, expect.any(Function)],
+      [`${getConfig().rootFolder}/scripts/app.sh`, ['start', app1.id], {}, expect.any(Function)],
+      [`${getConfig().rootFolder}/scripts/app.sh`, ['start', app2.id], {}, expect.any(Function)],
     ]);
     ]);
   });
   });
 
 

+ 13 - 13
packages/system-api/src/modules/apps/apps.helpers.ts

@@ -2,15 +2,17 @@ import portUsed from 'tcp-port-used';
 import { fileExists, getSeed, readdirSync, readFile, readJsonFile, runScript, writeFile } from '../fs/fs.helpers';
 import { fileExists, getSeed, readdirSync, readFile, readJsonFile, runScript, writeFile } from '../fs/fs.helpers';
 import InternalIp from 'internal-ip';
 import InternalIp from 'internal-ip';
 import crypto from 'crypto';
 import crypto from 'crypto';
-import config from '../../config';
 import { AppInfo, AppStatusEnum } from './apps.types';
 import { AppInfo, AppStatusEnum } from './apps.types';
 import logger from '../../config/logger/logger';
 import logger from '../../config/logger/logger';
 import App from './app.entity';
 import App from './app.entity';
+import { getConfig } from '../../core/config/TipiConfig';
+
+const { appsRepoId, internalIp } = getConfig();
 
 
 export const checkAppRequirements = async (appName: string) => {
 export const checkAppRequirements = async (appName: string) => {
   let valid = true;
   let valid = true;
 
 
-  const configFile: AppInfo | null = readJsonFile(`/repos/${config.APPS_REPO_ID}/apps/${appName}/config.json`);
+  const configFile: AppInfo | null = readJsonFile(`/repos/${appsRepoId}/apps/${appName}/config.json`);
 
 
   if (!configFile) {
   if (!configFile) {
     throw new Error(`App ${appName} not found`);
     throw new Error(`App ${appName} not found`);
@@ -110,7 +112,7 @@ export const generateEnvFile = (app: App) => {
     envFile += `APP_DOMAIN=${app.domain}\n`;
     envFile += `APP_DOMAIN=${app.domain}\n`;
     envFile += 'APP_PROTOCOL=https\n';
     envFile += 'APP_PROTOCOL=https\n';
   } else {
   } else {
-    envFile += `APP_DOMAIN=${config.INTERNAL_IP}:${configFile.port}\n`;
+    envFile += `APP_DOMAIN=${internalIp}:${configFile.port}\n`;
   }
   }
 
 
   writeFile(`/app-data/${app.id}/app.env`, envFile);
   writeFile(`/app-data/${app.id}/app.env`, envFile);
@@ -119,11 +121,11 @@ export const generateEnvFile = (app: App) => {
 export const getAvailableApps = async (): Promise<string[]> => {
 export const getAvailableApps = async (): Promise<string[]> => {
   const apps: string[] = [];
   const apps: string[] = [];
 
 
-  const appsDir = readdirSync(`/repos/${config.APPS_REPO_ID}/apps`);
+  const appsDir = readdirSync(`/repos/${appsRepoId}/apps`);
 
 
   appsDir.forEach((app) => {
   appsDir.forEach((app) => {
-    if (fileExists(`/repos/${config.APPS_REPO_ID}/apps/${app}/config.json`)) {
-      const configFile: AppInfo = readJsonFile(`/repos/${config.APPS_REPO_ID}/apps/${app}/config.json`);
+    if (fileExists(`/repos/${appsRepoId}/apps/${app}/config.json`)) {
+      const configFile: AppInfo = readJsonFile(`/repos/${appsRepoId}/apps/${app}/config.json`);
 
 
       if (configFile.available) {
       if (configFile.available) {
         apps.push(app);
         apps.push(app);
@@ -136,8 +138,6 @@ export const getAvailableApps = async (): Promise<string[]> => {
 
 
 export const getAppInfo = (id: string, status?: AppStatusEnum): AppInfo | null => {
 export const getAppInfo = (id: string, status?: AppStatusEnum): AppInfo | null => {
   try {
   try {
-    const repoId = config.APPS_REPO_ID;
-
     // Check if app is installed
     // Check if app is installed
     const installed = typeof status !== 'undefined' && status !== AppStatusEnum.MISSING;
     const installed = typeof status !== 'undefined' && status !== AppStatusEnum.MISSING;
 
 
@@ -145,9 +145,9 @@ export const getAppInfo = (id: string, status?: AppStatusEnum): AppInfo | null =
       const configFile: AppInfo = readJsonFile(`/apps/${id}/config.json`);
       const configFile: AppInfo = readJsonFile(`/apps/${id}/config.json`);
       configFile.description = readFile(`/apps/${id}/metadata/description.md`).toString();
       configFile.description = readFile(`/apps/${id}/metadata/description.md`).toString();
       return configFile;
       return configFile;
-    } else if (fileExists(`/repos/${repoId}/apps/${id}/config.json`)) {
-      const configFile: AppInfo = readJsonFile(`/repos/${repoId}/apps/${id}/config.json`);
-      configFile.description = readFile(`/repos/${repoId}/apps/${id}/metadata/description.md`);
+    } else if (fileExists(`/repos/${appsRepoId}/apps/${id}/config.json`)) {
+      const configFile: AppInfo = readJsonFile(`/repos/${appsRepoId}/apps/${id}/config.json`);
+      configFile.description = readFile(`/repos/${appsRepoId}/apps/${id}/metadata/description.md`);
 
 
       if (configFile.available) {
       if (configFile.available) {
         return configFile;
         return configFile;
@@ -164,13 +164,13 @@ export const getAppInfo = (id: string, status?: AppStatusEnum): AppInfo | null =
 export const getUpdateInfo = async (id: string) => {
 export const getUpdateInfo = async (id: string) => {
   const app = await App.findOne({ where: { id } });
   const app = await App.findOne({ where: { id } });
 
 
-  const doesFileExist = fileExists(`/repos/${config.APPS_REPO_ID}/apps/${id}`);
+  const doesFileExist = fileExists(`/repos/${appsRepoId}/apps/${id}`);
 
 
   if (!app || !doesFileExist) {
   if (!app || !doesFileExist) {
     return null;
     return null;
   }
   }
 
 
-  const repoConfig: AppInfo = readJsonFile(`/repos/${config.APPS_REPO_ID}/apps/${id}/config.json`);
+  const repoConfig: AppInfo = readJsonFile(`/repos/${appsRepoId}/apps/${id}/config.json`);
 
 
   return {
   return {
     current: app.version,
     current: app.version,

+ 3 - 3
packages/system-api/src/modules/apps/apps.service.ts

@@ -4,8 +4,8 @@ import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps,
 import { AppInfo, AppStatusEnum, ListAppsResonse } from './apps.types';
 import { AppInfo, AppStatusEnum, ListAppsResonse } from './apps.types';
 import App from './app.entity';
 import App from './app.entity';
 import logger from '../../config/logger/logger';
 import logger from '../../config/logger/logger';
-import config from '../../config';
 import { Not } from 'typeorm';
 import { Not } from 'typeorm';
+import { getConfig } from '../../core/config/TipiConfig';
 
 
 const sortApps = (a: AppInfo, b: AppInfo) => a.name.localeCompare(b.name);
 const sortApps = (a: AppInfo, b: AppInfo) => a.name.localeCompare(b.name);
 
 
@@ -124,7 +124,7 @@ const listApps = async (): Promise<ListAppsResonse> => {
   const apps: AppInfo[] = folders
   const apps: AppInfo[] = folders
     .map((app) => {
     .map((app) => {
       try {
       try {
-        return readJsonFile(`/repos/${config.APPS_REPO_ID}/apps/${app}/config.json`);
+        return readJsonFile(`/repos/${getConfig().appsRepoId}/apps/${app}/config.json`);
       } catch (e) {
       } catch (e) {
         return null;
         return null;
       }
       }
@@ -132,7 +132,7 @@ const listApps = async (): Promise<ListAppsResonse> => {
     .filter(Boolean);
     .filter(Boolean);
 
 
   apps.forEach((app) => {
   apps.forEach((app) => {
-    app.description = readFile(`/repos/${config.APPS_REPO_ID}/apps/${app.id}/metadata/description.md`);
+    app.description = readFile(`/repos/${getConfig().appsRepoId}/apps/${app.id}/metadata/description.md`);
   });
   });
 
 
   return { apps: apps.sort(sortApps), total: apps.length };
   return { apps: apps.sort(sortApps), total: apps.length };

+ 21 - 21
packages/system-api/src/modules/fs/__tests__/fs.helpers.test.ts

@@ -1,7 +1,7 @@
 import childProcess from 'child_process';
 import childProcess from 'child_process';
-import config from '../../../config';
 import { getAbsolutePath, readJsonFile, readFile, readdirSync, fileExists, writeFile, createFolder, deleteFolder, runScript, getSeed, ensureAppFolder } from '../fs.helpers';
 import { getAbsolutePath, readJsonFile, readFile, readdirSync, fileExists, writeFile, createFolder, deleteFolder, runScript, getSeed, ensureAppFolder } from '../fs.helpers';
 import fs from 'fs-extra';
 import fs from 'fs-extra';
+import { getConfig } from '../../../core/config/TipiConfig';
 
 
 jest.mock('fs-extra');
 jest.mock('fs-extra');
 
 
@@ -12,7 +12,7 @@ beforeEach(() => {
 
 
 describe('Test: getAbsolutePath', () => {
 describe('Test: getAbsolutePath', () => {
   it('should return the absolute path', () => {
   it('should return the absolute path', () => {
-    expect(getAbsolutePath('/test')).toBe(`${config.ROOT_FOLDER}/test`);
+    expect(getAbsolutePath('/test')).toBe(`${getConfig().rootFolder}/test`);
   });
   });
 });
 });
 
 
@@ -21,7 +21,7 @@ describe('Test: readJsonFile', () => {
     // Arrange
     // Arrange
     const rawFile = '{"test": "test"}';
     const rawFile = '{"test": "test"}';
     const mockFiles = {
     const mockFiles = {
-      [`${config.ROOT_FOLDER}/test-file.json`]: rawFile,
+      [`${getConfig().rootFolder}/test-file.json`]: rawFile,
     };
     };
     // @ts-ignore
     // @ts-ignore
     fs.__createMockFiles(mockFiles);
     fs.__createMockFiles(mockFiles);
@@ -42,7 +42,7 @@ describe('Test: readFile', () => {
   it('should return the file', () => {
   it('should return the file', () => {
     const rawFile = 'test';
     const rawFile = 'test';
     const mockFiles = {
     const mockFiles = {
-      [`${config.ROOT_FOLDER}/test-file.txt`]: rawFile,
+      [`${getConfig().rootFolder}/test-file.txt`]: rawFile,
     };
     };
 
 
     // @ts-ignore
     // @ts-ignore
@@ -59,7 +59,7 @@ describe('Test: readFile', () => {
 describe('Test: readdirSync', () => {
 describe('Test: readdirSync', () => {
   it('should return the files', () => {
   it('should return the files', () => {
     const mockFiles = {
     const mockFiles = {
-      [`${config.ROOT_FOLDER}/test/test-file.txt`]: 'test',
+      [`${getConfig().rootFolder}/test/test-file.txt`]: 'test',
     };
     };
 
 
     // @ts-ignore
     // @ts-ignore
@@ -76,7 +76,7 @@ describe('Test: readdirSync', () => {
 describe('Test: fileExists', () => {
 describe('Test: fileExists', () => {
   it('should return true if the file exists', () => {
   it('should return true if the file exists', () => {
     const mockFiles = {
     const mockFiles = {
-      [`${config.ROOT_FOLDER}/test-file.txt`]: 'test',
+      [`${getConfig().rootFolder}/test-file.txt`]: 'test',
     };
     };
 
 
     // @ts-ignore
     // @ts-ignore
@@ -96,7 +96,7 @@ describe('Test: writeFile', () => {
 
 
     writeFile('/test-file.txt', 'test');
     writeFile('/test-file.txt', 'test');
 
 
-    expect(spy).toHaveBeenCalledWith(`${config.ROOT_FOLDER}/test-file.txt`, 'test');
+    expect(spy).toHaveBeenCalledWith(`${getConfig().rootFolder}/test-file.txt`, 'test');
   });
   });
 });
 });
 
 
@@ -106,7 +106,7 @@ describe('Test: createFolder', () => {
 
 
     createFolder('/test');
     createFolder('/test');
 
 
-    expect(spy).toHaveBeenCalledWith(`${config.ROOT_FOLDER}/test`);
+    expect(spy).toHaveBeenCalledWith(`${getConfig().rootFolder}/test`);
   });
   });
 });
 });
 
 
@@ -116,7 +116,7 @@ describe('Test: deleteFolder', () => {
 
 
     deleteFolder('/test');
     deleteFolder('/test');
 
 
-    expect(spy).toHaveBeenCalledWith(`${config.ROOT_FOLDER}/test`, { recursive: true });
+    expect(spy).toHaveBeenCalledWith(`${getConfig().rootFolder}/test`, { recursive: true });
   });
   });
 });
 });
 
 
@@ -127,14 +127,14 @@ describe('Test: runScript', () => {
 
 
     runScript('/test', [], callback);
     runScript('/test', [], callback);
 
 
-    expect(spy).toHaveBeenCalledWith(`${config.ROOT_FOLDER}/test`, [], {}, callback);
+    expect(spy).toHaveBeenCalledWith(`${getConfig().rootFolder}/test`, [], {}, callback);
   });
   });
 });
 });
 
 
 describe('Test: getSeed', () => {
 describe('Test: getSeed', () => {
   it('should return the seed', () => {
   it('should return the seed', () => {
     const mockFiles = {
     const mockFiles = {
-      [`${config.ROOT_FOLDER}/state/seed`]: 'test',
+      [`${getConfig().rootFolder}/state/seed`]: 'test',
     };
     };
 
 
     // @ts-ignore
     // @ts-ignore
@@ -147,7 +147,7 @@ describe('Test: getSeed', () => {
 describe('Test: ensureAppFolder', () => {
 describe('Test: ensureAppFolder', () => {
   beforeEach(() => {
   beforeEach(() => {
     const mockFiles = {
     const mockFiles = {
-      [`${config.ROOT_FOLDER}/repos/${config.APPS_REPO_ID}/apps/test`]: ['test.yml'],
+      [`${getConfig().rootFolder}/repos/${getConfig().appsRepoId}/apps/test`]: ['test.yml'],
     };
     };
     // @ts-ignore
     // @ts-ignore
     fs.__createMockFiles(mockFiles);
     fs.__createMockFiles(mockFiles);
@@ -158,15 +158,15 @@ describe('Test: ensureAppFolder', () => {
     ensureAppFolder('test');
     ensureAppFolder('test');
 
 
     // Assert
     // Assert
-    const files = fs.readdirSync(`${config.ROOT_FOLDER}/apps/test`);
+    const files = fs.readdirSync(`${getConfig().rootFolder}/apps/test`);
     expect(files).toEqual(['test.yml']);
     expect(files).toEqual(['test.yml']);
   });
   });
 
 
   it('should not copy the folder if it already exists', () => {
   it('should not copy the folder if it already exists', () => {
     const mockFiles = {
     const mockFiles = {
-      [`${config.ROOT_FOLDER}/repos/${config.APPS_REPO_ID}/apps/test`]: ['test.yml'],
-      [`${config.ROOT_FOLDER}/apps/test`]: ['docker-compose.yml'],
-      [`${config.ROOT_FOLDER}/apps/test/docker-compose.yml`]: 'test',
+      [`${getConfig().rootFolder}/repos/${getConfig().appsRepoId}/apps/test`]: ['test.yml'],
+      [`${getConfig().rootFolder}/apps/test`]: ['docker-compose.yml'],
+      [`${getConfig().rootFolder}/apps/test/docker-compose.yml`]: 'test',
     };
     };
 
 
     // @ts-ignore
     // @ts-ignore
@@ -176,15 +176,15 @@ describe('Test: ensureAppFolder', () => {
     ensureAppFolder('test');
     ensureAppFolder('test');
 
 
     // Assert
     // Assert
-    const files = fs.readdirSync(`${config.ROOT_FOLDER}/apps/test`);
+    const files = fs.readdirSync(`${getConfig().rootFolder}/apps/test`);
     expect(files).toEqual(['docker-compose.yml']);
     expect(files).toEqual(['docker-compose.yml']);
   });
   });
 
 
   it('Should overwrite the folder if clean up is true', () => {
   it('Should overwrite the folder if clean up is true', () => {
     const mockFiles = {
     const mockFiles = {
-      [`${config.ROOT_FOLDER}/repos/${config.APPS_REPO_ID}/apps/test`]: ['test.yml'],
-      [`${config.ROOT_FOLDER}/apps/test`]: ['docker-compose.yml'],
-      [`${config.ROOT_FOLDER}/apps/test/docker-compose.yml`]: 'test',
+      [`${getConfig().rootFolder}/repos/${getConfig().appsRepoId}/apps/test`]: ['test.yml'],
+      [`${getConfig().rootFolder}/apps/test`]: ['docker-compose.yml'],
+      [`${getConfig().rootFolder}/apps/test/docker-compose.yml`]: 'test',
     };
     };
 
 
     // @ts-ignore
     // @ts-ignore
@@ -194,7 +194,7 @@ describe('Test: ensureAppFolder', () => {
     ensureAppFolder('test', true);
     ensureAppFolder('test', true);
 
 
     // Assert
     // Assert
-    const files = fs.readdirSync(`${config.ROOT_FOLDER}/apps/test`);
+    const files = fs.readdirSync(`${getConfig().rootFolder}/apps/test`);
     expect(files).toEqual(['test.yml']);
     expect(files).toEqual(['test.yml']);
   });
   });
 });
 });

+ 3 - 3
packages/system-api/src/modules/fs/fs.helpers.ts

@@ -1,8 +1,8 @@
 import fs from 'fs-extra';
 import fs from 'fs-extra';
 import childProcess from 'child_process';
 import childProcess from 'child_process';
-import config from '../../config';
+import { getConfig } from '../../core/config/TipiConfig';
 
 
-export const getAbsolutePath = (path: string) => `${config.ROOT_FOLDER}${path}`;
+export const getAbsolutePath = (path: string) => `${getConfig().rootFolder}${path}`;
 
 
 export const readJsonFile = (path: string): any => {
 export const readJsonFile = (path: string): any => {
   try {
   try {
@@ -54,6 +54,6 @@ export const ensureAppFolder = (appName: string, cleanup = false) => {
   if (!fileExists(`/apps/${appName}/docker-compose.yml`)) {
   if (!fileExists(`/apps/${appName}/docker-compose.yml`)) {
     if (fileExists(`/apps/${appName}`)) deleteFolder(`/apps/${appName}`);
     if (fileExists(`/apps/${appName}`)) deleteFolder(`/apps/${appName}`);
     // Copy from apps repo
     // Copy from apps repo
-    fs.copySync(getAbsolutePath(`/repos/${config.APPS_REPO_ID}/apps/${appName}`), getAbsolutePath(`/apps/${appName}`));
+    fs.copySync(getAbsolutePath(`/repos/${getConfig().appsRepoId}/apps/${appName}`), getAbsolutePath(`/apps/${appName}`));
   }
   }
 };
 };

+ 3 - 3
packages/system-api/src/modules/system/system.service.ts

@@ -1,6 +1,6 @@
 import axios from 'axios';
 import axios from 'axios';
-import config from '../../config';
 import TipiCache from '../../config/TipiCache';
 import TipiCache from '../../config/TipiCache';
+import { getConfig } from '../../core/config/TipiConfig';
 import { readJsonFile } from '../fs/fs.helpers';
 import { readJsonFile } from '../fs/fs.helpers';
 
 
 type SystemInfo = {
 type SystemInfo = {
@@ -38,9 +38,9 @@ const getVersion = async (): Promise<{ current: string; latest?: string }> => {
 
 
     TipiCache.set('latestVersion', version?.replace('v', ''));
     TipiCache.set('latestVersion', version?.replace('v', ''));
 
 
-    return { current: config.VERSION, latest: version?.replace('v', '') };
+    return { current: getConfig().version, latest: version?.replace('v', '') };
   } catch (e) {
   } catch (e) {
-    return { current: config.VERSION, latest: undefined };
+    return { current: getConfig().version, latest: undefined };
   }
   }
 };
 };
 
 

+ 22 - 8
packages/system-api/src/server.ts

@@ -16,9 +16,8 @@ import { runUpdates } from './core/updates/run';
 import recover from './core/updates/recover-migrations';
 import recover from './core/updates/recover-migrations';
 import { cloneRepo, updateRepo } from './helpers/repo-helpers';
 import { cloneRepo, updateRepo } from './helpers/repo-helpers';
 import startJobs from './core/jobs/jobs';
 import startJobs from './core/jobs/jobs';
-import { getConfig } from './core/config/TipiConfig';
-
-const { clientUrls, rootFolder, appsRepoId, appsRepoUrl } = getConfig();
+import { applyJsonConfig, getConfig } from './core/config/TipiConfig';
+import { ZodError } from 'zod';
 
 
 let corsOptions = {
 let corsOptions = {
   credentials: true,
   credentials: true,
@@ -29,7 +28,7 @@ let corsOptions = {
     // disallow requests with no origin
     // disallow requests with no origin
     if (!origin) return callback(new Error('Not allowed by CORS'), false);
     if (!origin) return callback(new Error('Not allowed by CORS'), false);
 
 
-    if (clientUrls.includes(origin)) {
+    if (getConfig().clientUrls.includes(origin)) {
       return callback(null, true);
       return callback(null, true);
     }
     }
 
 
@@ -38,12 +37,27 @@ let corsOptions = {
   },
   },
 };
 };
 
 
+const applyCustomConfig = () => {
+  try {
+    applyJsonConfig();
+  } catch (e) {
+    logger.error('Error applying settings.json config');
+    if (e instanceof ZodError) {
+      Object.keys(e.flatten().fieldErrors).forEach((key) => {
+        logger.error(`Error in field ${key}`);
+      });
+    }
+  }
+};
+
 const main = async () => {
 const main = async () => {
   try {
   try {
+    applyCustomConfig();
+
     const app = express();
     const app = express();
     const port = 3001;
     const port = 3001;
 
 
-    app.use(express.static(`${rootFolder}/repos/${appsRepoId}`));
+    app.use(express.static(`${getConfig().rootFolder}/repos/${getConfig().appsRepoId}`));
     app.use(cors(corsOptions));
     app.use(cors(corsOptions));
     app.use(getSessionMiddleware());
     app.use(getSessionMiddleware());
 
 
@@ -77,15 +91,15 @@ const main = async () => {
     await runUpdates();
     await runUpdates();
 
 
     httpServer.listen(port, async () => {
     httpServer.listen(port, async () => {
-      await cloneRepo(appsRepoUrl);
-      await updateRepo(appsRepoId);
+      await cloneRepo(getConfig().appsRepoUrl);
+      await updateRepo(getConfig().appsRepoUrl);
       startJobs();
       startJobs();
       // Start apps
       // Start apps
       appsService.startAllApps();
       appsService.startAllApps();
       console.info(`Server running on port ${port} 🚀 Production => ${__prod__}`);
       console.info(`Server running on port ${port} 🚀 Production => ${__prod__}`);
     });
     });
   } catch (error) {
   } catch (error) {
-    console.log(error);
+    console.error(error);
     logger.error(error);
     logger.error(error);
   }
   }
 };
 };

+ 18 - 65
scripts/app.sh

@@ -4,46 +4,28 @@
 
 
 set -euo pipefail
 set -euo pipefail
 
 
-# use greadlink instead of readlink on osx
-if [[ "$(uname)" == "Darwin" ]]; then
-  rdlk=greadlink
-else
-  rdlk=readlink
+cd /runtipi || echo ""
+# Ensure PWD ends with /runtipi
+if [[ "${PWD##*/}" != "runtipi" ]]; then
+  echo "Please run this script from the runtipi directory"
+  exit 1
 fi
 fi
 
 
-ROOT_FOLDER="$($rdlk -f $(dirname "${BASH_SOURCE[0]}")/..)"
-REPO_ID="$(echo -n "https://github.com/meienberger/runtipi-appstore" | sha256sum | awk '{print $1}')"
-STATE_FOLDER="${ROOT_FOLDER}/state"
-
-show_help() {
-  cat <<EOF
-app 0.0.1
+# Root folder in container is /runtipi
+ROOT_FOLDER="${PWD}"
 
 
-CLI for managing Tipi apps
+ENV_FILE="${ROOT_FOLDER}/.env"
 
 
-Usage: app <command> <app> [<arguments>]
-
-Commands:
-    install                    Pulls down images for an app and starts it
-    uninstall                  Removes images and destroys all data for an app
-    stop                       Stops an installed app
-    start                      Starts an installed app
-    compose                    Passes all arguments to Docker Compose
-    ls-installed               Lists installed apps
-EOF
-}
+# Root folder in host system
+ROOT_FOLDER_HOST=$(grep -v '^#' "${ENV_FILE}" | xargs -n 1 | grep ROOT_FOLDER_HOST | cut -d '=' -f2)
+REPO_ID=$(grep -v '^#' "${ENV_FILE}" | xargs -n 1 | grep APPS_REPO_ID | cut -d '=' -f2)
 
 
 # Get field from json file
 # Get field from json file
 function get_json_field() {
 function get_json_field() {
   local json_file="$1"
   local json_file="$1"
   local field="$2"
   local field="$2"
 
 
-  echo $(jq -r ".${field}" "${json_file}")
-}
-
-list_installed_apps() {
-  str=$(get_json_field ${STATE_FOLDER}/apps.json installed)
-  echo $str
+  jq -r ".${field}" "${json_file}"
 }
 }
 
 
 if [ -z ${1+x} ]; then
 if [ -z ${1+x} ]; then
@@ -52,31 +34,11 @@ else
   command="$1"
   command="$1"
 fi
 fi
 
 
-# Lists installed apps
-if [[ "$command" = "ls-installed" ]]; then
-  list_installed_apps
-
-  exit
-fi
-
 if [ -z ${2+x} ]; then
 if [ -z ${2+x} ]; then
   show_help
   show_help
   exit 1
   exit 1
 else
 else
-
   app="$2"
   app="$2"
-  root_folder_host="${3:-$ROOT_FOLDER}"
-  repo_id="${4:-$REPO_ID}"
-
-  if [[ -z "${repo_id}" ]]; then
-    echo "Error: Repo id not provided"
-    exit 1
-  fi
-
-  if [[ -z "${root_folder_host}" ]]; then
-    echo "Error: Root folder not provided"
-    exit 1
-  fi
 
 
   app_dir="${ROOT_FOLDER}/apps/${app}"
   app_dir="${ROOT_FOLDER}/apps/${app}"
 
 
@@ -84,7 +46,7 @@ else
     # copy from repo
     # copy from repo
     echo "Copying app from repo"
     echo "Copying app from repo"
     mkdir -p "${app_dir}"
     mkdir -p "${app_dir}"
-    cp -r "${ROOT_FOLDER}/repos/${repo_id}/apps/${app}"/* "${app_dir}"
+    cp -r "${ROOT_FOLDER}/repos/${REPO_ID}/apps/${app}"/* "${app_dir}"
   fi
   fi
 
 
   app_data_dir="${ROOT_FOLDER}/app-data/${app}"
   app_data_dir="${ROOT_FOLDER}/app-data/${app}"
@@ -113,7 +75,6 @@ compose() {
   fi
   fi
 
 
   # App data folder
   # App data folder
-  local env_file="${ROOT_FOLDER}/.env"
   local app_compose_file="${app_dir}/docker-compose.yml"
   local app_compose_file="${app_dir}/docker-compose.yml"
 
 
   # Pick arm architecture if running on arm and if the app has a docker-compose.arm.yml file
   # Pick arm architecture if running on arm and if the app has a docker-compose.arm.yml file
@@ -121,19 +82,14 @@ compose() {
     app_compose_file="${app_dir}/docker-compose.arm.yml"
     app_compose_file="${app_dir}/docker-compose.arm.yml"
   fi
   fi
 
 
-  local common_compose_file="${ROOT_FOLDER}/repos/${repo_id}/apps/docker-compose.common.yml"
+  local common_compose_file="${ROOT_FOLDER}/repos/${REPO_ID}/apps/docker-compose.common.yml"
 
 
   # Vars to use in compose file
   # Vars to use in compose file
-  export APP_DATA_DIR="${root_folder_host}/app-data/${app}"
-  export APP_DIR="${app_dir}"
-  export ROOT_FOLDER_HOST="${root_folder_host}"
-  export ROOT_FOLDER="${ROOT_FOLDER}"
-
-  # Docker Compose does not support multiple env files
-  # --env-file "${env_file}" \
+  export APP_DATA_DIR="${ROOT_FOLDER_HOST}/app-data/${app}"
+  export ROOT_FOLDER_HOST="${ROOT_FOLDER_HOST}"
 
 
   docker compose \
   docker compose \
-    --env-file "${ROOT_FOLDER}/app-data/${app}/app.env" \
+    --env-file "${app_data_dir}/app.env" \
     --project-name "${app}" \
     --project-name "${app}" \
     --file "${app_compose_file}" \
     --file "${app_compose_file}" \
     --file "${common_compose_file}" \
     --file "${common_compose_file}" \
@@ -189,7 +145,7 @@ if [[ "$command" = "update" ]]; then
   fi
   fi
 
 
   # Copy app from repo
   # Copy app from repo
-  cp -r "${ROOT_FOLDER}/repos/${repo_id}/apps/${app}" "${app_dir}"
+  cp -r "${ROOT_FOLDER}/repos/${REPO_ID}/apps/${app}" "${app_dir}"
 
 
   compose "${app}" pull
   compose "${app}" pull
   exit
   exit
@@ -215,7 +171,4 @@ if [[ "$command" = "compose" ]]; then
   exit
   exit
 fi
 fi
 
 
-# If we get here it means no valid command was supplied
-# Show help and exit
-show_help
 exit 1
 exit 1

+ 0 - 12
scripts/configure.sh

@@ -1,16 +1,4 @@
 #!/usr/bin/env bash
 #!/usr/bin/env bash
-ROOT_FOLDER="$(readlink -f "$(dirname "${BASH_SOURCE[0]}")"/..)"
-
-echo
-echo "======================================"
-if [[ -f "${ROOT_FOLDER}/state/configured" ]]; then
-  echo "=========== RECONFIGURING ============"
-else
-  echo "============ CONFIGURING ============="
-fi
-echo "=============== TIPI ================="
-echo "======================================"
-echo
 
 
 function install_docker() {
 function install_docker() {
   local os="${1}"
   local os="${1}"

+ 8 - 6
scripts/git.sh

@@ -1,13 +1,15 @@
 #!/usr/bin/env bash
 #!/usr/bin/env bash
+# Don't break if command fails
 
 
-# use greadlink instead of readlink on osx
-if [[ "$(uname)" == "Darwin" ]]; then
-    rdlk=greadlink
-else
-    rdlk=readlink
+cd /runtipi || echo ""
+# Ensure PWD ends with /runtipi
+if [[ "${PWD##*/}" != "runtipi" ]]; then
+    echo ${PWD}
+    echo "Please run this script from the runtipi directory"
+    exit 1
 fi
 fi
 
 
-ROOT_FOLDER="$($rdlk -f $(dirname "${BASH_SOURCE[0]}")/..)"
+ROOT_FOLDER="${PWD}"
 
 
 show_help() {
 show_help() {
     cat <<EOF
     cat <<EOF

+ 26 - 1
scripts/start.sh

@@ -106,7 +106,7 @@ if [[ "${NGINX_PORT}" != "80" ]] && [[ "${DOMAIN}" != "tipi.localhost" ]]; then
   exit 1
   exit 1
 fi
 fi
 
 
-ROOT_FOLDER="$($readlink -f $(dirname "${BASH_SOURCE[0]}")/..)"
+ROOT_FOLDER="${PWD}"
 STATE_FOLDER="${ROOT_FOLDER}/state"
 STATE_FOLDER="${ROOT_FOLDER}/state"
 SED_ROOT_FOLDER="$(echo $ROOT_FOLDER | sed 's/\//\\\//g')"
 SED_ROOT_FOLDER="$(echo $ROOT_FOLDER | sed 's/\//\\\//g')"
 
 
@@ -187,6 +187,31 @@ JWT_SECRET=$(derive_entropy "jwt")
 POSTGRES_PASSWORD=$(derive_entropy "postgres")
 POSTGRES_PASSWORD=$(derive_entropy "postgres")
 TIPI_VERSION=$(get_json_field "${ROOT_FOLDER}/package.json" version)
 TIPI_VERSION=$(get_json_field "${ROOT_FOLDER}/package.json" version)
 
 
+# Override vars with values from settings.json
+if [[ -f "${STATE_FOLDER}/settings.json" ]]; then
+
+  # If dnsIp is set in settings.json, use it
+  if [[ "$(get_json_field "${STATE_FOLDER}/settings.json" dnsIp)" != "null" ]]; then
+    DNS_IP=$(get_json_field "${STATE_FOLDER}/settings.json" dnsIp)
+  fi
+
+  # If domain is set in settings.json, use it
+  if [[ "$(get_json_field "${STATE_FOLDER}/settings.json" domain)" != "null" ]]; then
+    DOMAIN=$(get_json_field "${STATE_FOLDER}/settings.json" domain)
+  fi
+
+  # If appsRepoUrl is set in settings.json, use it
+  if [[ "$(get_json_field "${STATE_FOLDER}/settings.json" appsRepoUrl)" != "null" ]]; then
+    APPS_REPOSITORY_ESCAPED="$(echo ${APPS_REPOSITORY} | sed 's/\//\\\//g')"
+  fi
+
+  # If appsRepoId is set in settings.json, use it
+  if [[ "$(get_json_field "${STATE_FOLDER}/settings.json" appsRepoId)" != "null" ]]; then
+    REPO_ID=$(get_json_field "${STATE_FOLDER}/settings.json" appsRepoId)
+  fi
+
+fi
+
 echo "Creating .env file with the following values:"
 echo "Creating .env file with the following values:"
 echo "  DOMAIN=${DOMAIN}"
 echo "  DOMAIN=${DOMAIN}"
 echo "  INTERNAL_IP=${INTERNAL_IP}"
 echo "  INTERNAL_IP=${INTERNAL_IP}"

+ 6 - 10
scripts/stop.sh

@@ -1,13 +1,6 @@
 #!/usr/bin/env bash
 #!/usr/bin/env bash
 set -euo pipefail
 set -euo pipefail
 
 
-# use greadlink instead of readlink on osx
-if [[ "$(uname)" == "Darwin" ]]; then
-  readlink=greadlink
-else
-  readlink=readlink
-fi
-
 if [[ $UID != 0 ]]; then
 if [[ $UID != 0 ]]; then
   echo "Tipi must be stopped as root"
   echo "Tipi must be stopped as root"
   echo "Please re-run this script as"
   echo "Please re-run this script as"
@@ -15,10 +8,13 @@ if [[ $UID != 0 ]]; then
   exit 1
   exit 1
 fi
 fi
 
 
-ROOT_FOLDER="$($readlink -f $(dirname "${BASH_SOURCE[0]}")/..)"
-STATE_FOLDER="${ROOT_FOLDER}/state"
+# Ensure PWD ends with /runtipi
+if [[ $(basename $(pwd)) != "runtipi" ]] || [[ ! -f "${BASH_SOURCE[0]}" ]]; then
+  echo "Please run this script from the runtipi directory"
+  exit 1
+fi
 
 
-cd "$ROOT_FOLDER"
+ROOT_FOLDER="${PWD}"
 
 
 export DOCKER_CLIENT_TIMEOUT=240
 export DOCKER_CLIENT_TIMEOUT=240
 export COMPOSE_HTTP_TIMEOUT=240
 export COMPOSE_HTTP_TIMEOUT=240

+ 13 - 6
scripts/system-info.sh

@@ -1,25 +1,32 @@
 #!/usr/bin/env bash
 #!/usr/bin/env bash
 set -e # Exit immediately if a command exits with a non-zero status.
 set -e # Exit immediately if a command exits with a non-zero status.
 
 
-ROOT_FOLDER="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/..)"
+cd /runtipi || echo ""
+# Ensure PWD ends with /runtipi
+if [[ "${PWD##*/}" != "runtipi" ]]; then
+    echo "Please run this script from the runtipi directory"
+    exit 1
+fi
+
+ROOT_FOLDER="${PWD}"
 STATE_FOLDER="${ROOT_FOLDER}/state"
 STATE_FOLDER="${ROOT_FOLDER}/state"
 
 
 # Available disk space
 # Available disk space
 TOTAL_DISK_SPACE_BYTES=$(df -P -B 1 / | tail -n 1 | awk '{print $2}')
 TOTAL_DISK_SPACE_BYTES=$(df -P -B 1 / | tail -n 1 | awk '{print $2}')
 AVAILABLE_DISK_SPACE_BYTES=$(df -P -B 1 / | tail -n 1 | awk '{print $4}')
 AVAILABLE_DISK_SPACE_BYTES=$(df -P -B 1 / | tail -n 1 | awk '{print $4}')
-USED_DISK_SPACE_BYTES=$(($TOTAL_DISK_SPACE_BYTES - $AVAILABLE_DISK_SPACE_BYTES))
+USED_DISK_SPACE_BYTES=$((TOTAL_DISK_SPACE_BYTES - AVAILABLE_DISK_SPACE_BYTES))
 
 
 # CPU info
 # CPU info
 CPU_LOAD_PERCENTAGE=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}')
 CPU_LOAD_PERCENTAGE=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}')
 
 
 # Memory info
 # Memory info
-MEM_TOTAL_BYTES=$(($(cat /proc/meminfo | grep MemTotal |  awk '{print $2}') * 1024))
-MEM_AVAILABLE_BYTES=$(($(cat /proc/meminfo | grep MemAvailable |  awk '{print $2}') * 1024))
-MEM_USED_BYTES=$(($MEM_TOTAL_BYTES - $MEM_AVAILABLE_BYTES))
+MEM_TOTAL_BYTES=$(($(grep </proc/meminfo MemTotal | awk '{print $2}') * 1024))
+MEM_AVAILABLE_BYTES=$(($(grep </proc/meminfo MemAvailable | awk '{print $2}') * 1024))
+MEM_USED_BYTES=$((MEM_TOTAL_BYTES - MEM_AVAILABLE_BYTES))
 
 
 # Create temporary json file
 # Create temporary json file
 TEMP_JSON_FILE=$(mktemp)
 TEMP_JSON_FILE=$(mktemp)
 echo '{ "cpu": { "load": '"${CPU_LOAD_PERCENTAGE}"' }, "memory": { "total": '"${MEM_TOTAL_BYTES}"' , "used": '"${MEM_USED_BYTES}"', "available": '"${MEM_AVAILABLE_BYTES}"' }, "disk": { "total": '"${TOTAL_DISK_SPACE_BYTES}"' , "used": '"${USED_DISK_SPACE_BYTES}"', "available": '"${AVAILABLE_DISK_SPACE_BYTES}"' } }' >"${TEMP_JSON_FILE}"
 echo '{ "cpu": { "load": '"${CPU_LOAD_PERCENTAGE}"' }, "memory": { "total": '"${MEM_TOTAL_BYTES}"' , "used": '"${MEM_USED_BYTES}"', "available": '"${MEM_AVAILABLE_BYTES}"' }, "disk": { "total": '"${TOTAL_DISK_SPACE_BYTES}"' , "used": '"${USED_DISK_SPACE_BYTES}"', "available": '"${AVAILABLE_DISK_SPACE_BYTES}"' } }' >"${TEMP_JSON_FILE}"
 
 
 # Write to state file
 # Write to state file
-echo "$(cat "${TEMP_JSON_FILE}")" >"${STATE_FOLDER}/system-info.json"
+cat "${TEMP_JSON_FILE}" >"${STATE_FOLDER}/system-info.json"

+ 35 - 0
scripts/utils.sh

@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+
+cd /runtipi || echo ""
+
+# Ensure PWD ends with /runtipi
+if [[ "${PWD##*/}" != "runtipi" ]]; then
+    echo "Please run this script from the runtipi directory"
+    exit 1
+fi
+
+if [ -z ${1+x} ]; then
+    command=""
+else
+    command="$1"
+fi
+
+# Restart Tipi
+if [[ "$command" = "restart" ]]; then
+    echo "Restarting Tipi..."
+
+    scripts/stop.sh
+    scripts/start.sh
+
+    exit
+fi
+
+# Update Tipi
+if [[ "$command" = "update" ]]; then
+
+    scripts/stop.sh
+    git pull origin master
+    scripts/start.sh
+
+    exit
+fi