apps.helpers.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import portUsed from 'tcp-port-used';
  2. import { fileExists, getSeed, readdirSync, readFile, readJsonFile, runScript, writeFile } from '../fs/fs.helpers';
  3. import InternalIp from 'internal-ip';
  4. import crypto from 'crypto';
  5. import { AppInfo, AppStatusEnum } from './apps.types';
  6. import logger from '../../config/logger/logger';
  7. import App from './app.entity';
  8. import { getConfig } from '../../core/config/TipiConfig';
  9. import fs from 'fs-extra';
  10. export const checkAppRequirements = async (appName: string) => {
  11. let valid = true;
  12. const configFile: AppInfo | null = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appName}/config.json`);
  13. if (!configFile) {
  14. throw new Error(`App ${appName} not found`);
  15. }
  16. if (configFile?.requirements?.ports) {
  17. for (const port of configFile.requirements.ports) {
  18. const ip = await InternalIp.v4();
  19. const used = await portUsed.check(port, ip);
  20. if (used) valid = false;
  21. }
  22. }
  23. return valid;
  24. };
  25. export const getEnvMap = (appName: string): Map<string, string> => {
  26. const envFile = readFile(`/app/storage/app-data/${appName}/app.env`).toString();
  27. const envVars = envFile.split('\n');
  28. const envVarsMap = new Map<string, string>();
  29. envVars.forEach((envVar) => {
  30. const [key, value] = envVar.split('=');
  31. envVarsMap.set(key, value);
  32. });
  33. return envVarsMap;
  34. };
  35. export const checkEnvFile = (appName: string) => {
  36. const configFile: AppInfo | null = readJsonFile(`/app/storage/apps/${appName}/config.json`);
  37. const envMap = getEnvMap(appName);
  38. configFile?.form_fields?.forEach((field) => {
  39. const envVar = field.env_variable;
  40. const envVarValue = envMap.get(envVar);
  41. if (!envVarValue && field.required) {
  42. throw new Error('New info needed. App config needs to be updated');
  43. }
  44. });
  45. };
  46. export const runAppScript = async (params: string[]): Promise<void> => {
  47. return new Promise((resolve, reject) => {
  48. runScript('/runtipi/scripts/app.sh', [...params], (err: string) => {
  49. if (err) {
  50. logger.error(`Error running app script: ${err}`);
  51. reject(err);
  52. }
  53. resolve();
  54. });
  55. });
  56. };
  57. const getEntropy = (name: string, length: number) => {
  58. const hash = crypto.createHash('sha256');
  59. hash.update(name + getSeed());
  60. return hash.digest('hex').substring(0, length);
  61. };
  62. export const generateEnvFile = (app: App) => {
  63. const configFile: AppInfo | null = readJsonFile(`/app/storage/apps/${app.id}/config.json`);
  64. if (!configFile) {
  65. throw new Error(`App ${app.id} not found`);
  66. }
  67. const baseEnvFile = readFile('/runtipi/.env').toString();
  68. let envFile = `${baseEnvFile}\nAPP_PORT=${configFile.port}\n`;
  69. const envMap = getEnvMap(app.id);
  70. configFile.form_fields?.forEach((field) => {
  71. const formValue = app.config[field.env_variable];
  72. const envVar = field.env_variable;
  73. if (formValue) {
  74. envFile += `${envVar}=${formValue}\n`;
  75. } else if (field.type === 'random') {
  76. if (envMap.has(envVar)) {
  77. envFile += `${envVar}=${envMap.get(envVar)}\n`;
  78. } else {
  79. const length = field.min || 32;
  80. const randomString = getEntropy(field.env_variable, length);
  81. envFile += `${envVar}=${randomString}\n`;
  82. }
  83. } else if (field.required) {
  84. throw new Error(`Variable ${field.env_variable} is required`);
  85. }
  86. });
  87. if (app.exposed && app.domain) {
  88. envFile += 'APP_EXPOSED=true\n';
  89. envFile += `APP_DOMAIN=${app.domain}\n`;
  90. envFile += 'APP_PROTOCOL=https\n';
  91. } else {
  92. envFile += `APP_DOMAIN=${getConfig().internalIp}:${configFile.port}\n`;
  93. }
  94. // Create app-data folder if it doesn't exist
  95. if (!fs.existsSync(`/app/storage/app-data/${app.id}`)) {
  96. fs.mkdirSync(`/app/storage/app-data/${app.id}`, { recursive: true });
  97. }
  98. writeFile(`/app/storage/app-data/${app.id}/app.env`, envFile);
  99. };
  100. export const getAvailableApps = async (): Promise<string[]> => {
  101. const apps: string[] = [];
  102. const appsDir = readdirSync(`/runtipi/repos/${getConfig().appsRepoId}/apps`);
  103. appsDir.forEach((app) => {
  104. if (fileExists(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`)) {
  105. const configFile: AppInfo = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`);
  106. if (configFile.available) {
  107. apps.push(app);
  108. }
  109. }
  110. });
  111. return apps;
  112. };
  113. export const getAppInfo = (id: string, status?: AppStatusEnum): AppInfo | null => {
  114. try {
  115. // Check if app is installed
  116. const installed = typeof status !== 'undefined' && status !== AppStatusEnum.MISSING;
  117. if (installed && fileExists(`/app/storage/apps/${id}/config.json`)) {
  118. const configFile: AppInfo = readJsonFile(`/app/storage/apps/${id}/config.json`);
  119. configFile.description = readFile(`/app/storage/apps/${id}/metadata/description.md`).toString();
  120. return configFile;
  121. } else if (fileExists(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`)) {
  122. const configFile: AppInfo = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`);
  123. configFile.description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/metadata/description.md`);
  124. if (configFile.available) {
  125. return configFile;
  126. }
  127. }
  128. return null;
  129. } catch (e) {
  130. logger.error(`Error loading app: ${id}`);
  131. throw new Error(`Error loading app: ${id}`);
  132. }
  133. };
  134. export const getUpdateInfo = async (id: string) => {
  135. const app = await App.findOne({ where: { id } });
  136. const doesFileExist = fileExists(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}`);
  137. if (!app || !doesFileExist) {
  138. return null;
  139. }
  140. const repoConfig: AppInfo = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`);
  141. return {
  142. current: app.version,
  143. latest: repoConfig.tipi_version,
  144. dockerVersion: repoConfig.version,
  145. };
  146. };