apps.helpers.ts 5.6 KB

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