diff --git a/.dockerignore b/.dockerignore index 2bb2cc81..490133a7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -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 diff --git a/.env.example b/.env.example index bf6fb542..aba7cbb9 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore index 97792e72..0a57b099 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,6 @@ temp # Sentry Config File .sentryclirc + +# Runtipi data folder +/runtipi-data/ diff --git a/Dockerfile b/Dockerfile index e8cd0b86..cee7c36f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/Dockerfile.dev b/Dockerfile.dev index 1c3ef49f..ee23157d 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -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"] diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 4b26a0b3..4ab509c1 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -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 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index f64cd6fa..a2df718a 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -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 diff --git a/e2e/helpers/settings.ts b/e2e/helpers/settings.ts index 66042cb6..c78e0288 100644 --- a/e2e/helpers/settings.ts +++ b/e2e/helpers/settings.ts @@ -6,8 +6,8 @@ import { execRemoteCommand } from './write-remote-file'; export const setSettings = async (settings: z.infer) => { 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) => { 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'))) { diff --git a/package.json b/package.json index ef462a90..af942800 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/shared/src/schemas/env-schemas.ts b/packages/shared/src/schemas/env-schemas.ts index af5e8fab..ed296827 100644 --- a/packages/shared/src/schemas/env-schemas.ts +++ b/packages/shared/src/schemas/env-schemas.ts @@ -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, diff --git a/packages/worker/src/config/constants.ts b/packages/worker/src/config/constants.ts index 2a934ccf..927a44ef 100644 --- a/packages/worker/src/config/constants.ts +++ b/packages/worker/src/config/constants.ts @@ -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'; diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index 578d788a..e8e6df28 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -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); diff --git a/packages/worker/src/lib/docker/docker-helpers.ts b/packages/worker/src/lib/docker/docker-helpers.ts index d10685fc..70c3a40b 100644 --- a/packages/worker/src/lib/docker/docker-helpers.ts +++ b/packages/worker/src/lib/docker/docker-helpers.ts @@ -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}`); } diff --git a/packages/worker/src/lib/environment/environment.ts b/packages/worker/src/lib/environment/environment.ts index 77c93594..3f4002b3 100644 --- a/packages/worker/src/lib/environment/environment.ts +++ b/packages/worker/src/lib/environment/environment.ts @@ -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); + diff --git a/packages/worker/src/lib/logger/logger.ts b/packages/worker/src/lib/logger/logger.ts index 2963c628..4c062667 100644 --- a/packages/worker/src/lib/logger/logger.ts +++ b/packages/worker/src/lib/logger/logger.ts @@ -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); diff --git a/packages/worker/src/lib/migrations/run-migration.ts b/packages/worker/src/lib/migrations/run-migration.ts index 27c965c4..606d4a82 100644 --- a/packages/worker/src/lib/migrations/run-migration.ts +++ b/packages/worker/src/lib/migrations/run-migration.ts @@ -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; diff --git a/packages/worker/src/lib/socket/SocketManager.ts b/packages/worker/src/lib/socket/SocketManager.ts index 31dbaccd..a03c0731 100644 --- a/packages/worker/src/lib/socket/SocketManager.ts +++ b/packages/worker/src/lib/socket/SocketManager.ts @@ -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', () => {}); diff --git a/packages/worker/src/lib/system/system.helpers.ts b/packages/worker/src/lib/system/system.helpers.ts index 1892b5f5..32ce9d9e 100644 --- a/packages/worker/src/lib/system/system.helpers.ts +++ b/packages/worker/src/lib/system/system.helpers.ts @@ -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) => { // 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 }); }; /** diff --git a/packages/worker/src/services/app/app.executors.ts b/packages/worker/src/services/app/app.executors.ts index 3975d2dc..c26ca111 100644 --- a/packages/worker/src/services/app/app.executors.ts +++ b/packages/worker/src/services/app/app.executors.ts @@ -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}`); diff --git a/packages/worker/src/services/app/app.helpers.ts b/packages/worker/src/services/app/app.helpers.ts index 40d7ea53..e3f335fe 100644 --- a/packages/worker/src/services/app/app.helpers.ts +++ b/packages/worker/src/services/app/app.helpers.ts @@ -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) => { 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 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(() => {}); } }; diff --git a/packages/worker/src/services/app/env.helpers.ts b/packages/worker/src/services/app/env.helpers.ts index b46fa907..3f094198 100644 --- a/packages/worker/src/services/app/env.helpers.ts +++ b/packages/worker/src/services/app/env.helpers.ts @@ -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(); diff --git a/packages/worker/src/services/repo/repo.executors.ts b/packages/worker/src/services/repo/repo.executors.ts index 680aa8fa..93d4fe35 100644 --- a/packages/worker/src/services/repo/repo.executors.ts +++ b/packages/worker/src/services/repo/repo.executors.ts @@ -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`); diff --git a/packages/worker/src/services/system/system.executors.ts b/packages/worker/src/services/system/system.executors.ts index 7349dd4a..32ce6b55 100644 --- a/packages/worker/src/services/system/system.executors.ts +++ b/packages/worker/src/services/system/system.executors.ts @@ -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(); diff --git a/src/app/(dashboard)/settings/components/SettingsForm/SettingsForm.tsx b/src/app/(dashboard)/settings/components/SettingsForm/SettingsForm.tsx index 37b2f6f6..8d213887 100644 --- a/src/app/(dashboard)/settings/components/SettingsForm/SettingsForm.tsx +++ b/src/app/(dashboard)/settings/components/SettingsForm/SettingsForm.tsx @@ -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) => {
- {t('SETTINGS_GENERAL_STORAGE_PATH')} + {t('SETTINGS_GENERAL_APP_DATA_DIR')} - {t('SETTINGS_GENERAL_STORAGE_PATH_HINT')} + {t('SETTINGS_GENERAL_APP_DATA_DIR_HINT')} ? } - error={errors.storagePath?.message} - placeholder={t('SETTINGS_GENERAL_STORAGE_PATH')} + error={errors.appDataDirPath?.message} + placeholder={t('SETTINGS_GENERAL_APP_DATA_DIR')} />
diff --git a/src/app/api/app-image/route.ts b/src/app/api/app-image/route.ts index 9bd03c16..d53bdf49 100644 --- a/src/app/api/app-image/route.ts +++ b/src/app/api/app-image/route.ts @@ -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; diff --git a/src/app/api/certificate/route.ts b/src/app/api/certificate/route.ts index c3ab1285..4c002b6d 100644 --- a/src/app/api/certificate/route.ts +++ b/src/app/api/certificate/route.ts @@ -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); diff --git a/src/app/api/system-status/fetch-system-status.ts b/src/app/api/system-status/fetch-system-status.ts index aa25ec97..22b3d87b 100644 --- a/src/app/api/system-status/fetch-system-status.ts +++ b/src/app/api/system-status/fetch-system-status.ts @@ -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}` }, }); diff --git a/src/client/messages/af-ZA.json b/src/client/messages/af-ZA.json index 4407b346..dcfb55dd 100644 --- a/src/client/messages/af-ZA.json +++ b/src/client/messages/af-ZA.json @@ -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", diff --git a/src/client/messages/ar-SA.json b/src/client/messages/ar-SA.json index 4407b346..dcfb55dd 100644 --- a/src/client/messages/ar-SA.json +++ b/src/client/messages/ar-SA.json @@ -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", diff --git a/src/client/messages/ca-ES.json b/src/client/messages/ca-ES.json index aaf03d2e..d41963fe 100644 --- a/src/client/messages/ca-ES.json +++ b/src/client/messages/ca-ES.json @@ -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ó", diff --git a/src/client/messages/cs-CZ.json b/src/client/messages/cs-CZ.json index 4407b346..dcfb55dd 100644 --- a/src/client/messages/cs-CZ.json +++ b/src/client/messages/cs-CZ.json @@ -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", diff --git a/src/client/messages/da-DK.json b/src/client/messages/da-DK.json index 4407b346..dcfb55dd 100644 --- a/src/client/messages/da-DK.json +++ b/src/client/messages/da-DK.json @@ -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", diff --git a/src/client/messages/de-DE.json b/src/client/messages/de-DE.json index d1b4ab86..5b7d1ab2 100644 --- a/src/client/messages/de-DE.json +++ b/src/client/messages/de-DE.json @@ -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", diff --git a/src/client/messages/el-GR.json b/src/client/messages/el-GR.json index 967f82a2..2c385c6c 100644 --- a/src/client/messages/el-GR.json +++ b/src/client/messages/el-GR.json @@ -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": "Ρυθμίσεις", diff --git a/src/client/messages/en-US.json b/src/client/messages/en-US.json index 4407b346..dcfb55dd 100644 --- a/src/client/messages/en-US.json +++ b/src/client/messages/en-US.json @@ -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", diff --git a/src/client/messages/en.json b/src/client/messages/en.json index 4407b346..dcfb55dd 100644 --- a/src/client/messages/en.json +++ b/src/client/messages/en.json @@ -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", diff --git a/src/client/messages/es-ES.json b/src/client/messages/es-ES.json index 583e3de8..7080751a 100644 --- a/src/client/messages/es-ES.json +++ b/src/client/messages/es-ES.json @@ -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", diff --git a/src/client/messages/fi-FI.json b/src/client/messages/fi-FI.json index 4407b346..dcfb55dd 100644 --- a/src/client/messages/fi-FI.json +++ b/src/client/messages/fi-FI.json @@ -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", diff --git a/src/client/messages/fr-FR.json b/src/client/messages/fr-FR.json index c05ddaeb..735d9812 100644 --- a/src/client/messages/fr-FR.json +++ b/src/client/messages/fr-FR.json @@ -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", diff --git a/src/client/messages/he-IL.json b/src/client/messages/he-IL.json index 4407b346..dcfb55dd 100644 --- a/src/client/messages/he-IL.json +++ b/src/client/messages/he-IL.json @@ -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", diff --git a/src/client/messages/hu-HU.json b/src/client/messages/hu-HU.json index c9f496a8..32270f0d 100644 --- a/src/client/messages/hu-HU.json +++ b/src/client/messages/hu-HU.json @@ -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", diff --git a/src/client/messages/it-IT.json b/src/client/messages/it-IT.json index f9c37f48..77c4f637 100644 --- a/src/client/messages/it-IT.json +++ b/src/client/messages/it-IT.json @@ -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", diff --git a/src/client/messages/ja-JP.json b/src/client/messages/ja-JP.json index c56fea5a..2e74ee15 100644 --- a/src/client/messages/ja-JP.json +++ b/src/client/messages/ja-JP.json @@ -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": "設定", diff --git a/src/client/messages/ko-KR.json b/src/client/messages/ko-KR.json index 4407b346..dcfb55dd 100644 --- a/src/client/messages/ko-KR.json +++ b/src/client/messages/ko-KR.json @@ -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", diff --git a/src/client/messages/nl-NL.json b/src/client/messages/nl-NL.json index 2396e3d6..080183ef 100644 --- a/src/client/messages/nl-NL.json +++ b/src/client/messages/nl-NL.json @@ -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", diff --git a/src/client/messages/no-NO.json b/src/client/messages/no-NO.json index 4407b346..dcfb55dd 100644 --- a/src/client/messages/no-NO.json +++ b/src/client/messages/no-NO.json @@ -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", diff --git a/src/client/messages/pl-PL.json b/src/client/messages/pl-PL.json index 3ddee903..b9d89e77 100644 --- a/src/client/messages/pl-PL.json +++ b/src/client/messages/pl-PL.json @@ -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", diff --git a/src/client/messages/pt-BR.json b/src/client/messages/pt-BR.json index 4407b346..dcfb55dd 100644 --- a/src/client/messages/pt-BR.json +++ b/src/client/messages/pt-BR.json @@ -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", diff --git a/src/client/messages/pt-PT.json b/src/client/messages/pt-PT.json index dbd2f4fc..7334c63c 100644 --- a/src/client/messages/pt-PT.json +++ b/src/client/messages/pt-PT.json @@ -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", diff --git a/src/client/messages/ro-RO.json b/src/client/messages/ro-RO.json index 4ec6d84f..eb521414 100644 --- a/src/client/messages/ro-RO.json +++ b/src/client/messages/ro-RO.json @@ -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", diff --git a/src/client/messages/ru-RU.json b/src/client/messages/ru-RU.json index 5c8a02d8..41542059 100644 --- a/src/client/messages/ru-RU.json +++ b/src/client/messages/ru-RU.json @@ -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": "Настройки", diff --git a/src/client/messages/sr-SP.json b/src/client/messages/sr-SP.json index 4407b346..dcfb55dd 100644 --- a/src/client/messages/sr-SP.json +++ b/src/client/messages/sr-SP.json @@ -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", diff --git a/src/client/messages/sv-SE.json b/src/client/messages/sv-SE.json index cf4a150c..d74ad8f7 100644 --- a/src/client/messages/sv-SE.json +++ b/src/client/messages/sv-SE.json @@ -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", diff --git a/src/client/messages/tr-TR.json b/src/client/messages/tr-TR.json index 6d22ec93..226c63c8 100644 --- a/src/client/messages/tr-TR.json +++ b/src/client/messages/tr-TR.json @@ -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", diff --git a/src/client/messages/uk-UA.json b/src/client/messages/uk-UA.json index 4407b346..dcfb55dd 100644 --- a/src/client/messages/uk-UA.json +++ b/src/client/messages/uk-UA.json @@ -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", diff --git a/src/client/messages/vi-VN.json b/src/client/messages/vi-VN.json index 5d4a3c19..91c43749 100644 --- a/src/client/messages/vi-VN.json +++ b/src/client/messages/vi-VN.json @@ -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", diff --git a/src/client/messages/zh-CN.json b/src/client/messages/zh-CN.json index 2469ffcb..f1f1b9e3 100644 --- a/src/client/messages/zh-CN.json +++ b/src/client/messages/zh-CN.json @@ -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": "设置", diff --git a/src/client/messages/zh-TW.json b/src/client/messages/zh-TW.json index c00d6d52..c71a2346 100644 --- a/src/client/messages/zh-TW.json +++ b/src/client/messages/zh-TW.json @@ -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": "設定", diff --git a/src/config/constants.ts b/src/config/constants.ts new file mode 100644 index 00000000..345403e9 --- /dev/null +++ b/src/config/constants.ts @@ -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'; diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 00000000..c94f80f8 --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1 @@ +export * from './constants'; diff --git a/src/server/core/Logger/Logger.ts b/src/server/core/Logger/Logger.ts index 4da2d301..9df33d4c 100644 --- a/src/server/core/Logger/Logger.ts +++ b/src/server/core/Logger/Logger.ts @@ -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); diff --git a/src/server/core/TipiConfig/TipiConfig.test.ts b/src/server/core/TipiConfig/TipiConfig.test.ts index 79c5e022..0cfeb711 100644 --- a/src/server/core/TipiConfig/TipiConfig.test.ts +++ b/src/server/core/TipiConfig/TipiConfig.test.ts @@ -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(); diff --git a/src/server/core/TipiConfig/TipiConfig.ts b/src/server/core/TipiConfig/TipiConfig.ts index fc10d8b4..29744723 100644 --- a/src/server/core/TipiConfig/TipiConfig.ts +++ b/src/server/core/TipiConfig/TipiConfig.ts @@ -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; diff --git a/src/server/services/apps/apps.helpers.ts b/src/server/services/apps/apps.helpers.ts index d34f33d1..7618fb42 100644 --- a/src/server/services/apps/apps.helpers.ts +++ b/src/server/services/apps/apps.helpers.ts @@ -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 }; } } diff --git a/src/server/services/auth/auth.service.ts b/src/server/services/auth/auth.service.ts index 222f144a..5ab05bdf 100644 --- a/src/server/services/auth/auth.service.ts +++ b/src/server/services/auth/auth.service.ts @@ -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; diff --git a/src/server/services/system/system.service.ts b/src/server/services/system/system.service.ts index e79d219b..62e20c66 100644 --- a/src/server/services/system/system.service.ts +++ b/src/server/services/system/system.service.ts @@ -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; }; } diff --git a/src/server/tests/apps.factory.ts b/src/server/tests/apps.factory.ts index 942828e3..34ce0378 100644 --- a/src/server/tests/apps.factory.ts +++ b/src/server/tests/apps.factory.ts @@ -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) => { }); const mockFiles: Record = {}; - 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 = {}; - 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, appInfo: AppInfo, database: Test const mockFiles: Record = {}; 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 diff --git a/start.dev.sh b/start.dev.sh new file mode 100644 index 00000000..8b408be6 --- /dev/null +++ b/start.dev.sh @@ -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 diff --git a/start.prod.sh b/start.prod.sh new file mode 100644 index 00000000..a50db1c6 --- /dev/null +++ b/start.prod.sh @@ -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