refactor: merge worker and dashboard

add storage path

fix: fix compose file

chore: use dev compose file for now

refactor: try to not use hardcoded values

refactor(worker): use constants instead of hardcoded values

refactor(dashboard): use constants instead of hardcoded values

chore: ignore new runtipi-data folder

fix(worker): wrong constant

chore: remove root folder host as it is not being used anywhere

refactor: rename storagePath to appDataDirPath because thats what it does

feat: add production dockerfile

fix: add cmd to prod dockerfile

fix: small fixes in prod dockerfile

chore: fix path mistakes

fix: re-add ROOT_FOLDER_HOST

chore: revert storage path
This commit is contained in:
Stavros 2024-03-03 20:05:09 +02:00 committed by Nicolas Meienberger
parent 8268a1e5e8
commit 2681630996
69 changed files with 496 additions and 437 deletions

View File

@ -1,7 +1,8 @@
*
-
### Includes ###
!pnpm-*.yaml
### Includes
!pnpm-_.yaml
!package.json
!patches/**
!packages/**/src/**
@ -9,9 +10,10 @@
!**/package.json
!**/nodemon.json
!**/tsconfig.json
!**/build.js
!\*\*/build.js
!next.config.mjs
!sentry.*.config.ts
!sentry._.config.ts
!public/**
!src/**
!tests/**
!tests/\*\*
!start.\_.sh

View File

@ -11,12 +11,12 @@ STORAGE_PATH=/path/to/runtipi
NGINX_PORT=7000
NGINX_PORT_SSL=443
DOMAIN=tipi.localhost
POSTGRES_HOST=tipi-db
POSTGRES_HOST=runtipi-db
POSTGRES_DBNAME=tipi
POSTGRES_USERNAME=tipi
POSTGRES_PASSWORD=postgres
POSTGRES_PORT=5432
REDIS_HOST=tipi-redis
REDIS_HOST=runtipi-redis
REDIS_PASSWORD=redis
DEMO_MODE=false
LOCAL_DOMAIN=tipi.lan

3
.gitignore vendored
View File

@ -70,3 +70,6 @@ temp
# Sentry Config File
.sentryclirc
# Runtipi data folder
/runtipi-data/

View File

@ -3,15 +3,22 @@ ARG ALPINE_VERSION="3.18"
FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS node_base
# ---- BUILDER BASE ----
FROM node_base AS builder_base
RUN apk add --no-cache python3 make g++
RUN npm install pnpm -g
RUN apk add --no-cache curl python3 make g++
# BUILDER
FROM builder_base AS builder
# ---- RUNNER BASE ----
FROM node_base AS runner_base
WORKDIR /app
RUN apk add --no-cache curl openssl git
RUN npm install pm2 -g
# ---- BUILD DASHBOARD ----
FROM builder_base AS dashboard_builder
WORKDIR /dashboard
COPY ./pnpm-lock.yaml ./
RUN pnpm fetch
@ -43,21 +50,73 @@ ENV NEXT_SHARP_PATH=/app/node_modules/sharp
RUN pnpm build
# APP
FROM node_base AS app
# ---- BUILD WORKER ----
FROM builder_base AS worker_builder
ENV NODE_ENV production
WORKDIR /worker
USER 1000:1000
ARG TARGETARCH
ENV TARGETARCH=${TARGETARCH}
ARG DOCKER_COMPOSE_VERSION="v2.23.3"
WORKDIR /app
RUN echo "Building for ${TARGETARCH}"
COPY --from=builder /app/next.config.mjs ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
COPY --from=builder --chown=1000:1000 /app/.next/standalone ./
COPY --from=builder --chown=1000:1000 /app/.next/static ./.next/static
EXPOSE 3000
RUN if [ "${TARGETARCH}" = "arm64" ]; then \
curl -L -o docker-binary "https://github.com/docker/compose/releases/download/$DOCKER_COMPOSE_VERSION/docker-compose-linux-aarch64"; \
elif [ "${TARGETARCH}" = "amd64" ]; then \
curl -L -o docker-binary "https://github.com/docker/compose/releases/download/$DOCKER_COMPOSE_VERSION/docker-compose-linux-x86_64"; \
else \
echo "Unsupported architecture"; \
fi
CMD ["npm", "run", "start"]
RUN chmod +x docker-binary
COPY ./pnpm-lock.yaml ./
RUN pnpm fetch --ignore-scripts
COPY ./pnpm-workspace.yaml ./
COPY ./packages ./packages
RUN pnpm install -r --prefer-offline
COPY ./packages/worker/build.js ./packages/worker/build.js
COPY ./packages/worker/src ./packages/worker/src
COPY ./packages/worker/package.json ./packages/worker/package.json
COPY ./packages/worker/assets ./packages/worker/assets
ARG SENTRY_AUTH_TOKEN
ARG SENTRY_DISABLE_AUTO_UPLOAD
ARG TIPI_VERSION
ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN}
ENV SENTRY_DISABLE_AUTO_UPLOAD=${SENTRY_DISABLE_AUTO_UPLOAD}
ENV TIPI_VERSION=${TIPI_VERSION}
RUN pnpm -r build --filter @runtipi/worker
# ---- RUNNER ----
FROM runner_base AS app
ENV NODE_ENV=production
WORKDIR /worker
COPY --from=worker_builder /worker/packages/worker/dist .
COPY --from=worker_builder /worker/packages/worker/assets ./assets
COPY --from=worker_builder /worker/docker-binary /usr/local/bin/docker-compose
WORKDIR /dashboard
COPY --from=dashboard_builder /dashboard/next.config.mjs ./
COPY --from=dashboard_builder /dashboard/public ./public
COPY --from=dashboard_builder /dashboard/package.json ./package.json
COPY --from=dashboard_builder /dashboard/.next/standalone ./
COPY --from=dashboard_builder /dashboard/.next/static ./.next/static
WORKDIR /
COPY ./start.prod.sh ./start.sh
EXPOSE 3000 5000
CMD ["sh", "start.sh"]

View File

@ -3,16 +3,34 @@ ARG ALPINE_VERSION="3.18"
FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION}
RUN apk add --no-cache python3 make g++
RUN npm install pnpm -g
RUN apk add --no-cache python3 make g++ curl git
RUN npm install pnpm pm2 -g
ARG TARGETARCH
ARG DOCKER_COMPOSE_VERSION="v2.23.3"
ENV TARGETARCH=${TARGETARCH}
ENV NODE_ENV="development"
# Dashboard
WORKDIR /app
RUN echo "Building for ${TARGETARCH}"
RUN if [ "${TARGETARCH}" = "arm64" ]; then \
curl -L -o docker-binary "https://github.com/docker/compose/releases/download/$DOCKER_COMPOSE_VERSION/docker-compose-linux-aarch64"; \
elif [ "${TARGETARCH}" = "amd64" ]; then \
curl -L -o docker-binary "https://github.com/docker/compose/releases/download/$DOCKER_COMPOSE_VERSION/docker-compose-linux-x86_64"; \
fi
RUN chmod +x docker-binary
RUN mv docker-binary /usr/local/bin/docker-compose
COPY ./pnpm-lock.yaml ./
RUN pnpm fetch --ignore-scripts
COPY ./package*.json ./
COPY ./packages/shared ./packages/shared
COPY ./packages ./packages
RUN pnpm install -r --prefer-offline
@ -25,4 +43,6 @@ COPY ./sentry.client.config.ts ./sentry.client.config.ts
COPY ./sentry.edge.config.ts ./sentry.edge.config.ts
COPY ./sentry.server.config.ts ./sentry.server.config.ts
CMD ["npm", "run", "dev"]
COPY ./start.dev.sh ./start.sh
CMD ["sh", "start.sh"]

View File

@ -1,10 +1,10 @@
version: '3.7'
services:
tipi-reverse-proxy:
container_name: tipi-reverse-proxy
runtipi-reverse-proxy:
container_name: runtipi-reverse-proxy
depends_on:
- tipi-dashboard
- runtipi
image: traefik:v2.11
restart: unless-stopped
ports:
@ -19,8 +19,8 @@ services:
networks:
- tipi_main_network
tipi-db:
container_name: tipi-db
runtipi-db:
container_name: runtipi-db
image: postgres:14
restart: unless-stopped
stop_grace_period: 1m
@ -40,8 +40,8 @@ services:
networks:
- tipi_main_network
tipi-redis:
container_name: tipi-redis
runtipi-redis:
container_name: runtipi-redis
image: redis:7.2.0
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD} --stop-writes-on-bgsave-error no
@ -57,48 +57,71 @@ services:
networks:
- tipi_main_network
tipi-worker:
runtipi:
build:
context: .
dockerfile: ./packages/worker/Dockerfile.dev
container_name: tipi-worker
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3000/worker-api/healthcheck']
interval: 5s
timeout: 10s
retries: 120
start_period: 5s
dockerfile: Dockerfile.dev
container_name: runtipi
restart: unless-stopped
depends_on:
tipi-db:
runtipi-db:
condition: service_healthy
tipi-redis:
runtipi-redis:
condition: service_healthy
env_file:
- .env
volumes:
# Hot reload
- ./src:/app/src
- ./packages/worker/src:/app/packages/worker/src
- ./packages/shared/src:/app/packages/shared/src
# Data
- ./state:/data/state
- ./repos:/data/repos
- ./apps:/data/apps
- ./logs:/data/logs
- ./traefik:/data/traefik
- ./user-config:/data/user-config
- ${STORAGE_PATH:-$PWD}/app-data:/app-data
# Static
- ./.env:/data/.env
- /var/run/docker.sock:/var/run/docker.sock:ro
- /:/mnt/host:ro
networks:
- tipi_main_network
ports:
- 3000:3000
environment:
NODE_ENV: development
TIPI_VERSION: development
volumes:
# Dev mode
- ./packages/worker/src:/app/packages/worker/src
# Production mode
- /proc:/host/proc:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./.env:/app/.env
- ./state:/app/state
- ./repos:/app/repos
- ./apps:/app/apps
- ${STORAGE_PATH:-$PWD}/app-data:/storage/app-data
- ./logs:/app/logs
- ./traefik:/app/traefik
- ./user-config:/app/user-config
networks:
- tipi_main_network
WORKER_APP_DIR: /app/packages/worker
DASHBOARD_APP_DIR: /app
NEXT_PUBLIC_TIPI_VERSION: development
env_file:
- .env
labels:
# ---- General ----- #
traefik.enable: true
traefik.http.services.worker.loadbalancer.server.port: 3001
traefik.http.services.worker-api.loadbalancer.server.port: 3000
traefik.http.middlewares.redirect-to-https.redirectscheme.scheme: https
# ---- Dashboard ---- #
traefik.http.services.dashboard.loadbalancer.server.port: 3000
# Local ip
traefik.http.routers.dashboard.rule: PathPrefix("/")
traefik.http.routers.dashboard.service: dashboard
traefik.http.routers.dashboard.entrypoints: web
# Local domain
traefik.http.routers.dashboard-local-insecure.rule: Host(`${LOCAL_DOMAIN}`)
traefik.http.routers.dashboard-local-insecure.entrypoints: web
traefik.http.routers.dashboard-local-insecure.service: dashboard
traefik.http.routers.dashboard-local-insecure.middlewares: redirect-to-https
# Secure
traefik.http.routers.dashboard-local.rule: Host(`${LOCAL_DOMAIN}`)
traefik.http.routers.dashboard-local.entrypoints: websecure
traefik.http.routers.dashboard-local.tls: true
traefik.http.routers.dashboard-local.service: dashboard
# ---- Worker ---- #
traefik.http.services.worker.loadbalancer.server.port: 5001
traefik.http.services.worker-api.loadbalancer.server.port: 5000
# Local ip
traefik.http.routers.worker.rule: PathPrefix("/worker")
traefik.http.routers.worker.service: worker
@ -115,7 +138,7 @@ services:
traefik.http.routers.worker-api-local-insecure.entrypoints: web
traefik.http.routers.worker-api-local-insecure.service: worker-api
traefik.http.routers.worker-api-local-insecure.middlewares: redirect-to-https
# secure
# Secure
traefik.http.routers.worker-local.rule: Host(`${LOCAL_DOMAIN}`) && PathPrefix("/worker")
traefik.http.routers.worker-local.entrypoints: websecure
traefik.http.routers.worker-local.tls: true
@ -124,60 +147,6 @@ services:
traefik.http.routers.worker-api-local.entrypoints: websecure
traefik.http.routers.worker-api-local.tls: true
tipi-dashboard:
build:
context: .
dockerfile: Dockerfile.dev
container_name: tipi-dashboard
restart: unless-stopped
depends_on:
tipi-db:
condition: service_healthy
tipi-redis:
condition: service_healthy
tipi-worker:
condition: service_healthy
env_file:
- .env
environment:
NODE_ENV: development
TIPI_VERSION: development
NEXT_PUBLIC_TIPI_VERSION: 0.0.0
networks:
- tipi_main_network
ports:
- 3000:3000
volumes:
# - /dashboard/node_modules
# - /dashboard/.next
- ./.env:/runtipi/.env
- ./src:/app/src
- ./packages:/app/packages
- ./state:/runtipi/state
- ./repos:/runtipi/repos:ro
- ./apps:/runtipi/apps
- ./logs:/app/logs
- ./traefik:/runtipi/traefik
- ${STORAGE_PATH:-$PWD}:/app/storage
labels:
traefik.enable: true
traefik.http.services.dashboard.loadbalancer.server.port: 3000
traefik.http.middlewares.redirect-to-https.redirectscheme.scheme: https
# Local ip
traefik.http.routers.dashboard.rule: PathPrefix("/")
traefik.http.routers.dashboard.service: dashboard
traefik.http.routers.dashboard.entrypoints: web
# Local domain
traefik.http.routers.dashboard-local-insecure.rule: Host(`${LOCAL_DOMAIN}`)
traefik.http.routers.dashboard-local-insecure.entrypoints: web
traefik.http.routers.dashboard-local-insecure.service: dashboard
traefik.http.routers.dashboard-local-insecure.middlewares: redirect-to-https
# secure
traefik.http.routers.dashboard-local.rule: Host(`${LOCAL_DOMAIN}`)
traefik.http.routers.dashboard-local.entrypoints: websecure
traefik.http.routers.dashboard-local.tls: true
traefik.http.routers.dashboard-local.service: dashboard
networks:
tipi_main_network:
driver: bridge

View File

@ -1,10 +1,10 @@
version: '3.7'
version: '3.9'
services:
tipi-reverse-proxy:
container_name: tipi-reverse-proxy
runtipi-reverse-proxy:
container_name: runtipi-reverse-proxy
depends_on:
- tipi-dashboard
- runtipi
image: traefik:v2.11
restart: unless-stopped
ports:
@ -19,8 +19,8 @@ services:
networks:
- tipi_main_network
tipi-db:
container_name: tipi-db
runtipi-db:
container_name: runtipi-db
image: postgres:14
restart: unless-stopped
stop_grace_period: 1m
@ -40,8 +40,8 @@ services:
networks:
- tipi_main_network
tipi-redis:
container_name: tipi-redis
runtipi-redis:
container_name: runtipi-redis
image: redis:7.2.0
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD} --stop-writes-on-bgsave-error no
@ -57,48 +57,63 @@ services:
networks:
- tipi_main_network
tipi-worker:
runtipi:
build:
context: .
dockerfile: ./packages/worker/Dockerfile
dockerfile: Dockerfile
args:
- SENTRY_DISABLE_AUTO_UPLOAD=true
- TIPI_VERSION=development
container_name: tipi-worker
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3000/worker-api/healthcheck']
interval: 5s
timeout: 10s
retries: 120
start_period: 5s
depends_on:
tipi-db:
condition: service_healthy
tipi-redis:
condition: service_healthy
SENTRY_DISABLE_AUTO_UPLOAD: true
TIPI_VERSION: development
container_name: runtipi
restart: unless-stopped
env_file:
- .env
volumes:
# Data
- ./state:/data/state
- ./repos:/data/repos
- ./apps:/data/apps
- ./logs:/data/logs
- ./traefik:/data/traefik
- ./user-config:/data/user-config
- ${STORAGE_PATH:-$PWD}/app-data:/app-data
# Static
- ./.env:/data/.env
- /var/run/docker.sock:/var/run/docker.sock:ro
- /:/mnt/host:ro
environment:
NODE_ENV: production
TIPI_VERSION: development
volumes:
- /proc:/host/proc
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./.env:/app/.env
- ./state:/app/state
- ./repos:/app/repos
- ./apps:/app/apps
- ./logs:/app/logs
- ./traefik:/app/traefik
- ./user-config:/app/user-config
- ${STORAGE_PATH:-.}:/storage
NEXT_PUBLIC_TIPI_VERSION: development
networks:
- tipi_main_network
ports:
- 3000:3000
labels:
# ---- General ----- #
traefik.enable: true
traefik.http.services.worker.loadbalancer.server.port: 3001
traefik.http.services.worker-api.loadbalancer.server.port: 3000
traefik.http.middlewares.redirect-to-https.redirectscheme.scheme: https
# ---- Dashboard ----- #
traefik.http.services.dashboard.loadbalancer.server.port: 3000
# Local ip
traefik.http.routers.dashboard.rule: PathPrefix("/")
traefik.http.routers.dashboard.service: dashboard
traefik.http.routers.dashboard.entrypoints: web
# Local domain
traefik.http.routers.dashboard-local-insecure.rule: Host(`${LOCAL_DOMAIN}`)
traefik.http.routers.dashboard-local-insecure.entrypoints: web
traefik.http.routers.dashboard-local-insecure.service: dashboard
traefik.http.routers.dashboard-local-insecure.middlewares: redirect-to-https
# Secure
traefik.http.routers.dashboard-local.rule: Host(`${LOCAL_DOMAIN}`)
traefik.http.routers.dashboard-local.entrypoints: websecure
traefik.http.routers.dashboard-local.tls: true
traefik.http.routers.dashboard-local.service: dashboard
# ---- Worker ----- #
traefik.http.services.worker.loadbalancer.server.port: 5001
traefik.http.services.worker-api.loadbalancer.server.port: 5000
# Local ip
traefik.http.routers.worker.rule: PathPrefix("/worker")
traefik.http.routers.worker.service: worker
@ -115,7 +130,7 @@ services:
traefik.http.routers.worker-api-local-insecure.entrypoints: web
traefik.http.routers.worker-api-local-insecure.service: worker-api
traefik.http.routers.worker-api-local-insecure.middlewares: redirect-to-https
# secure
# Secure
traefik.http.routers.worker-local.rule: Host(`${LOCAL_DOMAIN}`) && PathPrefix("/worker")
traefik.http.routers.worker-local.entrypoints: websecure
traefik.http.routers.worker-local.tls: true
@ -124,58 +139,6 @@ services:
traefik.http.routers.worker-api-local.entrypoints: websecure
traefik.http.routers.worker-api-local.tls: true
tipi-dashboard:
build:
context: .
dockerfile: Dockerfile
args:
- SENTRY_DISABLE_AUTO_UPLOAD=true
- TIPI_VERSION=0.0.0
container_name: tipi-dashboard
depends_on:
tipi-db:
condition: service_healthy
tipi-redis:
condition: service_healthy
tipi-worker:
condition: service_healthy
env_file:
- .env
environment:
NODE_ENV: production
TIPI_VERSION: 0.0.0
NEXT_PUBLIC_TIPI_VERSION: 0.0.0
networks:
- tipi_main_network
ports:
- 3000:3000
volumes:
- ./.env:/runtipi/.env:ro
- ./state:/runtipi/state
- ./repos:/runtipi/repos:ro
- ./apps:/runtipi/apps
- ./logs:/app/logs
- ./traefik:/runtipi/traefik
- ${STORAGE_PATH:-.}:/app/storage
labels:
traefik.enable: true
traefik.http.services.dashboard.loadbalancer.server.port: 3000
traefik.http.middlewares.redirect-to-https.redirectscheme.scheme: https
# Local ip
traefik.http.routers.dashboard.rule: PathPrefix("/")
traefik.http.routers.dashboard.service: dashboard
traefik.http.routers.dashboard.entrypoints: web
# Local domain
traefik.http.routers.dashboard-local-insecure.rule: Host(`${LOCAL_DOMAIN}`)
traefik.http.routers.dashboard-local-insecure.entrypoints: web
traefik.http.routers.dashboard-local-insecure.service: dashboard
traefik.http.routers.dashboard-local-insecure.middlewares: redirect-to-https
# secure
traefik.http.routers.dashboard-local.rule: Host(`${LOCAL_DOMAIN}`)
traefik.http.routers.dashboard-local.entrypoints: websecure
traefik.http.routers.dashboard-local.tls: true
traefik.http.routers.dashboard-local.service: dashboard
networks:
tipi_main_network:
driver: bridge

View File

@ -6,8 +6,8 @@ import { execRemoteCommand } from './write-remote-file';
export const setSettings = async (settings: z.infer<typeof settingsSchema>) => {
if (process.env.REMOTE === 'true') {
await execRemoteCommand(`mkdir -p ./runtipi/state`);
await execRemoteCommand(`echo '${JSON.stringify(settings)}' > ./runtipi/state/settings.json`);
await execRemoteCommand(`mkdir -p ./data/state`);
await execRemoteCommand(`echo '${JSON.stringify(settings)}' > ./data/state/settings.json`);
} else {
// Create state folder if it doesn't exist
await promises.mkdir('./state', { recursive: true });
@ -18,7 +18,7 @@ export const setSettings = async (settings: z.infer<typeof settingsSchema>) => {
export const setPassowrdChangeRequest = async () => {
if (process.env.REMOTE === 'true') {
await execRemoteCommand('touch ./runtipi/state/password-change-request');
await execRemoteCommand('touch ./data/state/password-change-request');
} else {
await promises.writeFile('./state/password-change-request', '');
}
@ -26,7 +26,7 @@ export const setPassowrdChangeRequest = async () => {
export const unsetPasswordChangeRequest = async () => {
if (process.env.REMOTE === 'true') {
await execRemoteCommand('rm ./runtipi/state/password-change-request');
await execRemoteCommand('rm ./data/state/password-change-request');
} else if (await pathExists('./state/password-change-request')) {
await promises.unlink('./state/password-change-request');
}
@ -34,11 +34,11 @@ export const unsetPasswordChangeRequest = async () => {
export const setWelcomeSeen = async (seen: boolean) => {
if (seen && process.env.REMOTE === 'true') {
return execRemoteCommand('touch ./runtipi/state/seen-welcome');
return execRemoteCommand('touch ./data/state/seen-welcome');
}
if (!seen && process.env.REMOTE === 'true') {
return execRemoteCommand('rm ./runtipi/state/seen-welcome');
return execRemoteCommand('rm ./data/state/seen-welcome');
}
if (seen && !(await pathExists('./state/seen-welcome'))) {

View File

@ -20,7 +20,7 @@
"start:dev-container": "./.devcontainer/filewatcher.sh && npm run start:dev",
"start:rc": "docker compose -f docker-compose.rc.yml --env-file .env up --build",
"start:dev": "docker compose -f docker-compose.dev.yml up --build",
"start:prod": "docker compose --env-file ./.env -f docker-compose.prod.yml up --build",
"start:prod": "docker compose -f docker-compose.prod.yml up --build",
"start:pg": "docker run --name test-db -p 5433:5432 -d --rm -e POSTGRES_PASSWORD=postgres postgres:14",
"version": "echo $npm_package_version",
"release:rc": "./scripts/deploy/release-rc.sh",

View File

@ -13,7 +13,6 @@ export const envSchema = z.object({
redisPassword: z.string(),
architecture: z.nativeEnum(ARCHITECTURES),
dnsIp: z.string().ip().trim(),
rootFolder: z.string(),
internalIp: z.string(),
version: z.string(),
jwtSecret: z.string(),
@ -21,7 +20,7 @@ export const envSchema = z.object({
appsRepoUrl: z.string().url().trim(),
domain: z.string().trim(),
localDomain: z.string().trim(),
storagePath: z
appDataDirPath: z
.string()
.trim()
.optional()
@ -98,7 +97,7 @@ export const settingsSchema = envSchema
postgresPort: true,
appsRepoUrl: true,
domain: true,
storagePath: true,
appDataDirPath: true,
localDomain: true,
demoMode: true,
guestDashboard: true,

View File

@ -1,2 +1,3 @@
export const ROOT_FOLDER = '/app';
export const STORAGE_FOLDER = '/storage';
export const APP_DIR = process.env.WORKER_APP_DIR || '/worker';
export const DATA_DIR = '/data';
export const APP_DATA_DIR = '/app-data';

View File

@ -15,9 +15,9 @@ import { logger } from '@/lib/logger';
import { AppExecutors, RepoExecutors } from './services';
import { SocketManager } from './lib/socket/SocketManager';
import { setupRoutes } from './api';
import { DATA_DIR } from './config';
const rootFolder = '/app';
const envFile = path.join(rootFolder, '.env');
const envFile = path.join(DATA_DIR, '.env');
const setupSentry = (release?: string) => {
Sentry.init({
@ -111,7 +111,7 @@ const main = async () => {
appExecutor.startAllApps();
const app = new Hono().basePath('/worker-api');
serve({ fetch: app.fetch, port: 3000 }, (info) => {
serve({ fetch: app.fetch, port: 5000 }, (info) => {
startWorker();
setupRoutes(app);

View File

@ -2,7 +2,7 @@ import path from 'path';
import { execAsync, pathExists } from '@runtipi/shared/node';
import { logger } from '@/lib/logger';
import { getEnv } from '@/lib/environment';
import { ROOT_FOLDER, STORAGE_FOLDER } from '@/config/constants';
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
const composeUp = async (args: string[]) => {
logger.info(`Running docker compose with args ${args.join(' ')}`);
@ -22,13 +22,13 @@ const composeUp = async (args: string[]) => {
*/
export const compose = async (appId: string, command: string) => {
const { arch, appsRepoId } = getEnv();
const appDataDirPath = path.join(STORAGE_FOLDER, 'app-data', appId);
const appDirPath = path.join(ROOT_FOLDER, 'apps', appId);
const appDataDirPath = path.join(APP_DATA_DIR, appId);
const appDirPath = path.join(DATA_DIR, 'apps', appId);
const args: string[] = [`--env-file ${path.join(appDataDirPath, 'app.env')}`];
// User custom env file
const userEnvFile = path.join(ROOT_FOLDER, 'user-config', appId, 'app.env');
const userEnvFile = path.join(DATA_DIR, 'user-config', appId, 'app.env');
if (await pathExists(userEnvFile)) {
args.push(`--env-file ${userEnvFile}`);
}
@ -41,11 +41,11 @@ export const compose = async (appId: string, command: string) => {
}
args.push(`-f ${composeFile}`);
const commonComposeFile = path.join(ROOT_FOLDER, 'repos', appsRepoId, 'apps', 'docker-compose.common.yml');
const commonComposeFile = path.join(DATA_DIR, 'repos', appsRepoId, 'apps', 'docker-compose.common.yml');
args.push(`-f ${commonComposeFile}`);
// User defined overrides
const userComposeFile = path.join(ROOT_FOLDER, 'user-config', appId, 'docker-compose.yml');
const userComposeFile = path.join(DATA_DIR, 'user-config', appId, 'docker-compose.yml');
if (await pathExists(userComposeFile)) {
args.push(`--file ${userComposeFile}`);
}

View File

@ -26,7 +26,7 @@ const environmentSchema = z
})
.transform((env) => {
const {
STORAGE_PATH = '/app',
STORAGE_PATH,
ARCHITECTURE,
ROOT_FOLDER_HOST,
APPS_REPO_ID,
@ -63,3 +63,4 @@ const environmentSchema = z
});
export const getEnv = () => environmentSchema.parse(process.env);

View File

@ -1,4 +1,5 @@
import { DATA_DIR } from '@/config/constants';
import { FileLogger } from '@runtipi/shared/node';
import path from 'node:path';
export const logger = new FileLogger('worker', path.join('/app', 'logs'), true);
export const logger = new FileLogger('worker', path.join(DATA_DIR, 'logs'), true);

View File

@ -2,7 +2,7 @@ import path from 'path';
import pg from 'pg';
import { migrate } from '@runtipi/postgres-migrations';
import { logger } from '@/lib/logger';
import { ROOT_FOLDER } from '@/config/constants';
import { APP_DIR } from '@/config/constants';
type MigrationParams = {
postgresHost: string;
@ -13,7 +13,7 @@ type MigrationParams = {
};
export const runPostgresMigrations = async (params: MigrationParams) => {
const assetsFolder = path.join(ROOT_FOLDER, 'assets');
const assetsFolder = path.join(APP_DIR, 'assets');
const { postgresHost, postgresDatabase, postgresUsername, postgresPassword, postgresPort } = params;

View File

@ -6,7 +6,7 @@ class SocketManager {
private io: Server | null = null;
init() {
const io = new Server(3001, { cors: { origin: '*' }, path: '/worker/socket.io' });
const io = new Server(5001, { cors: { origin: '*' }, path: '/worker/socket.io' });
io.on('connection', (socket) => {
socket.on('disconnect', () => {});

View File

@ -8,7 +8,7 @@ import { envMapToString, envStringToMap, settingsSchema } from '@runtipi/shared'
import { execAsync, pathExists } from '@runtipi/shared/node';
import { logger } from '../logger/logger';
import { getRepoHash } from '../../services/repo/repo.helpers';
import { ROOT_FOLDER } from '@/config/constants';
import { APP_DATA_DIR, APP_DIR, DATA_DIR } from '@/config/constants';
type EnvKeys =
| 'APPS_REPO_ID'
@ -23,7 +23,7 @@ type EnvKeys =
| 'NGINX_PORT'
| 'NGINX_PORT_SSL'
| 'DOMAIN'
| 'STORAGE_PATH'
| 'APP_DATA_DIR'
| 'POSTGRES_PORT'
| 'POSTGRES_HOST'
| 'POSTGRES_DBNAME'
@ -48,7 +48,7 @@ const DEFAULT_REPO_URL = 'https://github.com/runtipi/runtipi-appstore';
* Reads and returns the generated seed
*/
const getSeed = async () => {
const seedFilePath = path.join(ROOT_FOLDER, 'state', 'seed');
const seedFilePath = path.join(DATA_DIR, 'state', 'seed');
if (!(await pathExists(seedFilePath))) {
throw new Error('Seed file not found');
@ -75,11 +75,11 @@ const deriveEntropy = async (entropy: string) => {
* Generates a random seed if it does not exist yet
*/
const generateSeed = async () => {
if (!(await pathExists(path.join(ROOT_FOLDER, 'state', 'seed')))) {
if (!(await pathExists(path.join(DATA_DIR, 'state', 'seed')))) {
const randomBytes = crypto.randomBytes(32);
const seed = randomBytes.toString('hex');
await fs.promises.writeFile(path.join(ROOT_FOLDER, 'state', 'seed'), seed);
await fs.promises.writeFile(path.join(DATA_DIR, 'state', 'seed'), seed);
}
};
@ -99,9 +99,9 @@ const getArchitecture = () => {
* Generates a valid .env file from the settings.json file
*/
export const generateSystemEnvFile = async () => {
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'state'), { recursive: true });
const settingsFilePath = path.join(ROOT_FOLDER, 'state', 'settings.json');
const envFilePath = path.join(ROOT_FOLDER, '.env');
await fs.promises.mkdir(path.join(DATA_DIR, 'state'), { recursive: true });
const settingsFilePath = path.join(DATA_DIR, 'state', 'settings.json');
const envFilePath = path.join(DATA_DIR, '.env');
if (!(await pathExists(envFilePath))) {
await fs.promises.writeFile(envFilePath, '');
@ -155,11 +155,11 @@ export const generateSystemEnvFile = async () => {
envMap.set('JWT_SECRET', jwtSecret);
envMap.set('DOMAIN', data.domain || envMap.get('DOMAIN') || 'example.com');
envMap.set('STORAGE_PATH', data.storagePath || envMap.get('STORAGE_PATH') || rootFolderHost);
envMap.set('POSTGRES_HOST', 'tipi-db');
envMap.set('POSTGRES_HOST', 'runtipi-db');
envMap.set('POSTGRES_DBNAME', 'tipi');
envMap.set('POSTGRES_USERNAME', 'tipi');
envMap.set('POSTGRES_PORT', String(5432));
envMap.set('REDIS_HOST', 'tipi-redis');
envMap.set('REDIS_HOST', 'runtipi-redis');
envMap.set('DEMO_MODE', typeof data.demoMode === 'boolean' || typeof data.demoMode === 'string' ? String(data.demoMode) : envMap.get('DEMO_MODE') || 'false');
envMap.set('GUEST_DASHBOARD', typeof data.guestDashboard === 'boolean' || typeof data.guestDashboard === 'string' ? String(data.guestDashboard) : envMap.get('GUEST_DASHBOARD') || 'false');
envMap.set('LOCAL_DOMAIN', data.localDomain || envMap.get('LOCAL_DOMAIN') || 'tipi.lan');
@ -183,56 +183,56 @@ export const generateSystemEnvFile = async () => {
*/
export const copySystemFiles = async (envMap: Map<EnvKeys, string>) => {
// Remove old unused files
if (await pathExists(path.join(ROOT_FOLDER, 'scripts'))) {
if (await pathExists(path.join(DATA_DIR, 'scripts'))) {
logger.info('Removing old scripts folder');
await fs.promises.rmdir(path.join(ROOT_FOLDER, 'scripts'), { recursive: true });
await fs.promises.rmdir(path.join(DATA_DIR, 'scripts'), { recursive: true });
}
const assetsFolder = path.join(ROOT_FOLDER, 'assets');
const assetsFolder = path.join(APP_DIR, 'assets');
// Copy traefik folder from assets
logger.info('Creating traefik folders');
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'traefik', 'dynamic'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'traefik', 'shared'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'traefik', 'tls'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'traefik', 'dynamic'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'traefik', 'shared'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'traefik', 'tls'), { recursive: true });
if (envMap.get('PERSIST_TRAEFIK_CONFIG') === 'true') {
logger.warn('Skipping the copy of traefik files because persistTraefikConfig is set to true');
} else {
logger.info('Copying traefik files');
await fs.promises.copyFile(path.join(assetsFolder, 'traefik', 'traefik.yml'), path.join(ROOT_FOLDER, 'traefik', 'traefik.yml'));
await fs.promises.copyFile(path.join(assetsFolder, 'traefik', 'dynamic', 'dynamic.yml'), path.join(ROOT_FOLDER, 'traefik', 'dynamic', 'dynamic.yml'));
await fs.promises.copyFile(path.join(assetsFolder, 'traefik', 'traefik.yml'), path.join(DATA_DIR, 'traefik', 'traefik.yml'));
await fs.promises.copyFile(path.join(assetsFolder, 'traefik', 'dynamic', 'dynamic.yml'), path.join(DATA_DIR, 'traefik', 'dynamic', 'dynamic.yml'));
}
// Create base folders
logger.info('Creating base folders');
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'apps'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'app-data'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'state'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'repos'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'apps'), { recursive: true });
await fs.promises.mkdir(APP_DATA_DIR, { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'state'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'repos'), { recursive: true });
// Create media folders
logger.info('Creating media folders');
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'torrents', 'watch'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'torrents', 'complete'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'torrents', 'incomplete'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'media', 'torrents', 'watch'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'media', 'torrents', 'complete'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'media', 'torrents', 'incomplete'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'usenet', 'watch'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'usenet', 'complete'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'usenet', 'incomplete'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'media', 'usenet', 'watch'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'media', 'usenet', 'complete'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'media', 'usenet', 'incomplete'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'downloads', 'watch'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'downloads', 'complete'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'downloads', 'incomplete'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'media', 'downloads', 'watch'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'media', 'downloads', 'complete'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'media', 'downloads', 'incomplete'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'data', 'books'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'data', 'comics'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'data', 'movies'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'data', 'music'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'data', 'tv'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'data', 'podcasts'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'data', 'images'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'data', 'roms'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'media', 'data', 'books'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'media', 'data', 'comics'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'media', 'data', 'movies'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'media', 'data', 'music'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'media', 'data', 'tv'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'media', 'data', 'podcasts'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'media', 'data', 'images'), { recursive: true });
await fs.promises.mkdir(path.join(DATA_DIR, 'media', 'data', 'roms'), { recursive: true });
};
/**

View File

@ -9,9 +9,9 @@ import { copyDataDir, generateEnvFile } from './app.helpers';
import { logger } from '@/lib/logger';
import { compose } from '@/lib/docker';
import { getEnv } from '@/lib/environment';
import { ROOT_FOLDER, STORAGE_FOLDER } from '@/config/constants';
import { SocketManager } from '@/lib/socket/SocketManager';
import { getDbClient } from '@/lib/db';
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
export class AppExecutors {
private readonly logger;
@ -38,10 +38,10 @@ export class AppExecutors {
private getAppPaths = (appId: string) => {
const { appsRepoId } = getEnv();
const appDataDirPath = path.join(STORAGE_FOLDER, 'app-data', appId);
const appDirPath = path.join(ROOT_FOLDER, 'apps', appId);
const appDataDirPath = path.join(APP_DATA_DIR, appId);
const appDirPath = path.join(DATA_DIR, 'apps', appId);
const configJsonPath = path.join(appDirPath, 'config.json');
const repoPath = path.join(ROOT_FOLDER, 'repos', appsRepoId, 'apps', appId);
const repoPath = path.join(DATA_DIR, 'repos', appsRepoId, 'apps', appId);
return { appDataDirPath, appDirPath, configJsonPath, repoPath };
};
@ -53,7 +53,7 @@ export class AppExecutors {
*/
private ensureAppDir = async (appId: string) => {
const { appDirPath, appDataDirPath, repoPath } = this.getAppPaths(appId);
const dockerFilePath = path.join(ROOT_FOLDER, 'apps', appId, 'docker-compose.yml');
const dockerFilePath = path.join(DATA_DIR, 'apps', appId, 'docker-compose.yml');
if (!(await pathExists(dockerFilePath))) {
// delete eventual app folder if exists
@ -104,7 +104,7 @@ export class AppExecutors {
const { appDirPath, repoPath, appDataDirPath } = this.getAppPaths(appId);
// Check if app exists in repo
const apps = await fs.promises.readdir(path.join(ROOT_FOLDER, 'repos', appsRepoId, 'apps'));
const apps = await fs.promises.readdir(path.join(DATA_DIR, 'repos', appsRepoId, 'apps'));
if (!apps.includes(appId)) {
this.logger.error(`App ${appId} not found in repo ${appsRepoId}`);

View File

@ -5,7 +5,7 @@ import { appInfoSchema, envMapToString, envStringToMap } from '@runtipi/shared';
import { pathExists, execAsync } from '@runtipi/shared/node';
import { generateVapidKeys, getAppEnvMap } from './env.helpers';
import { getEnv } from '@/lib/environment';
import { ROOT_FOLDER, STORAGE_FOLDER } from '@/config/constants';
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
/**
* This function generates a random string of the provided length by using the SHA-256 hash algorithm.
@ -17,7 +17,7 @@ import { ROOT_FOLDER, STORAGE_FOLDER } from '@/config/constants';
*/
const getEntropy = async (name: string, length: number) => {
const hash = crypto.createHash('sha256');
const seed = await fs.promises.readFile(path.join(ROOT_FOLDER, 'state', 'seed'));
const seed = await fs.promises.readFile(path.join(DATA_DIR, 'state', 'seed'));
hash.update(name + seed.toString());
return hash.digest('hex').substring(0, length);
@ -38,14 +38,14 @@ const getEntropy = async (name: string, length: number) => {
export const generateEnvFile = async (appId: string, config: Record<string, unknown>) => {
const { internalIp, storagePath, rootFolderHost } = getEnv();
const configFile = await fs.promises.readFile(path.join(ROOT_FOLDER, 'apps', appId, 'config.json'));
const configFile = await fs.promises.readFile(path.join(DATA_DIR, 'apps', appId, 'config.json'));
const parsedConfig = appInfoSchema.safeParse(JSON.parse(configFile.toString()));
if (!parsedConfig.success) {
throw new Error(`App ${appId} has invalid config.json file`);
}
const baseEnvFile = await fs.promises.readFile(path.join(ROOT_FOLDER, '.env'));
const baseEnvFile = await fs.promises.readFile(path.join(DATA_DIR, '.env'));
const envMap = envStringToMap(baseEnvFile.toString());
// Default always present env variables
@ -101,12 +101,12 @@ export const generateEnvFile = async (appId: string, config: Record<string, unkn
}
// Create app-data folder if it doesn't exist
const appDataDirectoryExists = await fs.promises.stat(path.join(STORAGE_FOLDER, 'app-data', appId)).catch(() => false);
const appDataDirectoryExists = await fs.promises.stat(path.join(APP_DATA_DIR, appId)).catch(() => false);
if (!appDataDirectoryExists) {
await fs.promises.mkdir(path.join(STORAGE_FOLDER, 'app-data', appId), { recursive: true });
await fs.promises.mkdir(path.join(APP_DATA_DIR, appId), { recursive: true });
}
await fs.promises.writeFile(path.join(STORAGE_FOLDER, 'app-data', appId, 'app.env'), envMapToString(envMap));
await fs.promises.writeFile(path.join(APP_DATA_DIR, appId, 'app.env'), envMapToString(envMap));
};
/**
@ -136,35 +136,35 @@ export const copyDataDir = async (id: string) => {
const envMap = await getAppEnvMap(id);
// return if app does not have a data directory
if (!(await pathExists(`${ROOT_FOLDER}/apps/${id}/data`))) {
if (!(await pathExists(`${DATA_DIR}/apps/${id}/data`))) {
return;
}
// Create app-data folder if it doesn't exist
if (!(await pathExists(`${STORAGE_FOLDER}/app-data/${id}/data`))) {
await fs.promises.mkdir(`${STORAGE_FOLDER}/app-data/${id}/data`, { recursive: true });
if (!(await pathExists(`${APP_DATA_DIR}/${id}/data`))) {
await fs.promises.mkdir(`${APP_DATA_DIR}/${id}/data`, { recursive: true });
}
const dataDir = await fs.promises.readdir(`${ROOT_FOLDER}/apps/${id}/data`);
const dataDir = await fs.promises.readdir(`${DATA_DIR}/apps/${id}/data`);
const processFile = async (file: string) => {
if (file.endsWith('.template')) {
const template = await fs.promises.readFile(`${ROOT_FOLDER}/apps/${id}/data/${file}`, 'utf-8');
const template = await fs.promises.readFile(`${DATA_DIR}/apps/${id}/data/${file}`, 'utf-8');
const renderedTemplate = renderTemplate(template, envMap);
await fs.promises.writeFile(`${STORAGE_FOLDER}/app-data/${id}/data/${file.replace('.template', '')}`, renderedTemplate);
await fs.promises.writeFile(`${APP_DATA_DIR}/${id}/data/${file.replace('.template', '')}`, renderedTemplate);
} else {
await fs.promises.copyFile(`${ROOT_FOLDER}/apps/${id}/data/${file}`, `${STORAGE_FOLDER}/app-data/${id}/data/${file}`);
await fs.promises.copyFile(`${DATA_DIR}/apps/${id}/data/${file}`, `/app-data/${id}/data/${file}`);
}
};
const processDir = async (p: string) => {
await fs.promises.mkdir(`${STORAGE_FOLDER}/app-data/${id}/data/${p}`, { recursive: true });
const files = await fs.promises.readdir(`${ROOT_FOLDER}/apps/${id}/data/${p}`);
await fs.promises.mkdir(`${APP_DATA_DIR}/${id}/data/${p}`, { recursive: true });
const files = await fs.promises.readdir(`${DATA_DIR}/apps/${id}/data/${p}`);
await Promise.all(
files.map(async (file) => {
const fullPath = `${ROOT_FOLDER}/apps/${id}/data/${p}/${file}`;
const fullPath = `${DATA_DIR}/apps/${id}/data/${p}/${file}`;
if ((await fs.promises.lstat(fullPath)).isDirectory()) {
await processDir(`${p}/${file}`);
@ -177,7 +177,7 @@ export const copyDataDir = async (id: string) => {
await Promise.all(
dataDir.map(async (file) => {
const fullPath = `${ROOT_FOLDER}/apps/${id}/data/${file}`;
const fullPath = `${DATA_DIR}/apps/${id}/data/${file}`;
if ((await fs.promises.lstat(fullPath)).isDirectory()) {
await processDir(file);
@ -188,7 +188,7 @@ export const copyDataDir = async (id: string) => {
);
// Remove any .gitkeep files from the app-data folder at any level
if (await pathExists(`${STORAGE_FOLDER}/app-data/${id}/data`)) {
await execAsync(`find ${STORAGE_FOLDER}/app-data/${id}/data -name .gitkeep -delete`).catch(() => {});
if (await pathExists(`${APP_DATA_DIR}/${id}/data`)) {
await execAsync(`find ${APP_DATA_DIR}/${id}/data -name .gitkeep -delete`).catch(() => {});
}
};

View File

@ -1,7 +1,7 @@
import webpush from 'web-push';
import fs from 'fs';
import path from 'path';
import { STORAGE_FOLDER } from '@/config/constants';
import { APP_DATA_DIR } from '@/config/constants';
/**
* This function reads the env file for the app with the provided id and returns a Map containing the key-value pairs of the environment variables.
@ -11,7 +11,7 @@ import { STORAGE_FOLDER } from '@/config/constants';
*/
export const getAppEnvMap = async (appId: string) => {
try {
const envFile = await fs.promises.readFile(path.join(STORAGE_FOLDER, 'app-data', appId, 'app.env'));
const envFile = await fs.promises.readFile(path.join(APP_DATA_DIR, appId, 'app.env'));
const envVars = envFile.toString().split('\n');
const envVarsMap = new Map<string, string>();

View File

@ -3,6 +3,7 @@ import { execAsync, pathExists } from '@runtipi/shared/node';
import * as Sentry from '@sentry/node';
import { getRepoHash, getRepoBaseUrlAndBranch } from './repo.helpers';
import { logger } from '@/lib/logger';
import { DATA_DIR } from '@/config/constants';
export class RepoExecutors {
private readonly logger;
@ -36,7 +37,7 @@ export class RepoExecutors {
// We may have a potential branch computed in the hash (see getRepoBaseUrlAndBranch)
// so we do it here before splitting the url into repoUrl and branch
const repoHash = getRepoHash(url);
const repoPath = path.join('/app', 'repos', repoHash);
const repoPath = path.join(DATA_DIR, 'repos', repoHash);
if (await pathExists(repoPath)) {
this.logger.info(`Repo ${url} already exists`);
@ -75,7 +76,7 @@ export class RepoExecutors {
public pullRepo = async (repoUrl: string) => {
try {
const repoHash = getRepoHash(repoUrl);
const repoPath = path.join('/app', 'repos', repoHash);
const repoPath = path.join(DATA_DIR, 'repos', repoHash);
if (!(await pathExists(repoPath))) {
this.logger.info(`Repo ${repoUrl} does not exist`);

View File

@ -29,13 +29,13 @@ export class SystemExecutors {
const memResult = { total: 0, used: 0, available: 0 };
try {
const memInfo = await fs.promises.readFile('/host/proc/meminfo');
const memInfo = await fs.promises.readFile('/mnt/host/proc/meminfo');
memResult.total = Number(memInfo.toString().match(/MemTotal:\s+(\d+)/)?.[1] ?? 0) * 1024;
memResult.available = Number(memInfo.toString().match(/MemAvailable:\s+(\d+)/)?.[1] ?? 0) * 1024;
memResult.used = memResult.total - memResult.available;
} catch (e) {
this.logger.error(`Unable to read /host/proc/meminfo: ${e}`);
this.logger.error(`Unable to read /mnt/host/proc/meminfo: ${e}`);
}
const [disk0] = await si.fsSize();

View File

@ -16,7 +16,7 @@ export type SettingsFormValues = {
internalIp?: string;
appsRepoUrl?: string;
domain?: string;
storagePath?: string;
appDataDirPath?: string;
localDomain?: string;
guestDashboard?: boolean;
allowAutoThemes?: boolean;
@ -246,18 +246,18 @@ export const SettingsForm = (props: IProps) => {
</div>
<div className="mb-3">
<Input
{...register('storagePath')}
{...register('appDataDirPath')}
label={
<>
{t('SETTINGS_GENERAL_STORAGE_PATH')}
{t('SETTINGS_GENERAL_APP_DATA_DIR')}
<Tooltip className="tooltip" anchorSelect=".storage-path-hint">
{t('SETTINGS_GENERAL_STORAGE_PATH_HINT')}
{t('SETTINGS_GENERAL_APP_DATA_DIR_HINT')}
</Tooltip>
<span className={clsx('ms-1 form-help storage-path-hint')}>?</span>
</>
}
error={errors.storagePath?.message}
placeholder={t('SETTINGS_GENERAL_STORAGE_PATH')}
error={errors.appDataDirPath?.message}
placeholder={t('SETTINGS_GENERAL_APP_DATA_DIR')}
/>
</div>
<div className="mb-3">

View File

@ -3,6 +3,7 @@ import { TipiConfig } from '@/server/core/TipiConfig/TipiConfig';
import { pathExists } from '@runtipi/shared/node';
import fs from 'fs-extra';
import path from 'path';
import { APP_DIR, DATA_DIR } from 'src/config';
export async function GET(request: Request) {
try {
@ -13,17 +14,10 @@ export async function GET(request: Request) {
return new Response('Not found', { status: 404 });
}
const defaultFilePath = path.join(TipiConfig.getConfig().rootFolder, 'apps', id, 'metadata', 'logo.jpg');
const appRepoFilePath = path.join(
TipiConfig.getConfig().rootFolder,
'repos',
TipiConfig.getConfig().appsRepoId,
'apps',
id,
'metadata',
'logo.jpg',
);
let filePath = path.join('/app', 'public', 'app-not-found.jpg');
const defaultFilePath = path.join(DATA_DIR, 'apps', id, 'metadata', 'logo.jpg');
const appRepoFilePath = path.join(DATA_DIR, 'repos', TipiConfig.getConfig().appsRepoId, 'apps', id, 'metadata', 'logo.jpg');
let filePath = path.join(APP_DIR, 'public', 'app-not-found.jpg');
if (await pathExists(defaultFilePath)) {
filePath = defaultFilePath;

View File

@ -1,14 +1,14 @@
import * as Sentry from '@sentry/nextjs';
import { getUserFromCookie } from '@/server/common/session.helpers';
import { TipiConfig } from '@/server/core/TipiConfig/TipiConfig';
import fs from 'fs-extra';
import { APP_DIR } from 'src/config';
export async function GET() {
try {
const user = await getUserFromCookie();
if (user?.operator) {
const filePath = `${TipiConfig.getConfig().rootFolder}/traefik/tls/cert.pem`;
const filePath = `${APP_DIR}/traefik/tls/cert.pem`;
if (await fs.pathExists(filePath)) {
const file = await fs.promises.readFile(filePath);

View File

@ -7,7 +7,7 @@ export async function fetchSystemStatus() {
try {
const { jwtSecret } = TipiConfig.getConfig();
const token = jwt.sign({ skill: 'issue' }, jwtSecret);
const response = await fetch('http://tipi-worker:3000/worker-api/system-status', {
const response = await fetch('http://localhost:5000/worker-api/system-status', {
method: 'GET',
headers: { Authorization: `Bearer ${token}` },
});

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Local domain",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Domain name used for accessing apps in your local network. Your apps will be accessible at app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Settings updated. Restart your instance to apply new settings.",
"SETTINGS_GENERAL_STORAGE_PATH": "Storage path",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Storage path",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Update settings",
"SETTINGS_GENERAL_SUBTITLE": "This will update your settings.json file. Make sure you know what you are doing before updating these values.",
"SETTINGS_GENERAL_TAB_TITLE": "Settings",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Local domain",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Domain name used for accessing apps in your local network. Your apps will be accessible at app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Settings updated. Restart your instance to apply new settings.",
"SETTINGS_GENERAL_STORAGE_PATH": "Storage path",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Storage path",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Update settings",
"SETTINGS_GENERAL_SUBTITLE": "This will update your settings.json file. Make sure you know what you are doing before updating these values.",
"SETTINGS_GENERAL_TAB_TITLE": "Settings",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Domini local",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Nom de domini utilitzat per accedir a les aplicacions a la teva xarxa local. Les aplicacions seran accessibles a nom-app.domini-local.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Configuració actualitzada. Reinicia la teva instància per aplicar la nova configuració.",
"SETTINGS_GENERAL_STORAGE_PATH": "Ruta de l'emmagatzematge",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Ruta de l'emmagatzematge",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Update settings",
"SETTINGS_GENERAL_SUBTITLE": "Això actualitzarà el teu fitxer settings.json. Assegura't de saber el que estàs fent abans d'actualitzar aquests valors.",
"SETTINGS_GENERAL_TAB_TITLE": "Configuració",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Local domain",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Domain name used for accessing apps in your local network. Your apps will be accessible at app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Settings updated. Restart your instance to apply new settings.",
"SETTINGS_GENERAL_STORAGE_PATH": "Storage path",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Storage path",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Update settings",
"SETTINGS_GENERAL_SUBTITLE": "This will update your settings.json file. Make sure you know what you are doing before updating these values.",
"SETTINGS_GENERAL_TAB_TITLE": "Settings",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Local domain",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Domain name used for accessing apps in your local network. Your apps will be accessible at app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Settings updated. Restart your instance to apply new settings.",
"SETTINGS_GENERAL_STORAGE_PATH": "Storage path",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Storage path",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Update settings",
"SETTINGS_GENERAL_SUBTITLE": "This will update your settings.json file. Make sure you know what you are doing before updating these values.",
"SETTINGS_GENERAL_TAB_TITLE": "Settings",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Local domain",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Domain name used for accessing apps in your local network. Your apps will be accessible at app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Einstellungen aktualisiert. Starten Sie Ihre Instanz neu, um die Einstellungen zu übernehmen.",
"SETTINGS_GENERAL_STORAGE_PATH": "Speicherpfad",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Speicherpfad",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Einstellungen aktualisieren",
"SETTINGS_GENERAL_SUBTITLE": "Dadurch wird Ihre Datei settings.json aktualisiert. Stellen Sie sicher, dass Sie wissen, was Sie tun, bevor Sie diese Werte aktualisieren.",
"SETTINGS_GENERAL_TAB_TITLE": "Einstellungen",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Όνομα τοπικού τομέα",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Όνομα τομέα που χρησιμοποιείται για την πρόσβαση εφαρμογών στο τοπικό σας δίκτυο. Οι εφαρμογές σας θα είναι προσβάσιμες στο app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Οι ρυθμίσεις ενημερώθηκαν. Επανεκκινήστε για να εφαρμόσετε νέες ρυθμίσεις.",
"SETTINGS_GENERAL_STORAGE_PATH": "Διαδρομή αποθήκευσης",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Διαδρομή αποθήκευσης",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Ενημέρωση ρυθμίσεων",
"SETTINGS_GENERAL_SUBTITLE": "Αυτό θα ενημερώσει το αρχείο settings.json σας. Βεβαιωθείτε ότι γνωρίζετε τι κάνετε πριν από την ενημέρωση αυτών των τιμών.",
"SETTINGS_GENERAL_TAB_TITLE": "Ρυθμίσεις",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Local domain",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Domain name used for accessing apps in your local network. Your apps will be accessible at app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Settings updated. Restart your instance to apply new settings.",
"SETTINGS_GENERAL_STORAGE_PATH": "Storage path",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Storage path",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Update settings",
"SETTINGS_GENERAL_SUBTITLE": "This will update your settings.json file. Make sure you know what you are doing before updating these values.",
"SETTINGS_GENERAL_TAB_TITLE": "Settings",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Local domain",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Domain name used for accessing apps in your local network. Your apps will be accessible at app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Settings updated. Restart your instance to apply new settings.",
"SETTINGS_GENERAL_STORAGE_PATH": "Storage path",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Storage path",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Update settings",
"SETTINGS_GENERAL_SUBTITLE": "This will update your settings.json file. Make sure you know what you are doing before updating these values.",
"SETTINGS_GENERAL_TAB_TITLE": "Settings",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Nombre de dominio local",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Nombre de dominio utilizado para acceder a las aplicaciones en tu red local. Tus aplicaciones serán accesibles en nombre-aplicación.dominio-local.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Ajustes actualizados. Reinicia tu instancia para aplicar los nuevos ajustes.",
"SETTINGS_GENERAL_STORAGE_PATH": "Ruta de almacenamiento",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Ruta al directorio de almacenamiento. Asegúrate de que sea una ruta absoluta y de que exista.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Ruta de almacenamiento",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Ruta al directorio de almacenamiento. Asegúrate de que sea una ruta absoluta y de que exista.",
"SETTINGS_GENERAL_SUBMIT": "Actualizar ajustes",
"SETTINGS_GENERAL_SUBTITLE": "Esto actualizará el archivo settings.json. Asegúrate de saber lo que estás haciendo antes de actualizar estos valores.",
"SETTINGS_GENERAL_TAB_TITLE": "Configuración",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Local domain",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Domain name used for accessing apps in your local network. Your apps will be accessible at app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Settings updated. Restart your instance to apply new settings.",
"SETTINGS_GENERAL_STORAGE_PATH": "Storage path",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Storage path",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Update settings",
"SETTINGS_GENERAL_SUBTITLE": "This will update your settings.json file. Make sure you know what you are doing before updating these values.",
"SETTINGS_GENERAL_TAB_TITLE": "Settings",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Domaine local",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Nom de domaine utilisé pour accéder aux applications dans votre réseau local. Vos applications seront accessibles sur app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Paramètres mis à jour. Redémarrez votre instance pour appliquer de nouveaux paramètres.",
"SETTINGS_GENERAL_STORAGE_PATH": "Répertoire de stockage",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Chemin vers le répertoire de stockage. Assurez-vous qu'il s'agit d'un chemin absolu et qu'il existe.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Répertoire de stockage",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Chemin vers le répertoire de stockage. Assurez-vous qu'il s'agit d'un chemin absolu et qu'il existe.",
"SETTINGS_GENERAL_SUBMIT": "Paramètres de mise à jour",
"SETTINGS_GENERAL_SUBTITLE": "Cela va mettre à jour votre fichier settings.json. Assurez-vous de savoir ce que vous faites avant de mettre à jour ces valeurs.",
"SETTINGS_GENERAL_TAB_TITLE": "Paramètres",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Local domain",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Domain name used for accessing apps in your local network. Your apps will be accessible at app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Settings updated. Restart your instance to apply new settings.",
"SETTINGS_GENERAL_STORAGE_PATH": "Storage path",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Storage path",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Update settings",
"SETTINGS_GENERAL_SUBTITLE": "This will update your settings.json file. Make sure you know what you are doing before updating these values.",
"SETTINGS_GENERAL_TAB_TITLE": "Settings",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Helyi domain",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "A helyi hálózat alkalmazásaihoz való hozzáféréshez használt domain név. Az alkalmazások az app-name.local-domain oldalon érhetők el.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "A beállítások frissítve. Indítsa újra a példányt az új beállítások alkalmazásához.",
"SETTINGS_GENERAL_STORAGE_PATH": "A tárhely elérési útja",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "A tárolási könyvtár elérési útvonala. Győződjön meg róla, hogy abszolút elérési útvonalról van szó, és hogy létezik.",
"SETTINGS_GENERAL_APP_DATA_DIR": "A tárhely elérési útja",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "A tárolási könyvtár elérési útvonala. Győződjön meg róla, hogy abszolút elérési útvonalról van szó, és hogy létezik.",
"SETTINGS_GENERAL_SUBMIT": "Beállítások frissítése",
"SETTINGS_GENERAL_SUBTITLE": "Ez frissíti a settings.json fájlt. Mielőtt frissítené ezeket az értékeket, győződjön meg arról, hogy tudja, mit csinál.",
"SETTINGS_GENERAL_TAB_TITLE": "Beállítások",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Nome di dominio locale",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Nome di dominio utilizzato per accedere alle app nella tua rete locale. Le tue app saranno accessibili all'indirizzo nome-app.nome-di-dominio-locale.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Impostazioni aggiornate. Riavvia la tua istanza per applicare le nuove impostazioni.",
"SETTINGS_GENERAL_STORAGE_PATH": "Percorso di archiviazione",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Percorso di archiviazione",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Update settings",
"SETTINGS_GENERAL_SUBTITLE": "Questo aggiornerà il tuo file settings.json. Assicurati di sapere cosa stai facendo prima di aggiornare questi valori.",
"SETTINGS_GENERAL_TAB_TITLE": "Impostazioni",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "ローカルドメイン",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "ローカルネットワーク内のアプリにアクセスするために使用されるドメイン名。あなたのアプリはapp-name.local-domainからアクセスできます。",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "設定が更新されました。新しい設定を適用するにはインスタンスを再起動してください。",
"SETTINGS_GENERAL_STORAGE_PATH": "ストレージの場所",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "ストレージの場所",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Update settings",
"SETTINGS_GENERAL_SUBTITLE": "これにより、settings.json ファイルが更新されます。これらの値を更新する前に何をしているかを確認してください。",
"SETTINGS_GENERAL_TAB_TITLE": "設定",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Local domain",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Domain name used for accessing apps in your local network. Your apps will be accessible at app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Settings updated. Restart your instance to apply new settings.",
"SETTINGS_GENERAL_STORAGE_PATH": "Storage path",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Storage path",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Update settings",
"SETTINGS_GENERAL_SUBTITLE": "This will update your settings.json file. Make sure you know what you are doing before updating these values.",
"SETTINGS_GENERAL_TAB_TITLE": "Settings",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Local domain",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Domain name used for accessing apps in your local network. Your apps will be accessible at app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Settings updated. Restart your instance to apply new settings.",
"SETTINGS_GENERAL_STORAGE_PATH": "Storage path",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Storage path",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Update settings",
"SETTINGS_GENERAL_SUBTITLE": "This will update your settings.json file. Make sure you know what you are doing before updating these values.",
"SETTINGS_GENERAL_TAB_TITLE": "Settings",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Local domain",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Domain name used for accessing apps in your local network. Your apps will be accessible at app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Settings updated. Restart your instance to apply new settings.",
"SETTINGS_GENERAL_STORAGE_PATH": "Storage path",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Storage path",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Update settings",
"SETTINGS_GENERAL_SUBTITLE": "This will update your settings.json file. Make sure you know what you are doing before updating these values.",
"SETTINGS_GENERAL_TAB_TITLE": "Settings",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Local domain",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Domain name used for accessing apps in your local network. Your apps will be accessible at app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Ustawienia zaktualizowane. Uruchom ponownie instancję, aby zastosować nowe ustawienia.",
"SETTINGS_GENERAL_STORAGE_PATH": "Ścieżka przechowywania",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Ścieżka przechowywania",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Update settings",
"SETTINGS_GENERAL_SUBTITLE": "Spowoduje to aktualizację pliku settings.json. Upewnij się, że wiesz, co robisz przed aktualizacją tych wartości.",
"SETTINGS_GENERAL_TAB_TITLE": "Ustawienia",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Local domain",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Domain name used for accessing apps in your local network. Your apps will be accessible at app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Settings updated. Restart your instance to apply new settings.",
"SETTINGS_GENERAL_STORAGE_PATH": "Storage path",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Storage path",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Update settings",
"SETTINGS_GENERAL_SUBTITLE": "This will update your settings.json file. Make sure you know what you are doing before updating these values.",
"SETTINGS_GENERAL_TAB_TITLE": "Settings",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Domínio local",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Nome de domínio usado para acessar aplicativos na sua rede local. Seus aplicativos estarão acessíveis em nome de app.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Configurações atualizadas. Reinicie sua instância para aplicar novas configurações.",
"SETTINGS_GENERAL_STORAGE_PATH": "Caminho de armazenamento",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Caminho de armazenamento",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Atualizar as configurações",
"SETTINGS_GENERAL_SUBTITLE": "Isto irá atualizar seu arquivo settings.json. Certifique-se de saber o que está fazendo antes de atualizar esses valores.",
"SETTINGS_GENERAL_TAB_TITLE": "Configurações",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Local domain",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Domain name used for accessing apps in your local network. Your apps will be accessible at app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Setări actualizate. Reporniți instanța pentru a aplica noile setări.",
"SETTINGS_GENERAL_STORAGE_PATH": "Calea de stocare",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Calea de stocare",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Actualizează Setări",
"SETTINGS_GENERAL_SUBTITLE": "Acest lucru va actualiza fișierul settings.json. Asigură-te că știi ce faci înainte de a actualiza aceste valori.",
"SETTINGS_GENERAL_TAB_TITLE": "Setări",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Локальный домен",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Доменное имя, используемое для доступа к приложениям в вашей локальной сети. Ваши приложения будут доступны по адресу app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Настройки обновлены. Перезапустите сервер Tipi для применения новых настроек.",
"SETTINGS_GENERAL_STORAGE_PATH": "Путь хранилища",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Путь к каталогу хранения. Убедитесь, что это абсолютный путь и что он существует.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Путь хранилища",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Путь к каталогу хранения. Убедитесь, что это абсолютный путь и что он существует.",
"SETTINGS_GENERAL_SUBMIT": "Обновить настройки",
"SETTINGS_GENERAL_SUBTITLE": "Это обновит файл settings.json. Убедитесь, что вы знаете, что делаете перед обновлением этих значений.",
"SETTINGS_GENERAL_TAB_TITLE": "Настройки",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Local domain",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Domain name used for accessing apps in your local network. Your apps will be accessible at app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Settings updated. Restart your instance to apply new settings.",
"SETTINGS_GENERAL_STORAGE_PATH": "Storage path",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Storage path",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Update settings",
"SETTINGS_GENERAL_SUBTITLE": "This will update your settings.json file. Make sure you know what you are doing before updating these values.",
"SETTINGS_GENERAL_TAB_TITLE": "Settings",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Lokal domän",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Domännamn som används för att komma åt appar i ditt lokala nätverk. Apparna kommer att vara tillgängliga på app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Inställningar uppdaterade. Starta om din instans för att tillämpa nya inställningar.",
"SETTINGS_GENERAL_STORAGE_PATH": "Lagring sökväg",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Sökväg till lagringskatalogen. Se till att den är en absolut sökväg och att den finns.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Lagring sökväg",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Sökväg till lagringskatalogen. Se till att den är en absolut sökväg och att den finns.",
"SETTINGS_GENERAL_SUBMIT": "Uppdatera inställningar",
"SETTINGS_GENERAL_SUBTITLE": "Detta kommer att uppdatera din settings.json fil. Se till att du vet vad du gör innan du uppdaterar dessa värden.",
"SETTINGS_GENERAL_TAB_TITLE": "Inställningar",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Yerel alan adı",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Yerel ağınızdaki uygulamalara erişmek için kullanılan alan adı. Uygulamalarınıza app-name.local-domain adresinden erişilebilir.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Ayarlar güncellendi. Yeni ayarları uygulamak için sunucunuzu yeniden başlatın.",
"SETTINGS_GENERAL_STORAGE_PATH": "Depolama yolu",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Depolama yolu",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Ayarları güncelle",
"SETTINGS_GENERAL_SUBTITLE": "Bu, settings.json dosyanızı güncelleyecektir. Bu değerleri güncellemeden önce ne yaptığınızı bildiğinizden emin olun.",
"SETTINGS_GENERAL_TAB_TITLE": "Ayarlar",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Local domain",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Domain name used for accessing apps in your local network. Your apps will be accessible at app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Settings updated. Restart your instance to apply new settings.",
"SETTINGS_GENERAL_STORAGE_PATH": "Storage path",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Storage path",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Update settings",
"SETTINGS_GENERAL_SUBTITLE": "This will update your settings.json file. Make sure you know what you are doing before updating these values.",
"SETTINGS_GENERAL_TAB_TITLE": "Settings",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "Tên miền nội bộ",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "Tên miền để truy cập ứng dụng từ mạng nội bộ của bạn. Ứng dụng có thể được truy cập tại app-name.local-domain.",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "Cài đặt đã được cập nhật. Khởi động lại máy chủ của bạn để áp dụng cài đặt mới.",
"SETTINGS_GENERAL_STORAGE_PATH": "Đường dẫn lưu trữ dữ liệu",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "Đường dẫn lưu trữ dữ liệu",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Cập nhật cài đặt",
"SETTINGS_GENERAL_SUBTITLE": "Điều này sẽ thay đổi file setting.json của bạn. Hãy chắc chắn rằng bạn hiểu rõ mình đang làm gì trước khi thay đổi các giá trị này.",
"SETTINGS_GENERAL_TAB_TITLE": "Cài đặt",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "本地域名",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "访问本地网络应用所使用的域名。您的应用将可以在app-name.local-domain访问。",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "设置已更新。重新启动您的实例以应用新设置。",
"SETTINGS_GENERAL_STORAGE_PATH": "保存路径",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "保存路径",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Update settings",
"SETTINGS_GENERAL_SUBTITLE": "这将更新您的 settings.json 文件。在更新这些值之前,请确保您知道您正在做什么。",
"SETTINGS_GENERAL_TAB_TITLE": "设置",

View File

@ -237,8 +237,8 @@
"SETTINGS_GENERAL_LOCAL_DOMAIN": "本地域名",
"SETTINGS_GENERAL_LOCAL_DOMAIN_HINT": "訪問本地網絡應用所使用的域名。您的應用將可以在app-name.local-domain訪問。",
"SETTINGS_GENERAL_SETTINGS_UPDATED": "設置更新。 重新啟動您的實例以應用新設置。",
"SETTINGS_GENERAL_STORAGE_PATH": "儲存路徑",
"SETTINGS_GENERAL_STORAGE_PATH_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_APP_DATA_DIR": "儲存路徑",
"SETTINGS_GENERAL_APP_DATA_DIR_HINT": "Path to the storage directory. Make sure it is an absolute path and that it exists.",
"SETTINGS_GENERAL_SUBMIT": "Update settings",
"SETTINGS_GENERAL_SUBTITLE": "這將更新您的 settings.json 文件。 在更新這些值之前,請確保您知道自己在做什麼。",
"SETTINGS_GENERAL_TAB_TITLE": "設定",

3
src/config/constants.ts Normal file
View File

@ -0,0 +1,3 @@
export const APP_DIR = process.env.DASHBOARD_APP_DIR || '/dashboard';
export const DATA_DIR = '/data';
export const APP_DATA_DIR = '/app-data';

1
src/config/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './constants';

View File

@ -1,5 +1,6 @@
import fs from 'fs-extra';
import path from 'path';
import { DATA_DIR } from 'src/config';
import { createLogger, format, transports } from 'winston';
const { align, printf, timestamp, combine, colorize } = format;
@ -22,7 +23,7 @@ const combinedLogFormatDev = combine(
);
const productionLogger = () => {
const logsFolder = '/app/logs';
const logsFolder = `${DATA_DIR}/logs`;
try {
if (!fs.existsSync(logsFolder)) {
fs.mkdirSync(logsFolder);

View File

@ -24,7 +24,7 @@ describe('Test: getConfig', () => {
domain: faker.lorem.word(),
};
const MockFiles = {
'/runtipi/state/settings.json': JSON.stringify(settingsJson),
'/data/state/settings.json': JSON.stringify(settingsJson),
};
// @ts-expect-error - We are mocking fs
fs.__createMockFiles(MockFiles);
@ -78,7 +78,7 @@ describe('Test: setConfig', () => {
expect(config).toBeDefined();
expect(config.appsRepoUrl).toBe(randomWord);
const settingsJson = readJsonFile('/runtipi/state/settings.json') as { [key: string]: string };
const settingsJson = readJsonFile('/data/state/settings.json') as { [key: string]: string };
expect(settingsJson).toBeDefined();
expect(settingsJson.appsRepoUrl).toBe(randomWord);
@ -91,7 +91,7 @@ describe('Test: getSettings', () => {
const fakeSettings = {
appsRepoUrl: faker.internet.url(),
};
const MockFiles = { '/runtipi/state/settings.json': JSON.stringify(fakeSettings) };
const MockFiles = { '/data/state/settings.json': JSON.stringify(fakeSettings) };
// @ts-expect-error - We are mocking fs
fs.__createMockFiles(MockFiles);
@ -106,7 +106,7 @@ describe('Test: getSettings', () => {
it('It should return current config if settings.json has any invalid value', () => {
// arrange
const tipiConf = new TipiConfigClass(0);
const MockFiles = { '/runtipi/state/settings.json': JSON.stringify({ appsRepoUrl: 10 }) };
const MockFiles = { '/data/state/settings.json': JSON.stringify({ appsRepoUrl: 10 }) };
// @ts-expect-error - We are mocking fs
fs.__createMockFiles(MockFiles);
@ -129,7 +129,7 @@ describe('Test: setSettings', () => {
// act
await new TipiConfigClass(0).setSettings(fakeSettings);
const settingsJson = readJsonFile('/runtipi/state/settings.json') as { [key: string]: string };
const settingsJson = readJsonFile('/data/state/settings.json') as { [key: string]: string };
// assert
expect(settingsJson).toBeDefined();
@ -142,7 +142,7 @@ describe('Test: setSettings', () => {
// act
new TipiConfigClass(0).setSettings(fakeSettings as object);
const settingsJson = (readJsonFile('/runtipi/state/settings.json') || {}) as { [key: string]: string };
const settingsJson = (readJsonFile('/data/state/settings.json') || {}) as { [key: string]: string };
// assert
expect(settingsJson).toBeDefined();

View File

@ -3,6 +3,7 @@ import { envSchema, envStringToMap, settingsSchema } from '@runtipi/shared';
import fs from 'fs-extra';
import nextConfig from 'next/config';
import * as Sentry from '@sentry/nextjs';
import { DATA_DIR } from 'src/config';
import { readJsonFile } from '../../common/fs.helpers';
import { Logger } from '../Logger';
@ -34,7 +35,7 @@ export class TipiConfigClass {
private genConfig() {
let envFile = '';
try {
envFile = fs.readFileSync('/runtipi/.env').toString();
envFile = fs.readFileSync(`${DATA_DIR}/.env`).toString();
} catch (e) {
Sentry.captureException(e);
Logger.error('❌ .env file not found');
@ -53,7 +54,6 @@ export class TipiConfigClass {
redisPassword: conf.REDIS_PASSWORD,
NODE_ENV: process.env.NODE_ENV || 'production',
architecture: conf.ARCHITECTURE || 'amd64',
rootFolder: '/runtipi',
internalIp: conf.INTERNAL_IP,
version: conf.TIPI_VERSION,
jwtSecret: conf.JWT_SECRET,
@ -62,7 +62,7 @@ export class TipiConfigClass {
domain: conf.DOMAIN,
localDomain: conf.LOCAL_DOMAIN,
dnsIp: conf.DNS_IP || '9.9.9.9',
storagePath: conf.STORAGE_PATH,
appDataDirPath: conf.APP_DATA_DIR,
demoMode: conf.DEMO_MODE,
guestDashboard: conf.GUEST_DASHBOARD,
allowErrorMonitoring: conf.ALLOW_ERROR_MONITORING,
@ -89,7 +89,7 @@ export class TipiConfigClass {
if (this.fileConfigCache && now - this.cacheTime < this.cacheTimeout) {
fileConfig = this.fileConfigCache;
} else {
const rawFileConfig = readJsonFile('/runtipi/state/settings.json') || {};
const rawFileConfig = readJsonFile(`${DATA_DIR}/state/settings.json`) || {};
const parsedFileConfig = settingsSchema.safeParse(rawFileConfig);
if (parsedFileConfig.success) {
@ -137,13 +137,13 @@ export class TipiConfigClass {
this.config = envSchema.parse(newConf);
if (writeFile) {
const currentJsonConf = readJsonFile('/runtipi/state/settings.json') || {};
const currentJsonConf = readJsonFile(`${DATA_DIR}/state/settings.json`) || {};
const parsedConf = envSchema.partial().parse(currentJsonConf);
parsedConf[key] = value;
const parsed = envSchema.partial().parse(parsedConf);
await fs.promises.writeFile('/runtipi/state/settings.json', JSON.stringify(parsed));
await fs.promises.writeFile(`${DATA_DIR}/state/settings.json`, JSON.stringify(parsed));
}
}
@ -159,7 +159,7 @@ export class TipiConfigClass {
return;
}
await fs.promises.writeFile('/runtipi/state/settings.json', JSON.stringify(parsed.data));
await fs.promises.writeFile(`${DATA_DIR}/state/settings.json`, JSON.stringify(parsed.data));
// Reset cache
this.cacheTime = 0;

View File

@ -5,6 +5,7 @@ import { fileExists, readdirSync, readFile, readJsonFile } from '../../common/fs
import { TipiConfig } from '../../core/TipiConfig';
import { Logger } from '../../core/Logger';
import { notEmpty } from '../../common/typescript.helpers';
import { DATA_DIR } from 'src/config';
/**
* This function checks the requirements for the app with the provided name.
@ -17,7 +18,7 @@ import { notEmpty } from '../../common/typescript.helpers';
* @throws Will throw an error if the app has an invalid config.json file or if the current system architecture is not supported by the app.
*/
export const checkAppRequirements = (appName: string) => {
const configFile = readJsonFile(`/runtipi/repos/${TipiConfig.getConfig().appsRepoId}/apps/${appName}/config.json`);
const configFile = readJsonFile(`${DATA_DIR}/repos/${TipiConfig.getConfig().appsRepoId}/apps/${appName}/config.json`);
const parsedConfig = appInfoSchema.safeParse(configFile);
if (!parsedConfig.success) {
@ -37,12 +38,12 @@ export const checkAppRequirements = (appName: string) => {
If the config.json file is invalid, it logs an error message.
*/
export const getAvailableApps = async () => {
if (!(await pathExists(`/runtipi/repos/${TipiConfig.getConfig().appsRepoId}/apps`))) {
if (!(await pathExists(`${DATA_DIR}/repos/${TipiConfig.getConfig().appsRepoId}/apps`))) {
Logger.error(`Apps repo ${TipiConfig.getConfig().appsRepoId} not found. Make sure your repo is configured correctly.`);
return [];
}
const appsDir = readdirSync(`/runtipi/repos/${TipiConfig.getConfig().appsRepoId}/apps`);
const appsDir = readdirSync(`${DATA_DIR}/repos/${TipiConfig.getConfig().appsRepoId}/apps`);
const skippedFiles = ['__tests__', 'docker-compose.common.yml', 'schema.json', '.DS_Store'];
@ -50,14 +51,14 @@ export const getAvailableApps = async () => {
.map((app) => {
if (skippedFiles.includes(app)) return null;
const configFile = readJsonFile(`/runtipi/repos/${TipiConfig.getConfig().appsRepoId}/apps/${app}/config.json`);
const configFile = readJsonFile(`${DATA_DIR}/repos/${TipiConfig.getConfig().appsRepoId}/apps/${app}/config.json`);
const parsedConfig = appInfoSchema.safeParse(configFile);
if (!parsedConfig.success) {
Logger.error(`App ${JSON.stringify(app)} has invalid config.json`);
Logger.error(JSON.stringify(parsedConfig.error, null, 2));
} else if (parsedConfig.data.available) {
const description = readFile(`/runtipi/repos/${TipiConfig.getConfig().appsRepoId}/apps/${parsedConfig.data.id}/metadata/description.md`);
const description = readFile(`${DATA_DIR}/repos/${TipiConfig.getConfig().appsRepoId}/apps/${parsedConfig.data.id}/metadata/description.md`);
return { ...parsedConfig.data, description };
}
@ -77,7 +78,7 @@ export const getAvailableApps = async () => {
* @param {string} id - The app id.
*/
export const getUpdateInfo = (id: string) => {
const repoConfig = readJsonFile(`/runtipi/repos/${TipiConfig.getConfig().appsRepoId}/apps/${id}/config.json`);
const repoConfig = readJsonFile(`${DATA_DIR}/repos/${TipiConfig.getConfig().appsRepoId}/apps/${id}/config.json`);
const parsedConfig = appInfoSchema.safeParse(repoConfig);
if (parsedConfig.success) {
@ -105,22 +106,22 @@ export const getAppInfo = (id: string, status?: App['status']) => {
// Check if app is installed
const installed = typeof status !== 'undefined' && status !== 'missing';
if (installed && fileExists(`/runtipi/apps/${id}/config.json`)) {
const configFile = readJsonFile(`/runtipi/apps/${id}/config.json`);
if (installed && fileExists(`${DATA_DIR}/apps/${id}/config.json`)) {
const configFile = readJsonFile(`${DATA_DIR}/apps/${id}/config.json`);
const parsedConfig = appInfoSchema.safeParse(configFile);
if (parsedConfig.success && parsedConfig.data.available) {
const description = readFile(`/runtipi/apps/${id}/metadata/description.md`);
const description = readFile(`${DATA_DIR}/apps/${id}/metadata/description.md`);
return { ...parsedConfig.data, description };
}
}
if (fileExists(`/runtipi/repos/${TipiConfig.getConfig().appsRepoId}/apps/${id}/config.json`)) {
const configFile = readJsonFile(`/runtipi/repos/${TipiConfig.getConfig().appsRepoId}/apps/${id}/config.json`);
if (fileExists(`/data/repos/${TipiConfig.getConfig().appsRepoId}/apps/${id}/config.json`)) {
const configFile = readJsonFile(`${DATA_DIR}/repos/${TipiConfig.getConfig().appsRepoId}/apps/${id}/config.json`);
const parsedConfig = appInfoSchema.safeParse(configFile);
if (parsedConfig.success && parsedConfig.data.available) {
const description = readFile(`/runtipi/repos/${TipiConfig.getConfig().appsRepoId}/apps/${id}/metadata/description.md`);
const description = readFile(`${DATA_DIR}/repos/${TipiConfig.getConfig().appsRepoId}/apps/${id}/metadata/description.md`);
return { ...parsedConfig.data, description };
}
}

View File

@ -11,6 +11,7 @@ import { tipiCache } from '@/server/core/TipiCache/TipiCache';
import { TipiConfig } from '../../core/TipiConfig';
import { fileExists, unlinkFile } from '../../common/fs.helpers';
import { decrypt, encrypt } from '../../utils/encryption';
import { DATA_DIR } from 'src/config';
type UsernamePasswordInput = {
username: string;
@ -294,7 +295,7 @@ export class AuthServiceClass {
await this.queries.updateUser(user.id, { password: hash, totpEnabled: false, totpSecret: null });
await unlinkFile(`/runtipi/state/password-change-request`);
await unlinkFile(`${DATA_DIR}/state/password-change-request`);
return { email: user.username };
};
@ -306,7 +307,7 @@ export class AuthServiceClass {
* @returns {boolean} - A boolean indicating if there is a password change request or not
*/
public static checkPasswordChangeRequest = () => {
if (fileExists(`/runtipi/state/password-change-request`)) {
if (fileExists(`${DATA_DIR}/state/password-change-request`)) {
return true;
}
@ -321,8 +322,8 @@ export class AuthServiceClass {
* @throws {Error} - If the file cannot be removed
*/
public static cancelPasswordChangeRequest = async () => {
if (fileExists(`/runtipi/state/password-change-request`)) {
await unlinkFile(`/runtipi/state/password-change-request`);
if (fileExists(`${DATA_DIR}/state/password-change-request`)) {
await unlinkFile(`${DATA_DIR}/state/password-change-request`);
}
return true;

View File

@ -4,6 +4,7 @@ import { tipiCache } from '@/server/core/TipiCache';
import { fileExists } from '../../common/fs.helpers';
import { Logger } from '../../core/Logger';
import { TipiConfig } from '../../core/TipiConfig';
import { DATA_DIR } from 'src/config';
export class SystemServiceClass {
/**
@ -42,12 +43,12 @@ export class SystemServiceClass {
};
public static hasSeenWelcome = async () => {
return fileExists(`/runtipi/state/seen-welcome`);
return fileExists(`${DATA_DIR}/state/seen-welcome`);
};
public static markSeenWelcome = async () => {
// Create file state/seen-welcome
await promises.writeFile(`/runtipi/state/seen-welcome`, '');
await promises.writeFile(`${DATA_DIR}/state/seen-welcome`, '');
return true;
};
}

View File

@ -4,6 +4,7 @@ import { eq } from 'drizzle-orm';
import { AppInfo, Architecture, appInfoSchema, APP_CATEGORIES } from '@runtipi/shared';
import { TestDatabase } from './test-utils';
import { appTable, AppStatus, App, NewApp } from '../db/schema';
import { APP_DATA_DIR, DATA_DIR } from 'src/config';
interface IProps {
installed?: boolean;
@ -34,10 +35,10 @@ const createAppConfig = (props?: Partial<AppInfo>) => {
});
const mockFiles: Record<string, string | string[]> = {};
mockFiles['/runtipi/.env'] = 'TEST=test';
mockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfoSchema.parse(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';
mockFiles['/data/.env'] = 'TEST=test';
mockFiles[`/data/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfoSchema.parse(appInfo));
mockFiles[`/data/repos/repo-id/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
mockFiles[`/data/repos/repo-id/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
// @ts-expect-error - custom mock method
fs.__applyMockFiles(mockFiles);
@ -101,10 +102,10 @@ const createApp = async (props: IProps, database: TestDatabase) => {
}
const mockFiles: Record<string, string | string[]> = {};
mockFiles['/runtipi/.env'] = 'TEST=test';
mockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfoSchema.parse(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';
mockFiles[`${DATA_DIR}/.env`] = 'TEST=test';
mockFiles[`${DATA_DIR}/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfoSchema.parse(appInfo));
mockFiles[`${DATA_DIR}/repos/repo-id/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
mockFiles[`${DATA_DIR}/repos/repo-id/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
let appEntity: App = {} as App;
if (installed) {
@ -122,9 +123,9 @@ const createApp = async (props: IProps, database: TestDatabase) => {
// eslint-disable-next-line prefer-destructuring
appEntity = insertedApp[0] as App;
mockFiles[`/app/storage/app-data/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
mockFiles[`/runtipi/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
mockFiles[`/runtipi/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
mockFiles[`${APP_DATA_DIR}/${appInfo.id}/app.env`] = 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test';
mockFiles[`${DATA_DIR}/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
mockFiles[`${DATA_DIR}/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
}
// @ts-expect-error - custom mock method
@ -146,12 +147,12 @@ const insertApp = async (data: Partial<NewApp>, appInfo: AppInfo, database: Test
const mockFiles: Record<string, string | string[]> = {};
if (data.status !== 'missing') {
mockFiles[`/app/storage/app-data/${values.id}/app.env`] = `TEST=test\nAPP_PORT=3000\n${Object.entries(data.config || {})
mockFiles[`app-data/${values.id}/app.env`] = `TEST=test\nAPP_PORT=3000\n${Object.entries(data.config || {})
.map(([key, value]) => `${key}=${value}`)
.join('\n')}`;
mockFiles[`/runtipi/apps/${values.id}/config.json`] = JSON.stringify(appInfo);
mockFiles[`/runtipi/apps/${values.id}/metadata/description.md`] = 'md desc';
mockFiles[`/runtipi/apps/${values.id}/docker-compose.yml`] = 'compose';
mockFiles[`${DATA_DIR}/apps/${values.id}/config.json`] = JSON.stringify(appInfo);
mockFiles[`${DATA_DIR}/apps/${values.id}/metadata/description.md`] = 'md desc';
mockFiles[`${DATA_DIR}/apps/${values.id}/docker-compose.yml`] = 'compose';
}
// @ts-expect-error - custom mock method

17
start.dev.sh Normal file
View File

@ -0,0 +1,17 @@
#!/bin/sh
# Start worker
pm2 start pnpm --name worker -- --filter @runtipi/worker -r dev
# Wait for http://localhost:5000/healthcheck to return OK
while true; do
if [[ "$(curl -s http://localhost:5000/worker-api/healthcheck)" == "OK" ]]; then
break
fi
sleep 1
done
pm2 start npm --name dashboard -- run dev
# Log apps realtime
pm2 logs --raw

20
start.prod.sh Normal file
View File

@ -0,0 +1,20 @@
#!/bin/sh
# Start worker
cd worker/
pm2 start index.js --ignore-watch="/worker" --name worker -- start
# Wait for http://localhost:5000/healthcheck to return OK with a maximum of 5 retries
while true; do
if [[ "$(curl -s http://localhost:5000/worker-api/healthcheck)" == "OK" ]]; then
break
fi
sleep 1
done
# Start apps
cd /dashboard
pm2 start npm --ignore-watch="/dashboard" --name dashboard -- run start
# Log apps realtime
pm2 logs