feat: customize applications storage-path
This commit is contained in:
parent
164635d33b
commit
bb5a50e143
27 changed files with 288 additions and 280 deletions
|
@ -4,7 +4,7 @@ WORKDIR /
|
|||
|
||||
RUN apt-get update
|
||||
# Install docker
|
||||
RUN apt-get install -y ca-certificates curl gnupg lsb-release
|
||||
RUN apt-get install -y ca-certificates curl gnupg lsb-release jq
|
||||
RUN mkdir -p /etc/apt/keyrings
|
||||
RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
RUN echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list >/dev/null
|
||||
|
|
|
@ -55,7 +55,8 @@ services:
|
|||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ${PWD}:/runtipi
|
||||
- ${PWD}/packages/system-api/src:/api/src
|
||||
- ${PWD}/logs:/api/logs
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${STORAGE_PATH}:/app/storage
|
||||
# - /api/node_modules
|
||||
environment:
|
||||
INTERNAL_IP: ${INTERNAL_IP}
|
||||
|
|
|
@ -47,7 +47,8 @@ services:
|
|||
## Docker sock
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ${PWD}:/runtipi
|
||||
- ${PWD}/logs:/api/logs
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${STORAGE_PATH}:/app/storage
|
||||
environment:
|
||||
INTERNAL_IP: ${INTERNAL_IP}
|
||||
TIPI_VERSION: ${TIPI_VERSION}
|
||||
|
|
|
@ -47,7 +47,8 @@ services:
|
|||
## Docker sock
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ${PWD}:/runtipi
|
||||
- ${PWD}/logs:/api/logs
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${STORAGE_PATH}:/app/storage
|
||||
environment:
|
||||
INTERNAL_IP: ${INTERNAL_IP}
|
||||
TIPI_VERSION: ${TIPI_VERSION}
|
||||
|
|
1
packages/system-api/.gitignore
vendored
1
packages/system-api/.gitignore
vendored
|
@ -5,3 +5,4 @@ dist/
|
|||
coverage/
|
||||
logs/
|
||||
sessions/
|
||||
.vscode
|
||||
|
|
|
@ -20,6 +20,7 @@ const {
|
|||
APPS_REPO_ID = '',
|
||||
APPS_REPO_URL = '',
|
||||
DOMAIN = '',
|
||||
STORAGE_PATH = '/runtipi',
|
||||
} = process.env;
|
||||
|
||||
const configSchema = z.object({
|
||||
|
@ -39,6 +40,7 @@ const configSchema = z.object({
|
|||
appsRepoId: z.string(),
|
||||
appsRepoUrl: z.string(),
|
||||
domain: z.string(),
|
||||
storagePath: z.string(),
|
||||
});
|
||||
|
||||
class Config {
|
||||
|
@ -64,6 +66,7 @@ class Config {
|
|||
domain: DOMAIN,
|
||||
dnsIp: '9.9.9.9',
|
||||
status: 'RUNNING',
|
||||
storagePath: STORAGE_PATH,
|
||||
};
|
||||
|
||||
const parsed = configSchema.parse({
|
||||
|
@ -85,7 +88,7 @@ class Config {
|
|||
}
|
||||
|
||||
public applyJsonConfig() {
|
||||
const fileConfig = readJsonFile('/state/settings.json') || {};
|
||||
const fileConfig = readJsonFile('/runtipi/state/settings.json') || {};
|
||||
|
||||
const parsed = configSchema.parse({
|
||||
...this.config,
|
||||
|
@ -102,12 +105,12 @@ class Config {
|
|||
this.config = configSchema.parse(newConf);
|
||||
|
||||
if (writeFile) {
|
||||
const currentJsonConf = readJsonFile('/state/settings.json') || {};
|
||||
const currentJsonConf = readJsonFile('/runtipi/state/settings.json') || {};
|
||||
currentJsonConf[key] = value;
|
||||
const partialConfig = configSchema.partial();
|
||||
const parsed = partialConfig.parse(currentJsonConf);
|
||||
|
||||
fs.writeFileSync(`${this.config.rootFolder}/state/settings.json`, JSON.stringify(parsed));
|
||||
fs.writeFileSync('/runtipi/state/settings.json', JSON.stringify(parsed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ describe('Test: setConfig', () => {
|
|||
expect(config).toBeDefined();
|
||||
expect(config.appsRepoUrl).toBe(randomWord);
|
||||
|
||||
const settingsJson = readJsonFile('/state/settings.json');
|
||||
const settingsJson = readJsonFile('/runtipi/state/settings.json');
|
||||
|
||||
expect(settingsJson).toBeDefined();
|
||||
expect(settingsJson.appsRepoUrl).toBe(randomWord);
|
||||
|
|
|
@ -80,7 +80,7 @@ describe('State/apps.json exists with no installed app', () => {
|
|||
|
||||
it('Should delete state file after update', async () => {
|
||||
await updateV040();
|
||||
expect(fs.existsSync(`${getConfig().rootFolder}/state/apps.json`)).toBe(false);
|
||||
expect(fs.existsSync('/runtipi/state/apps.json')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -89,9 +89,9 @@ describe('State/apps.json exists with one installed app', () => {
|
|||
beforeEach(async () => {
|
||||
const { MockFiles, appInfo } = await createApp({});
|
||||
app1 = appInfo;
|
||||
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';
|
||||
MockFiles['/runtipi/state/apps.json'] = createState([appInfo.id]);
|
||||
MockFiles[`/app/storage/app-data/${appInfo.id}`] = '';
|
||||
MockFiles[`/app/storage/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MockFiles);
|
||||
});
|
||||
|
@ -118,9 +118,9 @@ describe('State/apps.json exists with one installed app', () => {
|
|||
it('Should not try to migrate app if it already exists', async () => {
|
||||
const { MockFiles, appInfo } = await createApp({ installed: true });
|
||||
app1 = appInfo;
|
||||
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';
|
||||
MockFiles['/runtipi/state/apps.json'] = createState([appInfo.id]);
|
||||
MockFiles[`/app/storage/app-data/${appInfo.id}`] = '';
|
||||
MockFiles[`/app/storage/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MockFiles);
|
||||
|
||||
|
|
|
@ -20,15 +20,15 @@ export const updateV040 = async (): Promise<void> => {
|
|||
}
|
||||
|
||||
// Migrate apps
|
||||
if (fileExists('/state/apps.json')) {
|
||||
const state: AppsState = await readJsonFile('/state/apps.json');
|
||||
if (fileExists('/runtipi/state/apps.json')) {
|
||||
const state: AppsState = await readJsonFile('/runtipi/state/apps.json');
|
||||
const installed: string[] = state.installed.split(' ').filter(Boolean);
|
||||
|
||||
for (const appId of installed) {
|
||||
const app = await App.findOne({ where: { id: appId } });
|
||||
|
||||
if (!app) {
|
||||
const envFile = readFile(`/app-data/${appId}/app.env`).toString();
|
||||
const envFile = readFile(`/app/storage/app-data/${appId}/app.env`).toString();
|
||||
const envVars = envFile.split('\n');
|
||||
const envVarsMap = new Map<string, string>();
|
||||
|
||||
|
@ -39,7 +39,7 @@ export const updateV040 = async (): Promise<void> => {
|
|||
|
||||
const form: Record<string, string> = {};
|
||||
|
||||
const configFile: AppInfo | null = readJsonFile(`/repos/${getConfig().appsRepoId}/apps/${appId}/config.json`);
|
||||
const configFile: AppInfo | null = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appId}/config.json`);
|
||||
configFile?.form_fields?.forEach((field) => {
|
||||
const envVar = field.env_variable;
|
||||
const envVarValue = envVarsMap.get(envVar);
|
||||
|
@ -54,17 +54,17 @@ export const updateV040 = async (): Promise<void> => {
|
|||
logger.info('App already migrated');
|
||||
}
|
||||
}
|
||||
deleteFolder('/state/apps.json');
|
||||
deleteFolder('/runtipi/state/apps.json');
|
||||
}
|
||||
|
||||
// Migrate users
|
||||
if (fileExists('/state/users.json')) {
|
||||
const state: { email: string; password: string }[] = await readJsonFile('/state/users.json');
|
||||
const state: { email: string; password: string }[] = await readJsonFile('/runtipi/state/users.json');
|
||||
|
||||
for (const user of state) {
|
||||
await User.create({ username: user.email.trim().toLowerCase(), password: user.password }).save();
|
||||
}
|
||||
deleteFolder('/state/users.json');
|
||||
deleteFolder('/runtipi/state/users.json');
|
||||
}
|
||||
|
||||
await Update.create({ name: UPDATE_NAME, status: UpdateStatusEnum.SUCCESS }).save();
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { faker } from '@faker-js/faker';
|
||||
import childProcess from 'child_process';
|
||||
import logger from '../../config/logger/logger';
|
||||
import { getConfig } from '../../core/config/TipiConfig';
|
||||
import { cloneRepo, updateRepo } from '../repo-helpers';
|
||||
|
||||
jest.mock('child_process');
|
||||
|
@ -26,7 +25,7 @@ describe('Test: updateRepo', () => {
|
|||
|
||||
await updateRepo(url);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(`${getConfig().rootFolder}/scripts/git.sh`, ['update', url], {}, expect.any(Function));
|
||||
expect(spy).toHaveBeenCalledWith('/runtipi/scripts/git.sh', ['update', url], {}, expect.any(Function));
|
||||
expect(log).toHaveBeenCalledWith(`Update result: ${stdout}`);
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
@ -70,7 +69,7 @@ describe('Test: cloneRepo', () => {
|
|||
|
||||
await cloneRepo(url);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(`${getConfig().rootFolder}/scripts/git.sh`, ['clone', url], {}, expect.any(Function));
|
||||
expect(spy).toHaveBeenCalledWith('/runtipi/scripts/git.sh', ['clone', url], {}, expect.any(Function));
|
||||
expect(log).toHaveBeenCalledWith(`Clone result ${stdout}`);
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@ import { runScript } from '../modules/fs/fs.helpers';
|
|||
|
||||
export const updateRepo = (repo: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
runScript('/scripts/git.sh', ['update', repo], (err: string, stdout: string) => {
|
||||
runScript('/runtipi/scripts/git.sh', ['update', repo], (err: string, stdout: string) => {
|
||||
if (err) {
|
||||
Logger.error(`Error updating repo: ${err}`);
|
||||
reject(err);
|
||||
|
@ -18,7 +18,7 @@ export const updateRepo = (repo: string): Promise<void> => {
|
|||
|
||||
export const cloneRepo = (repo: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
runScript('/scripts/git.sh', ['clone', repo], (err: string, stdout: string) => {
|
||||
runScript('/runtipi/scripts/git.sh', ['clone', repo], (err: string, stdout: string) => {
|
||||
if (err) {
|
||||
Logger.error(`Error cloning repo: ${err}`);
|
||||
reject(err);
|
||||
|
|
|
@ -55,11 +55,11 @@ const createApp = async (props: IProps) => {
|
|||
}
|
||||
|
||||
let MockFiles: any = {};
|
||||
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';
|
||||
MockFiles['/runtipi/.env'] = 'TEST=test';
|
||||
MockFiles['/runtipi/repos/repo-id'] = '';
|
||||
MockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
|
||||
MockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
|
||||
MockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
|
||||
|
||||
let appEntity = new App();
|
||||
if (installed) {
|
||||
|
@ -71,10 +71,10 @@ const createApp = async (props: IProps) => {
|
|||
domain,
|
||||
}).save();
|
||||
|
||||
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';
|
||||
MockFiles[`/app/storage/app-data/${appInfo.id}`] = '';
|
||||
MockFiles[`/app/storage/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
|
||||
MockFiles[`/app/storage/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
|
||||
MockFiles[`/app/storage/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
|
||||
}
|
||||
|
||||
return { appInfo, MockFiles, appEntity };
|
||||
|
|
|
@ -95,7 +95,7 @@ describe('checkEnvFile', () => {
|
|||
|
||||
it('Should throw if a required field is missing', () => {
|
||||
const newAppEnv = 'APP_PORT=test\n';
|
||||
fs.writeFileSync(`${getConfig().rootFolder}/app-data/${app1.id}/app.env`, newAppEnv);
|
||||
fs.writeFileSync(`/app/storage/app-data/${app1.id}/app.env`, newAppEnv);
|
||||
|
||||
try {
|
||||
checkEnvFile(app1.id);
|
||||
|
@ -167,7 +167,7 @@ describe('generateEnvFile', () => {
|
|||
|
||||
const randomField = faker.random.alphaNumeric(32);
|
||||
|
||||
fs.writeFileSync(`${getConfig().rootFolder}/app-data/${appInfo.id}/app.env`, `RANDOM_FIELD=${randomField}`);
|
||||
fs.writeFileSync(`/app/storage/app-data/${appInfo.id}/app.env`, `RANDOM_FIELD=${randomField}`);
|
||||
|
||||
generateEnvFile(appEntity);
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ describe('Install app', () => {
|
|||
|
||||
it('Should correctly generate env file for app', async () => {
|
||||
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
|
||||
const envFile = fs.readFileSync(`${getConfig().rootFolder}/app-data/${app1.id}/app.env`).toString();
|
||||
const envFile = fs.readFileSync(`/app/storage/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}`);
|
||||
});
|
||||
|
@ -74,8 +74,8 @@ describe('Install app', () => {
|
|||
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
|
||||
|
||||
expect(spy.mock.calls.length).toBe(2);
|
||||
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)]);
|
||||
expect(spy.mock.calls[0]).toEqual(['/runtipi/scripts/app.sh', ['install', app1.id], {}, expect.any(Function)]);
|
||||
expect(spy.mock.calls[1]).toEqual(['/runtipi/scripts/app.sh', ['start', app1.id], {}, expect.any(Function)]);
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
@ -112,7 +112,7 @@ describe('Install app', () => {
|
|||
|
||||
it('Should correctly copy app from repos to apps folder', async () => {
|
||||
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
|
||||
const appFolder = fs.readdirSync(`${getConfig().rootFolder}/apps/${app1.id}`);
|
||||
const appFolder = fs.readdirSync(`/app/storage/apps/${app1.id}`);
|
||||
|
||||
expect(appFolder).toBeDefined();
|
||||
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 () => {
|
||||
const { MockFiles, appInfo } = await createApp({});
|
||||
app1 = appInfo;
|
||||
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'];
|
||||
MockFiles[`/app/storage/apps/${appInfo.id}/docker-compose.yml`] = 'test';
|
||||
MockFiles[`/app/storage/apps/${appInfo.id}/test.yml`] = 'test';
|
||||
MockFiles[`/app/storage/apps/${appInfo.id}`] = ['test.yml', 'docker-compose.yml'];
|
||||
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MockFiles);
|
||||
|
||||
expect(fs.existsSync(`${getConfig().rootFolder}/apps/${app1.id}/test.yml`)).toBe(true);
|
||||
expect(fs.existsSync(`/app/storage/apps/${app1.id}/test.yml`)).toBe(true);
|
||||
|
||||
await AppsService.installApp(app1.id, { TEST_FIELD: 'test' });
|
||||
|
||||
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);
|
||||
expect(fs.existsSync(`/app/storage/apps/${app1.id}/test.yml`)).toBe(false);
|
||||
expect(fs.existsSync(`/app/storage/apps/${app1.id}/docker-compose.yml`)).toBe(true);
|
||||
});
|
||||
|
||||
it('Should throw if app is exposed and domain is not provided', async () => {
|
||||
|
@ -266,11 +266,11 @@ describe('Start app', () => {
|
|||
});
|
||||
|
||||
it('Regenerate env file', async () => {
|
||||
fs.writeFile(`${getConfig().rootFolder}/app-data/${app1.id}/app.env`, 'TEST=test\nAPP_PORT=3000', () => {});
|
||||
fs.writeFile(`/app/storage/app-data/${app1.id}/app.env`, 'TEST=test\nAPP_PORT=3000', () => {});
|
||||
|
||||
await AppsService.startApp(app1.id);
|
||||
|
||||
const envFile = fs.readFileSync(`${getConfig().rootFolder}/app-data/${app1.id}/app.env`).toString();
|
||||
const envFile = fs.readFileSync(`/app/storage/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}`);
|
||||
});
|
||||
|
@ -334,7 +334,7 @@ describe('Update app config', () => {
|
|||
it('Should correctly update app config', async () => {
|
||||
await AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' });
|
||||
|
||||
const envFile = fs.readFileSync(`${getConfig().rootFolder}/app-data/${app1.id}/app.env`).toString();
|
||||
const envFile = fs.readFileSync(`/app/storage/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}`);
|
||||
});
|
||||
|
@ -352,8 +352,8 @@ describe('Update app config', () => {
|
|||
// @ts-ignore
|
||||
fs.__createMockFiles(MockFiles);
|
||||
|
||||
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`);
|
||||
const envFile = fs.readFileSync(`/app/storage/app-data/${appInfo.id}/app.env`).toString();
|
||||
fs.writeFileSync(`/app/storage/app-data/${appInfo.id}/app.env`, `${envFile}\nRANDOM_FIELD=test`);
|
||||
|
||||
await AppsService.updateAppConfig(appInfo.id, { TEST_FIELD: 'test' });
|
||||
|
||||
|
|
|
@ -6,13 +6,12 @@ import { AppInfo, AppStatusEnum } from './apps.types';
|
|||
import logger from '../../config/logger/logger';
|
||||
import App from './app.entity';
|
||||
import { getConfig } from '../../core/config/TipiConfig';
|
||||
|
||||
const { appsRepoId, internalIp } = getConfig();
|
||||
import fs from 'fs-extra';
|
||||
|
||||
export const checkAppRequirements = async (appName: string) => {
|
||||
let valid = true;
|
||||
|
||||
const configFile: AppInfo | null = readJsonFile(`/repos/${appsRepoId}/apps/${appName}/config.json`);
|
||||
const configFile: AppInfo | null = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appName}/config.json`);
|
||||
|
||||
if (!configFile) {
|
||||
throw new Error(`App ${appName} not found`);
|
||||
|
@ -31,7 +30,7 @@ export const checkAppRequirements = async (appName: string) => {
|
|||
};
|
||||
|
||||
export const getEnvMap = (appName: string): Map<string, string> => {
|
||||
const envFile = readFile(`/app-data/${appName}/app.env`).toString();
|
||||
const envFile = readFile(`/app/storage/app-data/${appName}/app.env`).toString();
|
||||
const envVars = envFile.split('\n');
|
||||
const envVarsMap = new Map<string, string>();
|
||||
|
||||
|
@ -44,7 +43,7 @@ export const getEnvMap = (appName: string): Map<string, string> => {
|
|||
};
|
||||
|
||||
export const checkEnvFile = (appName: string) => {
|
||||
const configFile: AppInfo | null = readJsonFile(`/apps/${appName}/config.json`);
|
||||
const configFile: AppInfo | null = readJsonFile(`/app/storage/apps/${appName}/config.json`);
|
||||
const envMap = getEnvMap(appName);
|
||||
|
||||
configFile?.form_fields?.forEach((field) => {
|
||||
|
@ -59,7 +58,7 @@ export const checkEnvFile = (appName: string) => {
|
|||
|
||||
export const runAppScript = async (params: string[]): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
runScript('/scripts/app.sh', [...params], (err: string) => {
|
||||
runScript('/runtipi/scripts/app.sh', [...params], (err: string) => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
reject(err);
|
||||
|
@ -77,13 +76,13 @@ const getEntropy = (name: string, length: number) => {
|
|||
};
|
||||
|
||||
export const generateEnvFile = (app: App) => {
|
||||
const configFile: AppInfo | null = readJsonFile(`/apps/${app.id}/config.json`);
|
||||
const configFile: AppInfo | null = readJsonFile(`/app/storage/apps/${app.id}/config.json`);
|
||||
|
||||
if (!configFile) {
|
||||
throw new Error(`App ${app.id} not found`);
|
||||
}
|
||||
|
||||
const baseEnvFile = readFile('/.env').toString();
|
||||
const baseEnvFile = readFile('/runtipi/.env').toString();
|
||||
let envFile = `${baseEnvFile}\nAPP_PORT=${configFile.port}\n`;
|
||||
const envMap = getEnvMap(app.id);
|
||||
|
||||
|
@ -112,20 +111,25 @@ export const generateEnvFile = (app: App) => {
|
|||
envFile += `APP_DOMAIN=${app.domain}\n`;
|
||||
envFile += 'APP_PROTOCOL=https\n';
|
||||
} else {
|
||||
envFile += `APP_DOMAIN=${internalIp}:${configFile.port}\n`;
|
||||
envFile += `APP_DOMAIN=${getConfig().internalIp}:${configFile.port}\n`;
|
||||
}
|
||||
|
||||
writeFile(`/app-data/${app.id}/app.env`, envFile);
|
||||
// Create app-data folder if it doesn't exist
|
||||
if (!fs.existsSync(`/app/storage/app-data/${app.id}`)) {
|
||||
fs.mkdirSync(`/app/storage/app-data/${app.id}`, { recursive: true });
|
||||
}
|
||||
|
||||
writeFile(`/app/storage/app-data/${app.id}/app.env`, envFile);
|
||||
};
|
||||
|
||||
export const getAvailableApps = async (): Promise<string[]> => {
|
||||
const apps: string[] = [];
|
||||
|
||||
const appsDir = readdirSync(`/repos/${appsRepoId}/apps`);
|
||||
const appsDir = readdirSync(`/runtipi/repos/${getConfig().appsRepoId}/apps`);
|
||||
|
||||
appsDir.forEach((app) => {
|
||||
if (fileExists(`/repos/${appsRepoId}/apps/${app}/config.json`)) {
|
||||
const configFile: AppInfo = readJsonFile(`/repos/${appsRepoId}/apps/${app}/config.json`);
|
||||
if (fileExists(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`)) {
|
||||
const configFile: AppInfo = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`);
|
||||
|
||||
if (configFile.available) {
|
||||
apps.push(app);
|
||||
|
@ -141,13 +145,13 @@ export const getAppInfo = (id: string, status?: AppStatusEnum): AppInfo | null =
|
|||
// Check if app is installed
|
||||
const installed = typeof status !== 'undefined' && status !== AppStatusEnum.MISSING;
|
||||
|
||||
if (installed && fileExists(`/apps/${id}/config.json`)) {
|
||||
const configFile: AppInfo = readJsonFile(`/apps/${id}/config.json`);
|
||||
configFile.description = readFile(`/apps/${id}/metadata/description.md`).toString();
|
||||
if (installed && fileExists(`/app/storage/apps/${id}/config.json`)) {
|
||||
const configFile: AppInfo = readJsonFile(`/app/storage/apps/${id}/config.json`);
|
||||
configFile.description = readFile(`/app/storage/apps/${id}/metadata/description.md`).toString();
|
||||
return configFile;
|
||||
} 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`);
|
||||
} else if (fileExists(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`)) {
|
||||
const configFile: AppInfo = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`);
|
||||
configFile.description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/metadata/description.md`);
|
||||
|
||||
if (configFile.available) {
|
||||
return configFile;
|
||||
|
@ -164,13 +168,13 @@ export const getAppInfo = (id: string, status?: AppStatusEnum): AppInfo | null =
|
|||
export const getUpdateInfo = async (id: string) => {
|
||||
const app = await App.findOne({ where: { id } });
|
||||
|
||||
const doesFileExist = fileExists(`/repos/${appsRepoId}/apps/${id}`);
|
||||
const doesFileExist = fileExists(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}`);
|
||||
|
||||
if (!app || !doesFileExist) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const repoConfig: AppInfo = readJsonFile(`/repos/${appsRepoId}/apps/${id}/config.json`);
|
||||
const repoConfig: AppInfo = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`);
|
||||
|
||||
return {
|
||||
current: app.version,
|
||||
|
|
|
@ -83,9 +83,9 @@ const installApp = async (id: string, form: Record<string, string>, exposed?: bo
|
|||
}
|
||||
|
||||
// Create app folder
|
||||
createFolder(`/app-data/${id}`);
|
||||
createFolder(`/app/storage/app-data/${id}`);
|
||||
|
||||
const appInfo: AppInfo | null = await readJsonFile(`/apps/${id}/config.json`);
|
||||
const appInfo: AppInfo | null = await readJsonFile(`/app/storage/apps/${id}/config.json`);
|
||||
|
||||
if (!appInfo?.exposable && exposed) {
|
||||
throw new Error(`App ${id} is not exposable`);
|
||||
|
@ -124,7 +124,7 @@ const listApps = async (): Promise<ListAppsResonse> => {
|
|||
const apps: AppInfo[] = folders
|
||||
.map((app) => {
|
||||
try {
|
||||
return readJsonFile(`/repos/${getConfig().appsRepoId}/apps/${app}/config.json`);
|
||||
return readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ const listApps = async (): Promise<ListAppsResonse> => {
|
|||
.filter(Boolean);
|
||||
|
||||
apps.forEach((app) => {
|
||||
app.description = readFile(`/repos/${getConfig().appsRepoId}/apps/${app.id}/metadata/description.md`);
|
||||
app.description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app.id}/metadata/description.md`);
|
||||
});
|
||||
|
||||
return { apps: apps.sort(sortApps), total: apps.length };
|
||||
|
@ -147,7 +147,7 @@ const updateAppConfig = async (id: string, form: Record<string, string>, exposed
|
|||
throw new Error(`Domain ${domain} is not valid`);
|
||||
}
|
||||
|
||||
const appInfo: AppInfo | null = await readJsonFile(`/apps/${id}/config.json`);
|
||||
const appInfo: AppInfo | null = await readJsonFile(`/app/storage/apps/${id}/config.json`);
|
||||
|
||||
if (!appInfo?.exposable && exposed) {
|
||||
throw new Error(`App ${id} is not exposable`);
|
||||
|
@ -250,7 +250,7 @@ const updateApp = async (id: string) => {
|
|||
// Run script
|
||||
try {
|
||||
await runAppScript(['update', id]);
|
||||
const appInfo: AppInfo | null = await readJsonFile(`/apps/${id}/config.json`);
|
||||
const appInfo: AppInfo | null = await readJsonFile(`/app/storage/apps/${id}/config.json`);
|
||||
await App.update({ id }, { status: AppStatusEnum.RUNNING, version: Number(appInfo?.tipi_version) });
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import childProcess from 'child_process';
|
||||
import { getAbsolutePath, readJsonFile, readFile, readdirSync, fileExists, writeFile, createFolder, deleteFolder, runScript, getSeed, ensureAppFolder } from '../fs.helpers';
|
||||
import { readJsonFile, readFile, readdirSync, fileExists, writeFile, createFolder, deleteFolder, runScript, getSeed, ensureAppFolder } from '../fs.helpers';
|
||||
import fs from 'fs-extra';
|
||||
import { getConfig } from '../../../core/config/TipiConfig';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
@ -11,24 +11,18 @@ beforeEach(() => {
|
|||
fs.__resetAllMocks();
|
||||
});
|
||||
|
||||
describe('Test: getAbsolutePath', () => {
|
||||
it('should return the absolute path', () => {
|
||||
expect(getAbsolutePath('/test')).toBe(`${getConfig().rootFolder}/test`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test: readJsonFile', () => {
|
||||
it('should return the json file', () => {
|
||||
// Arrange
|
||||
const rawFile = '{"test": "test"}';
|
||||
const mockFiles = {
|
||||
[`${getConfig().rootFolder}/test-file.json`]: rawFile,
|
||||
['/runtipi/test-file.json']: rawFile,
|
||||
};
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(mockFiles);
|
||||
|
||||
// Act
|
||||
const file = readJsonFile('/test-file.json');
|
||||
const file = readJsonFile('/runtipi/test-file.json');
|
||||
|
||||
// Assert
|
||||
expect(file).toEqual({ test: 'test' });
|
||||
|
@ -59,13 +53,13 @@ describe('Test: readFile', () => {
|
|||
it('should return the file', () => {
|
||||
const rawFile = 'test';
|
||||
const mockFiles = {
|
||||
[`${getConfig().rootFolder}/test-file.txt`]: rawFile,
|
||||
['/runtipi/test-file.txt']: rawFile,
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(mockFiles);
|
||||
|
||||
expect(readFile('/test-file.txt')).toEqual('test');
|
||||
expect(readFile('/runtipi/test-file.txt')).toEqual('test');
|
||||
});
|
||||
|
||||
it('should return empty string if the file does not exist', () => {
|
||||
|
@ -76,13 +70,13 @@ describe('Test: readFile', () => {
|
|||
describe('Test: readdirSync', () => {
|
||||
it('should return the files', () => {
|
||||
const mockFiles = {
|
||||
[`${getConfig().rootFolder}/test/test-file.txt`]: 'test',
|
||||
['/runtipi/test/test-file.txt']: 'test',
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(mockFiles);
|
||||
|
||||
expect(readdirSync('/test')).toEqual(['test-file.txt']);
|
||||
expect(readdirSync('/runtipi/test')).toEqual(['test-file.txt']);
|
||||
});
|
||||
|
||||
it('should return empty array if the directory does not exist', () => {
|
||||
|
@ -93,13 +87,13 @@ describe('Test: readdirSync', () => {
|
|||
describe('Test: fileExists', () => {
|
||||
it('should return true if the file exists', () => {
|
||||
const mockFiles = {
|
||||
[`${getConfig().rootFolder}/test-file.txt`]: 'test',
|
||||
['/runtipi/test-file.txt']: 'test',
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(mockFiles);
|
||||
|
||||
expect(fileExists('/test-file.txt')).toBeTruthy();
|
||||
expect(fileExists('/runtipi/test-file.txt')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return false if the file does not exist', () => {
|
||||
|
@ -111,9 +105,9 @@ describe('Test: writeFile', () => {
|
|||
it('should write the file', () => {
|
||||
const spy = jest.spyOn(fs, 'writeFileSync');
|
||||
|
||||
writeFile('/test-file.txt', 'test');
|
||||
writeFile('/runtipi/test-file.txt', 'test');
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(`${getConfig().rootFolder}/test-file.txt`, 'test');
|
||||
expect(spy).toHaveBeenCalledWith('/runtipi/test-file.txt', 'test');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -123,7 +117,7 @@ describe('Test: createFolder', () => {
|
|||
|
||||
createFolder('/test');
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(`${getConfig().rootFolder}/test`);
|
||||
expect(spy).toHaveBeenCalledWith('/test', { recursive: true });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -133,7 +127,7 @@ describe('Test: deleteFolder', () => {
|
|||
|
||||
deleteFolder('/test');
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(`${getConfig().rootFolder}/test`, { recursive: true });
|
||||
expect(spy).toHaveBeenCalledWith('/test', { recursive: true });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -144,14 +138,14 @@ describe('Test: runScript', () => {
|
|||
|
||||
runScript('/test', [], callback);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(`${getConfig().rootFolder}/test`, [], {}, callback);
|
||||
expect(spy).toHaveBeenCalledWith('/test', [], {}, callback);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test: getSeed', () => {
|
||||
it('should return the seed', () => {
|
||||
const mockFiles = {
|
||||
[`${getConfig().rootFolder}/state/seed`]: 'test',
|
||||
['/runtipi/state/seed']: 'test',
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
|
@ -164,7 +158,7 @@ describe('Test: getSeed', () => {
|
|||
describe('Test: ensureAppFolder', () => {
|
||||
beforeEach(() => {
|
||||
const mockFiles = {
|
||||
[`${getConfig().rootFolder}/repos/${getConfig().appsRepoId}/apps/test`]: ['test.yml'],
|
||||
[`/runtipi/repos/${getConfig().appsRepoId}/apps/test`]: ['test.yml'],
|
||||
};
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(mockFiles);
|
||||
|
@ -175,15 +169,15 @@ describe('Test: ensureAppFolder', () => {
|
|||
ensureAppFolder('test');
|
||||
|
||||
// Assert
|
||||
const files = fs.readdirSync(`${getConfig().rootFolder}/apps/test`);
|
||||
const files = fs.readdirSync('/app/storage/apps/test');
|
||||
expect(files).toEqual(['test.yml']);
|
||||
});
|
||||
|
||||
it('should not copy the folder if it already exists', () => {
|
||||
const mockFiles = {
|
||||
[`${getConfig().rootFolder}/repos/${getConfig().appsRepoId}/apps/test`]: ['test.yml'],
|
||||
[`${getConfig().rootFolder}/apps/test`]: ['docker-compose.yml'],
|
||||
[`${getConfig().rootFolder}/apps/test/docker-compose.yml`]: 'test',
|
||||
[`/runtipi/repos/${getConfig().appsRepoId}/apps/test`]: ['test.yml'],
|
||||
['/app/storage/apps/test']: ['docker-compose.yml'],
|
||||
['/app/storage/apps/test/docker-compose.yml']: 'test',
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
|
@ -193,15 +187,15 @@ describe('Test: ensureAppFolder', () => {
|
|||
ensureAppFolder('test');
|
||||
|
||||
// Assert
|
||||
const files = fs.readdirSync(`${getConfig().rootFolder}/apps/test`);
|
||||
const files = fs.readdirSync('/app/storage/apps/test');
|
||||
expect(files).toEqual(['docker-compose.yml']);
|
||||
});
|
||||
|
||||
it('Should overwrite the folder if clean up is true', () => {
|
||||
const mockFiles = {
|
||||
[`${getConfig().rootFolder}/repos/${getConfig().appsRepoId}/apps/test`]: ['test.yml'],
|
||||
[`${getConfig().rootFolder}/apps/test`]: ['docker-compose.yml'],
|
||||
[`${getConfig().rootFolder}/apps/test/docker-compose.yml`]: 'test',
|
||||
[`/runtipi/repos/${getConfig().appsRepoId}/apps/test`]: ['test.yml'],
|
||||
['/app/storage/apps/test']: ['docker-compose.yml'],
|
||||
['/app/storage/apps/test/docker-compose.yml']: 'test',
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
|
@ -211,7 +205,7 @@ describe('Test: ensureAppFolder', () => {
|
|||
ensureAppFolder('test', true);
|
||||
|
||||
// Assert
|
||||
const files = fs.readdirSync(`${getConfig().rootFolder}/apps/test`);
|
||||
const files = fs.readdirSync('/app/storage/apps/test');
|
||||
expect(files).toEqual(['test.yml']);
|
||||
});
|
||||
|
||||
|
@ -219,8 +213,8 @@ describe('Test: ensureAppFolder', () => {
|
|||
// Arrange
|
||||
const randomFileName = `${faker.random.word()}.yml`;
|
||||
const mockFiles = {
|
||||
[`${getConfig().rootFolder}/repos/${getConfig().appsRepoId}/apps/test`]: [randomFileName],
|
||||
[`${getConfig().rootFolder}/apps/test`]: ['test.yml'],
|
||||
[`/runtipi/repos/${getConfig().appsRepoId}/apps/test`]: [randomFileName],
|
||||
['/app/storage/apps/test']: ['test.yml'],
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
|
@ -230,7 +224,7 @@ describe('Test: ensureAppFolder', () => {
|
|||
ensureAppFolder('test');
|
||||
|
||||
// Assert
|
||||
const files = fs.readdirSync(`${getConfig().rootFolder}/apps/test`);
|
||||
const files = fs.readdirSync('/app/storage/apps/test');
|
||||
expect(files).toEqual([randomFileName]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,11 +2,9 @@ import fs from 'fs-extra';
|
|||
import childProcess from 'child_process';
|
||||
import { getConfig } from '../../core/config/TipiConfig';
|
||||
|
||||
export const getAbsolutePath = (path: string) => `${getConfig().rootFolder}${path}`;
|
||||
|
||||
export const readJsonFile = (path: string): any => {
|
||||
try {
|
||||
const rawFile = fs.readFileSync(getAbsolutePath(path))?.toString();
|
||||
const rawFile = fs.readFileSync(path)?.toString();
|
||||
|
||||
if (!rawFile) {
|
||||
return null;
|
||||
|
@ -20,40 +18,40 @@ export const readJsonFile = (path: string): any => {
|
|||
|
||||
export const readFile = (path: string): string => {
|
||||
try {
|
||||
return fs.readFileSync(getAbsolutePath(path)).toString();
|
||||
return fs.readFileSync(path).toString();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const readdirSync = (path: string): string[] => fs.readdirSync(getAbsolutePath(path));
|
||||
export const readdirSync = (path: string): string[] => fs.readdirSync(path);
|
||||
|
||||
export const fileExists = (path: string): boolean => fs.existsSync(getAbsolutePath(path));
|
||||
export const fileExists = (path: string): boolean => fs.existsSync(path);
|
||||
|
||||
export const writeFile = (path: string, data: any) => fs.writeFileSync(getAbsolutePath(path), data);
|
||||
export const writeFile = (path: string, data: any) => fs.writeFileSync(path, data);
|
||||
|
||||
export const createFolder = (path: string) => {
|
||||
if (!fileExists(path)) {
|
||||
fs.mkdirSync(getAbsolutePath(path));
|
||||
fs.mkdirSync(path, { recursive: true });
|
||||
}
|
||||
};
|
||||
export const deleteFolder = (path: string) => fs.rmSync(getAbsolutePath(path), { recursive: true });
|
||||
export const deleteFolder = (path: string) => fs.rmSync(path, { recursive: true });
|
||||
|
||||
export const runScript = (path: string, args: string[], callback?: any) => childProcess.execFile(getAbsolutePath(path), args, {}, callback);
|
||||
export const runScript = (path: string, args: string[], callback?: any) => childProcess.execFile(path, args, {}, callback);
|
||||
|
||||
export const getSeed = () => {
|
||||
const seed = readFile('/state/seed');
|
||||
const seed = readFile('/runtipi/state/seed');
|
||||
return seed.toString();
|
||||
};
|
||||
|
||||
export const ensureAppFolder = (appName: string, cleanup = false) => {
|
||||
if (cleanup && fileExists(`/apps/${appName}`)) {
|
||||
deleteFolder(`/apps/${appName}`);
|
||||
if (cleanup && fileExists(`/app/storage/apps/${appName}`)) {
|
||||
deleteFolder(`/app/storage/apps/${appName}`);
|
||||
}
|
||||
|
||||
if (!fileExists(`/apps/${appName}/docker-compose.yml`)) {
|
||||
if (fileExists(`/apps/${appName}`)) deleteFolder(`/apps/${appName}`);
|
||||
if (!fileExists(`/app/storage/apps/${appName}/docker-compose.yml`)) {
|
||||
if (fileExists(`/app/storage/apps/${appName}`)) deleteFolder(`/app/storage/apps/${appName}`);
|
||||
// Copy from apps repo
|
||||
fs.copySync(getAbsolutePath(`/repos/${getConfig().appsRepoId}/apps/${appName}`), getAbsolutePath(`/apps/${appName}`));
|
||||
fs.copySync(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appName}`, `/app/storage/apps/${appName}`);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -23,7 +23,7 @@ const systemInfoSchema = z.object({
|
|||
});
|
||||
|
||||
const systemInfo = (): z.infer<typeof systemInfoSchema> => {
|
||||
const info = systemInfoSchema.safeParse(readJsonFile('/state/system-info.json'));
|
||||
const info = systemInfoSchema.safeParse(readJsonFile('/runtipi/state/system-info.json'));
|
||||
|
||||
if (!info.success) {
|
||||
logger.error('Error parsing system info');
|
||||
|
@ -57,7 +57,7 @@ const getVersion = async (): Promise<{ current: string; latest?: string }> => {
|
|||
const restart = async (): Promise<boolean> => {
|
||||
setConfig('status', 'RESTARTING');
|
||||
|
||||
runScript('/scripts/system.sh', ['restart'], (err: string) => {
|
||||
runScript('/runtipi/scripts/system.sh', ['restart'], (err: string) => {
|
||||
setConfig('status', 'RUNNING');
|
||||
if (err) {
|
||||
logger.error(`Error restarting: ${err}`);
|
||||
|
@ -90,7 +90,7 @@ const update = async (): Promise<boolean> => {
|
|||
|
||||
setConfig('status', 'UPDATING');
|
||||
|
||||
runScript('/scripts/system.sh', ['update'], (err: string) => {
|
||||
runScript('/runtipi/scripts/system.sh', ['update'], (err: string) => {
|
||||
setConfig('status', 'RUNNING');
|
||||
if (err) {
|
||||
logger.error(`Error updating: ${err}`);
|
||||
|
|
|
@ -2,31 +2,32 @@
|
|||
# Required Notice: Copyright
|
||||
# Umbrel (https://umbrel.com)
|
||||
|
||||
echo "Starting app script"
|
||||
|
||||
source "${BASH_SOURCE%/*}/common.sh"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd /runtipi || echo ""
|
||||
# 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
|
||||
ensure_pwd
|
||||
|
||||
# Root folder in container is /runtipi
|
||||
ROOT_FOLDER="${PWD}"
|
||||
|
||||
STATE_FOLDER="${ROOT_FOLDER}/state"
|
||||
ENV_FILE="${ROOT_FOLDER}/.env"
|
||||
|
||||
# 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)
|
||||
STORAGE_PATH=$(grep -v '^#' "${ENV_FILE}" | xargs -n 1 | grep STORAGE_PATH | cut -d '=' -f2)
|
||||
|
||||
# Get field from json file
|
||||
function get_json_field() {
|
||||
local json_file="$1"
|
||||
local field="$2"
|
||||
# Override vars with values from settings.json
|
||||
if [[ -f "${STATE_FOLDER}/settings.json" ]]; then
|
||||
# If storagePath is set in settings.json, use it
|
||||
if [[ "$(get_json_field "${STATE_FOLDER}/settings.json" storagePath)" != "null" ]]; then
|
||||
STORAGE_PATH="$(get_json_field "${STATE_FOLDER}/settings.json" storagePath)"
|
||||
fi
|
||||
fi
|
||||
|
||||
jq -r ".${field}" "${json_file}"
|
||||
}
|
||||
write_log "Running app script: ROOT_FOLDER=${ROOT_FOLDER}, ROOT_FOLDER_HOST=${ROOT_FOLDER_HOST}, REPO_ID=${REPO_ID}, STORAGE_PATH=${STORAGE_PATH}"
|
||||
|
||||
if [ -z ${1+x} ]; then
|
||||
command=""
|
||||
|
@ -35,7 +36,6 @@ else
|
|||
fi
|
||||
|
||||
if [ -z ${2+x} ]; then
|
||||
show_help
|
||||
exit 1
|
||||
else
|
||||
app="$2"
|
||||
|
@ -49,13 +49,12 @@ else
|
|||
cp -r "${ROOT_FOLDER}/repos/${REPO_ID}/apps/${app}"/* "${app_dir}"
|
||||
fi
|
||||
|
||||
app_data_dir="${ROOT_FOLDER}/app-data/${app}"
|
||||
app_data_dir="/app/storage/app-data/${app}"
|
||||
|
||||
if [[ -z "${app}" ]] || [[ ! -d "${app_dir}" ]]; then
|
||||
echo "Error: \"${app}\" is not a valid app"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
if [ -z ${3+x} ]; then
|
||||
|
@ -83,12 +82,21 @@ compose() {
|
|||
app_compose_file="${app_dir}/docker-compose.arm.yml"
|
||||
fi
|
||||
|
||||
# Pick arm architecture if running on arm and if the app has a docker-compose.arm64.yml file
|
||||
if [[ "$architecture" == "arm64" ]] && [[ -f "${app_dir}/docker-compose.arm64.yml" ]]; then
|
||||
app_compose_file="${app_dir}/docker-compose.arm64.yml"
|
||||
fi
|
||||
|
||||
local common_compose_file="${ROOT_FOLDER}/repos/${REPO_ID}/apps/docker-compose.common.yml"
|
||||
|
||||
# Vars to use in compose file
|
||||
export APP_DATA_DIR="${ROOT_FOLDER_HOST}/app-data/${app}"
|
||||
export APP_DATA_DIR="${STORAGE_PATH}/app-data/${app}"
|
||||
export ROOT_FOLDER_HOST="${ROOT_FOLDER_HOST}"
|
||||
|
||||
write_log "Running docker compose -f ${app_compose_file} -f ${common_compose_file} ${*}"
|
||||
write_log "APP_DATA_DIR=${APP_DATA_DIR}"
|
||||
write_log "ROOT_FOLDER_HOST=${ROOT_FOLDER_HOST}"
|
||||
|
||||
docker compose \
|
||||
--env-file "${app_data_dir}/app.env" \
|
||||
--project-name "${app}" \
|
||||
|
@ -99,6 +107,9 @@ compose() {
|
|||
|
||||
# Install new app
|
||||
if [[ "$command" = "install" ]]; then
|
||||
# Write to file script.log
|
||||
write_log "Installing app ${app}..."
|
||||
|
||||
compose "${app}" pull
|
||||
|
||||
# Copy default data dir to app data dir if it exists
|
||||
|
|
71
scripts/common.sh
Normal file
71
scripts/common.sh
Normal file
|
@ -0,0 +1,71 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Get field from json file
|
||||
function get_json_field() {
|
||||
local json_file="$1"
|
||||
local field="$2"
|
||||
|
||||
jq -r ".${field}" "${json_file}"
|
||||
}
|
||||
|
||||
function write_log() {
|
||||
local message="$1"
|
||||
local log_file="/app/logs/script.log"
|
||||
|
||||
echo "$(date) - ${message}" >>"${log_file}"
|
||||
}
|
||||
|
||||
function derive_entropy() {
|
||||
SEED_FILE="${STATE_FOLDER}/seed"
|
||||
identifier="${1}"
|
||||
tipi_seed=$(cat "${SEED_FILE}") || true
|
||||
|
||||
if [[ -z "$tipi_seed" ]] || [[ -z "$identifier" ]]; then
|
||||
echo >&2 "Missing derivation parameter, this is unsafe, exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "%s" "${identifier}" | openssl dgst -sha256 -hmac "${tipi_seed}" | sed 's/^.* //'
|
||||
}
|
||||
|
||||
function ensure_pwd() {
|
||||
# # Ensure PWD ends with /runtipi
|
||||
cd /runtipi || echo ""
|
||||
|
||||
if [[ $(basename "$(pwd)") != "runtipi" ]] || [[ ! -f "${BASH_SOURCE[0]}" ]]; then
|
||||
echo "Please run this script from the runtipi directory"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function ensure_root() {
|
||||
if [[ $UID != 0 ]]; then
|
||||
echo "Tipi must be started as root"
|
||||
echo "Please re-run this script as"
|
||||
echo " sudo ./scripts/start"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function ensure_linux() {
|
||||
# Check we are on linux
|
||||
if [[ "$(uname)" != "Linux" ]]; then
|
||||
echo "Tipi only works on Linux"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function clean_logs() {
|
||||
# Clean logs folder
|
||||
logs_folder="${ROOT_FOLDER}/logs"
|
||||
if [ "$(find "${logs_folder}" -maxdepth 1 -type f | wc -l)" -gt 0 ]; then
|
||||
echo "Cleaning logs folder..."
|
||||
|
||||
files=($(ls -d "${logs_folder}"/* | xargs -n 1 basename | sed 's/\///g'))
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
echo "Removing ${file}"
|
||||
rm -rf "${ROOT_FOLDER}/logs/${file}"
|
||||
done
|
||||
fi
|
||||
}
|
|
@ -11,21 +11,6 @@ fi
|
|||
|
||||
ROOT_FOLDER="${PWD}"
|
||||
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
app 0.0.1
|
||||
|
||||
CLI for managing Tipi apps
|
||||
|
||||
Usage: git <command> <repo> [<arguments>]
|
||||
|
||||
Commands:
|
||||
clone Clones a repo in the repo folder
|
||||
update Updates the repo folder
|
||||
get_hash Gets the local hash of the repo
|
||||
EOF
|
||||
}
|
||||
|
||||
# Get a static hash based on the repo url
|
||||
function get_hash() {
|
||||
url="${1}"
|
||||
|
|
135
scripts/start.sh
135
scripts/start.sh
|
@ -1,30 +1,45 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Required Notice: Copyright
|
||||
# Umbrel (https://umbrel.com)
|
||||
|
||||
set -e # Exit immediately if a command exits with a non-zero status.
|
||||
|
||||
source "${BASH_SOURCE%/*}/common.sh"
|
||||
|
||||
write_log "Starting Tipi..."
|
||||
|
||||
ROOT_FOLDER="${PWD}"
|
||||
|
||||
# Cleanup and ensure environment
|
||||
ensure_linux
|
||||
ensure_pwd
|
||||
ensure_root
|
||||
clean_logs
|
||||
|
||||
# Default variables
|
||||
NGINX_PORT=80
|
||||
NGINX_PORT_SSL=443
|
||||
DOMAIN=tipi.localhost
|
||||
|
||||
# Check we are on linux
|
||||
if [[ "$(uname)" != "Linux" ]]; then
|
||||
echo "Tipi only works on Linux"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure BASH_SOURCE is ./scripts/start.sh
|
||||
if [[ $(basename "$(pwd)") != "runtipi" ]] || [[ ! -f "${BASH_SOURCE[0]}" ]]; then
|
||||
echo "Please make sure this script is executed from runtipi/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NETWORK_INTERFACE="$(ip route | grep default | awk '{print $5}' | uniq)"
|
||||
INTERNAL_IP="$(ip addr show "${NETWORK_INTERFACE}" | grep "inet " | awk '{print $2}' | cut -d/ -f1)"
|
||||
STATE_FOLDER="${ROOT_FOLDER}/state"
|
||||
SED_ROOT_FOLDER="$(echo "$ROOT_FOLDER" | sed 's/\//\\\//g')"
|
||||
DNS_IP=9.9.9.9 # Default to Quad9 DNS
|
||||
ARCHITECTURE="$(uname -m)"
|
||||
TZ="$(timedatectl | grep "Time zone" | awk '{print $3}' | sed 's/\//\\\//g' || Europe\/Berlin)"
|
||||
apps_repository="https://github.com/meienberger/runtipi-appstore"
|
||||
REPO_ID="$("${ROOT_FOLDER}"/scripts/git.sh get_hash ${apps_repository})"
|
||||
APPS_REPOSITORY_ESCAPED="$(echo ${apps_repository} | sed 's/\//\\\//g')"
|
||||
JWT_SECRET=$(derive_entropy "jwt")
|
||||
POSTGRES_PASSWORD=$(derive_entropy "postgres")
|
||||
TIPI_VERSION=$(get_json_field "${ROOT_FOLDER}/package.json" version)
|
||||
storage_path="${ROOT_FOLDER}"
|
||||
STORAGE_PATH_ESCAPED="$(echo "${storage_path}" | sed 's/\//\\\//g')"
|
||||
|
||||
while [ -n "$1" ]; do # while loop starts
|
||||
if [[ "$ARCHITECTURE" == "aarch64" ]]; then
|
||||
ARCHITECTURE="arm64"
|
||||
fi
|
||||
|
||||
# Parse arguments
|
||||
while [ -n "$1" ]; do
|
||||
case "$1" in
|
||||
--rc) rc="true" ;;
|
||||
--ci) ci="true" ;;
|
||||
|
@ -87,67 +102,14 @@ if [[ "${NGINX_PORT}" != "80" ]] && [[ "${DOMAIN}" != "tipi.localhost" ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
ROOT_FOLDER="${PWD}"
|
||||
STATE_FOLDER="${ROOT_FOLDER}/state"
|
||||
SED_ROOT_FOLDER="$(echo "$ROOT_FOLDER" | sed 's/\//\\\//g')"
|
||||
|
||||
DNS_IP=9.9.9.9 # Default to Quad9 DNS
|
||||
ARCHITECTURE="$(uname -m)"
|
||||
TZ="$(timedatectl | grep "Time zone" | awk '{print $3}' | sed 's/\//\\\//g' || Europe\/Berlin)"
|
||||
APPS_REPOSITORY="https://github.com/meienberger/runtipi-appstore"
|
||||
REPO_ID="$("${ROOT_FOLDER}"/scripts/git.sh get_hash ${APPS_REPOSITORY})"
|
||||
APPS_REPOSITORY_ESCAPED="$(echo ${APPS_REPOSITORY} | sed 's/\//\\\//g')"
|
||||
|
||||
if [[ "$ARCHITECTURE" == "aarch64" ]]; then
|
||||
ARCHITECTURE="arm64"
|
||||
fi
|
||||
|
||||
if [[ $UID != 0 ]]; then
|
||||
echo "Tipi must be started as root"
|
||||
echo "Please re-run this script as"
|
||||
echo " sudo ./scripts/start"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Configure Tipi if it isn't already configured
|
||||
# Configure Tipi
|
||||
"${ROOT_FOLDER}/scripts/configure.sh"
|
||||
|
||||
# Get field from json file
|
||||
function get_json_field() {
|
||||
local json_file="$1"
|
||||
local field="$2"
|
||||
|
||||
jq -r ".${field}" "${json_file}"
|
||||
}
|
||||
|
||||
# Deterministically derives 128 bits of cryptographically secure entropy
|
||||
function derive_entropy() {
|
||||
SEED_FILE="${STATE_FOLDER}/seed"
|
||||
identifier="${1}"
|
||||
tipi_seed=$(cat "${SEED_FILE}") || true
|
||||
|
||||
if [[ -z "$tipi_seed" ]] || [[ -z "$identifier" ]]; then
|
||||
echo >&2 "Missing derivation parameter, this is unsafe, exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# We need `sed 's/^.* //'` to trim the "(stdin)= " prefix from some versions of openssl
|
||||
printf "%s" "${identifier}" | openssl dgst -sha256 -hmac "${tipi_seed}" | sed 's/^.* //'
|
||||
}
|
||||
|
||||
# Copy the config sample if it isn't here
|
||||
if [[ ! -f "${STATE_FOLDER}/apps.json" ]]; then
|
||||
cp "${ROOT_FOLDER}/templates/config-sample.json" "${STATE_FOLDER}/config.json"
|
||||
fi
|
||||
|
||||
# Get current dns from host
|
||||
if [[ -f "/etc/resolv.conf" ]]; then
|
||||
TEMP=$(grep -E -o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' /etc/resolv.conf | head -n 1)
|
||||
fi
|
||||
|
||||
# Clean logs folder
|
||||
rm -rf "${ROOT_FOLDER}/logs/*"
|
||||
|
||||
# Create seed file with cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1
|
||||
if [[ ! -f "${STATE_FOLDER}/seed" ]]; then
|
||||
echo "Generating seed..."
|
||||
|
@ -167,10 +129,6 @@ ENV_FILE=$(mktemp)
|
|||
# Copy template configs to intermediary configs
|
||||
[[ -f "$ROOT_FOLDER/templates/env-sample" ]] && cp "$ROOT_FOLDER/templates/env-sample" "$ENV_FILE"
|
||||
|
||||
JWT_SECRET=$(derive_entropy "jwt")
|
||||
POSTGRES_PASSWORD=$(derive_entropy "postgres")
|
||||
TIPI_VERSION=$(get_json_field "${ROOT_FOLDER}/package.json" version)
|
||||
|
||||
# Override vars with values from settings.json
|
||||
if [[ -f "${STATE_FOLDER}/settings.json" ]]; then
|
||||
|
||||
|
@ -186,7 +144,7 @@ if [[ -f "${STATE_FOLDER}/settings.json" ]]; then
|
|||
|
||||
# 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')"
|
||||
APPS_REPOSITORY_ESCAPED="$(echo ${apps_repository} | sed 's/\//\\\//g')"
|
||||
fi
|
||||
|
||||
# If appsRepoId is set in settings.json, use it
|
||||
|
@ -208,23 +166,17 @@ if [[ -f "${STATE_FOLDER}/settings.json" ]]; then
|
|||
if [[ "$(get_json_field "${STATE_FOLDER}/settings.json" listenIp)" != "null" ]]; then
|
||||
INTERNAL_IP=$(get_json_field "${STATE_FOLDER}/settings.json" listenIp)
|
||||
fi
|
||||
|
||||
# If storagePath is set in settings.json, use it
|
||||
if [[ "$(get_json_field "${STATE_FOLDER}/settings.json" storagePath)" != "null" ]]; then
|
||||
storage_path="$(get_json_field "${STATE_FOLDER}/settings.json" storagePath)"
|
||||
STORAGE_PATH_ESCAPED="$(echo "${storage_path}" | sed 's/\//\\\//g')"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Creating .env file with the following values:"
|
||||
echo " DOMAIN=${DOMAIN}"
|
||||
echo " INTERNAL_IP=${INTERNAL_IP}"
|
||||
echo " NGINX_PORT=${NGINX_PORT}"
|
||||
echo " NGINX_PORT_SSL=${NGINX_PORT_SSL}"
|
||||
echo " DNS_IP=${DNS_IP}"
|
||||
echo " ARCHITECTURE=${ARCHITECTURE}"
|
||||
echo " TZ=${TZ}"
|
||||
echo " APPS_REPOSITORY=${APPS_REPOSITORY}"
|
||||
echo " REPO_ID=${REPO_ID}"
|
||||
echo " JWT_SECRET=<redacted>"
|
||||
echo " POSTGRES_PASSWORD=<redacted>"
|
||||
echo " TIPI_VERSION=${TIPI_VERSION}"
|
||||
echo " ROOT_FOLDER=${SED_ROOT_FOLDER}"
|
||||
echo " APPS_REPOSITORY=${APPS_REPOSITORY_ESCAPED}"
|
||||
# Set array with all new values
|
||||
new_values="DOMAIN=${DOMAIN}\nDNS_IP=${DNS_IP}\nAPPS_REPOSITORY=${APPS_REPOSITORY_ESCAPED}\nREPO_ID=${REPO_ID}\nNGINX_PORT=${NGINX_PORT}\nNGINX_PORT_SSL=${NGINX_PORT_SSL}\nINTERNAL_IP=${INTERNAL_IP}\nSTORAGE_PATH=${STORAGE_PATH_ESCAPED}\nTZ=${TZ}\nJWT_SECRET=${JWT_SECRET}\nROOT_FOLDER=${SED_ROOT_FOLDER}\nTIPI_VERSION=${TIPI_VERSION}\nARCHITECTURE=${ARCHITECTURE}"
|
||||
write_log "Final values: \n${new_values}"
|
||||
|
||||
for template in ${ENV_FILE}; do
|
||||
sed -i "s/<dns_ip>/${DNS_IP}/g" "${template}"
|
||||
|
@ -240,6 +192,7 @@ for template in ${ENV_FILE}; do
|
|||
sed -i "s/<apps_repo_id>/${REPO_ID}/g" "${template}"
|
||||
sed -i "s/<apps_repo_url>/${APPS_REPOSITORY_ESCAPED}/g" "${template}"
|
||||
sed -i "s/<domain>/${DOMAIN}/g" "${template}"
|
||||
sed -i "s/<storage_path>/${STORAGE_PATH_ESCAPED}/g" "${template}"
|
||||
done
|
||||
|
||||
mv -f "$ENV_FILE" "$ROOT_FOLDER/.env"
|
||||
|
|
|
@ -1,18 +1,10 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $UID != 0 ]]; then
|
||||
echo "Tipi must be stopped as root"
|
||||
echo "Please re-run this script as"
|
||||
echo " sudo ./scripts/stop.sh"
|
||||
exit 1
|
||||
fi
|
||||
source "${BASH_SOURCE%/*}/common.sh"
|
||||
|
||||
# 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
|
||||
ensure_root
|
||||
ensure_pwd
|
||||
|
||||
ROOT_FOLDER="${PWD}"
|
||||
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
#!/usr/bin/env bash
|
||||
set -e # Exit immediately if a command exits with a non-zero status.
|
||||
|
||||
cd /runtipi || echo ""
|
||||
source "${BASH_SOURCE%/*}/common.sh"
|
||||
|
||||
if [[ $(basename "$(pwd)") != "runtipi" ]] || [[ ! -f "${BASH_SOURCE[0]}" ]]; then
|
||||
echo "Please make sure this script is executed from runtipi/"
|
||||
exit 1
|
||||
fi
|
||||
ensure_pwd
|
||||
|
||||
ROOT_FOLDER="$(pwd)"
|
||||
STATE_FOLDER="${ROOT_FOLDER}/state"
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
cd /runtipi || echo ""
|
||||
source "${BASH_SOURCE%/*}/common.sh"
|
||||
|
||||
# Ensure PWD ends with /runtipi
|
||||
if [[ $(basename "$(pwd)") != "runtipi" ]] || [[ ! -f "${BASH_SOURCE[0]}" ]]; then
|
||||
echo "Please make sure this script is executed from runtipi/"
|
||||
exit 1
|
||||
fi
|
||||
ensure_pwd
|
||||
|
||||
if [ -z ${1+x} ]; then
|
||||
command=""
|
||||
|
|
|
@ -14,3 +14,4 @@ NGINX_PORT=<nginx_port>
|
|||
NGINX_PORT_SSL=<nginx_port_ssl>
|
||||
POSTGRES_PASSWORD=<postgres_password>
|
||||
DOMAIN=<domain>
|
||||
STORAGE_PATH=<storage_path>
|
Loading…
Reference in a new issue