feat: customize applications storage-path

This commit is contained in:
Nicolas Meienberger 2022-09-27 20:25:56 +02:00
parent 164635d33b
commit bb5a50e143
27 changed files with 288 additions and 280 deletions

View file

@ -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

View file

@ -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}

View file

@ -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}

View file

@ -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}

View file

@ -5,3 +5,4 @@ dist/
coverage/
logs/
sessions/
.vscode

View file

@ -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));
}
}
}

View file

@ -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);

View file

@ -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);

View file

@ -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();

View file

@ -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();
});

View file

@ -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);

View file

@ -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 };

View file

@ -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);

View file

@ -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' });

View file

@ -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,

View file

@ -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);

View file

@ -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]);
});
});

View file

@ -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}`);
}
};

View file

@ -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}`);

View file

@ -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
View 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
}

View file

@ -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}"

View file

@ -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"

View file

@ -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}"

View file

@ -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"

View file

@ -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=""

View file

@ -13,4 +13,5 @@ ROOT_FOLDER_HOST=<root_folder>
NGINX_PORT=<nginx_port>
NGINX_PORT_SSL=<nginx_port_ssl>
POSTGRES_PASSWORD=<postgres_password>
DOMAIN=<domain>
DOMAIN=<domain>
STORAGE_PATH=<storage_path>