From a15a4f602a138f909436e639c18b37894684efea Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Tue, 15 Aug 2023 22:53:24 +0200 Subject: [PATCH] refactor: import from packages/shared and remove duplicate code --- Dockerfile | 2 + Dockerfile.dev | 1 + esbuild.js | 2 +- next.config.mjs | 1 + nodemon.json | 2 +- src/client/components/AppTile/AppTile.tsx | 2 +- src/client/core/constants.ts | 2 +- src/client/core/types.ts | 3 - src/client/mocks/fixtures/app.fixtures.ts | 4 +- .../components/AppStoreTile/AppStoreTile.tsx | 2 +- .../CategorySelector/CategorySelector.tsx | 2 +- .../helpers/__tests__/table.helpers.test.ts | 14 +- .../modules/AppStore/helpers/table.helpers.ts | 2 +- .../modules/AppStore/helpers/table.types.ts | 2 +- .../modules/AppStore/state/appStoreState.ts | 2 +- .../components/AppActions/AppActions.test.tsx | 2 +- .../Apps/components/AppDetailsTabs.tsx | 2 +- .../InstallForm/InstallForm.test.tsx | 2 +- .../components/InstallForm/InstallForm.tsx | 2 +- .../InstallModal/InstallModal.test.tsx | 2 +- .../components/InstallModal/InstallModal.tsx | 2 +- .../modules/Apps/components/StopModal.tsx | 2 +- .../Apps/components/UninstallModal.tsx | 2 +- .../components/UpdateModal/UpdateModal.tsx | 2 +- .../Apps/components/UpdateSettingsModal.tsx | 2 +- .../Apps/utils/validators/validators.test.tsx | 2 +- .../Apps/utils/validators/validators.ts | 2 +- .../ResetPasswordContainer.tsx | 2 +- .../containers/DashboardContainer.test.tsx | 2 +- src/server/core/TipiConfig/TipiConfig.test.ts | 31 --- src/server/core/TipiConfig/TipiConfig.ts | 74 +---- src/server/routers/system/system.router.ts | 2 +- src/server/services/apps/apps.helpers.test.ts | 238 +--------------- src/server/services/apps/apps.helpers.ts | 259 +----------------- src/server/tests/apps.factory.ts | 4 +- 35 files changed, 57 insertions(+), 622 deletions(-) diff --git a/Dockerfile b/Dockerfile index e90cb0f4..a88552d8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,9 +13,11 @@ FROM builder_base AS builder WORKDIR /app COPY ./pnpm-lock.yaml ./ +COPY ./pnpm-workspace.yaml ./ RUN pnpm fetch --no-scripts COPY ./package*.json ./ +COPY ./packages ./packages RUN pnpm install -r --prefer-offline COPY ./src ./src diff --git a/Dockerfile.dev b/Dockerfile.dev index cd68b48a..d941ccd5 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -11,6 +11,7 @@ COPY ./pnpm-lock.yaml ./ RUN pnpm fetch --ignore-scripts COPY ./package*.json ./ +COPY ./packages ./packages RUN pnpm install -r --prefer-offline diff --git a/esbuild.js b/esbuild.js index 5da88459..3cea2f29 100644 --- a/esbuild.js +++ b/esbuild.js @@ -18,7 +18,7 @@ const onRebuild = () => { } }; -const included = ['express', 'pg', '@runtipi/postgres-migrations', 'connect-redis', 'express-session', 'drizzle-orm']; +const included = ['express', 'pg', '@runtipi/postgres-migrations', 'connect-redis', 'express-session', 'drizzle-orm', '@runtipi/shared']; const excluded = ['pg-native', '*required-server-files.json']; const external = Object.keys(pkg.dependencies || {}).filter((dep) => !included.includes(dep)); external.push(...excluded); diff --git a/next.config.mjs b/next.config.mjs index 87ee2fde..03c12cf8 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -3,6 +3,7 @@ const nextConfig = { swcMinify: true, output: 'standalone', reactStrictMode: true, + transpilePackages: ['@runtipi/shared'], serverRuntimeConfig: { INTERNAL_IP: process.env.INTERNAL_IP, TIPI_VERSION: process.env.TIPI_VERSION, diff --git a/nodemon.json b/nodemon.json index 97f146fa..d74f660b 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,5 +1,5 @@ { - "watch": ["src/server"], + "watch": ["src/server", "packages/shared"], "exec": "node ./esbuild.js dev", "ext": "js ts" } diff --git a/src/client/components/AppTile/AppTile.tsx b/src/client/components/AppTile/AppTile.tsx index c04eded6..3b88030e 100644 --- a/src/client/components/AppTile/AppTile.tsx +++ b/src/client/components/AppTile/AppTile.tsx @@ -4,11 +4,11 @@ import { IconDownload } from '@tabler/icons-react'; import { Tooltip } from 'react-tooltip'; import type { AppStatus as AppStatusEnum } from '@/server/db/schema'; import { useTranslations } from 'next-intl'; +import type { AppInfo } from '@runtipi/shared'; import { AppStatus } from '../AppStatus'; import { AppLogo } from '../AppLogo/AppLogo'; import { limitText } from '../../modules/AppStore/helpers/table.helpers'; import styles from './AppTile.module.scss'; -import { AppInfo } from '../../core/types'; type AppTileInfo = Pick; diff --git a/src/client/core/constants.ts b/src/client/core/constants.ts index 59eb941f..78f453f7 100644 --- a/src/client/core/constants.ts +++ b/src/client/core/constants.ts @@ -16,7 +16,7 @@ import { IconTool, IconUsers, } from '@tabler/icons-react'; -import { AppCategory } from './types'; +import { AppCategory } from '@runtipi/shared'; type AppCategoryEntry = { id: AppCategory; diff --git a/src/client/core/types.ts b/src/client/core/types.ts index 2801f19e..b0fc52ab 100644 --- a/src/client/core/types.ts +++ b/src/client/core/types.ts @@ -1,7 +1,4 @@ import * as Router from '../../server/routers/_app'; -export type { FormField, AppInfo } from '@/server/services/apps/apps.helpers'; -export type { AppCategory } from '@/server/services/apps/apps.types'; - export type App = Omit; export type AppWithInfo = Router.RouterOutput['app']['getApp']; diff --git a/src/client/mocks/fixtures/app.fixtures.ts b/src/client/mocks/fixtures/app.fixtures.ts index 695593d5..93a30d15 100644 --- a/src/client/mocks/fixtures/app.fixtures.ts +++ b/src/client/mocks/fixtures/app.fixtures.ts @@ -1,7 +1,7 @@ import { faker } from '@faker-js/faker'; import type { AppStatus } from '@/server/db/schema'; -import { APP_CATEGORIES } from '../../../server/services/apps/apps.types'; -import { App, AppCategory, AppInfo, AppWithInfo } from '../../core/types'; +import { AppInfo, AppCategory, APP_CATEGORIES } from '@runtipi/shared'; +import { App, AppWithInfo } from '../../core/types'; const randomCategory = (): AppCategory[] => { const categories = Object.values(APP_CATEGORIES); diff --git a/src/client/modules/AppStore/components/AppStoreTile/AppStoreTile.tsx b/src/client/modules/AppStore/components/AppStoreTile/AppStoreTile.tsx index 3df092d3..03dd8d81 100644 --- a/src/client/modules/AppStore/components/AppStoreTile/AppStoreTile.tsx +++ b/src/client/modules/AppStore/components/AppStoreTile/AppStoreTile.tsx @@ -2,8 +2,8 @@ import clsx from 'clsx'; import Link from 'next/link'; import React from 'react'; import { useTranslations } from 'next-intl'; +import { AppCategory } from '@runtipi/shared'; import { AppLogo } from '../../../../components/AppLogo/AppLogo'; -import { AppCategory } from '../../../../core/types'; import { colorSchemeForCategory, limitText } from '../../helpers/table.helpers'; import styles from './AppStoreTile.module.scss'; diff --git a/src/client/modules/AppStore/components/CategorySelector/CategorySelector.tsx b/src/client/modules/AppStore/components/CategorySelector/CategorySelector.tsx index fdb37eb4..5e08c50b 100644 --- a/src/client/modules/AppStore/components/CategorySelector/CategorySelector.tsx +++ b/src/client/modules/AppStore/components/CategorySelector/CategorySelector.tsx @@ -2,8 +2,8 @@ import React from 'react'; import Select, { SingleValue, OptionProps, ControlProps, components } from 'react-select'; import { Icon } from '@tabler/icons-react'; import { useTranslations } from 'next-intl'; +import { AppCategory } from '@runtipi/shared'; import { APP_CATEGORIES } from '../../../../core/constants'; -import { AppCategory } from '../../../../core/types'; import { useUIStore } from '../../../../state/uiStore'; const { Option, Control } = components; diff --git a/src/client/modules/AppStore/helpers/__tests__/table.helpers.test.ts b/src/client/modules/AppStore/helpers/__tests__/table.helpers.test.ts index f7243b4e..838aab16 100644 --- a/src/client/modules/AppStore/helpers/__tests__/table.helpers.test.ts +++ b/src/client/modules/AppStore/helpers/__tests__/table.helpers.test.ts @@ -3,33 +3,33 @@ import { limitText, sortTable } from '../table.helpers'; import { AppTableData } from '../table.types'; describe('sortTable function', () => { - const app = createAppConfig({ name: 'a', categories: ['social'] }); - const app2 = createAppConfig({ name: 'b', categories: ['network', 'automation'] }); - const app3 = createAppConfig({ name: 'c', categories: ['network'] }); + const app = createAppConfig({ id: 'a', name: 'a', categories: ['social'] }); + const app2 = createAppConfig({ id: 'b', name: 'B', categories: ['network', 'automation'] }); + const app3 = createAppConfig({ id: 'c', name: 'c', categories: ['network'] }); // Randomize the order of the apps const data: AppTableData = [app3, app, app2]; it('should sort by name in ascending order', () => { - const sortedData = sortTable({ data, direction: 'asc', col: 'name', search: '' }); + const sortedData = sortTable({ data, direction: 'asc', col: 'id', search: '' }); expect(sortedData).toEqual([app, app2, app3]); }); it('should sort by name in descending order', () => { - const sortedData = sortTable({ data, direction: 'desc', col: 'name', search: '' }); + const sortedData = sortTable({ data, direction: 'desc', col: 'id', search: '' }); expect(sortedData).toEqual([app3, app2, app]); }); it('should filter by search term', () => { - const sortedData = sortTable({ data, direction: 'asc', col: 'name', search: 'b' }); + const sortedData = sortTable({ data, direction: 'asc', col: 'id', search: 'b' }); expect(sortedData).toEqual([app2]); }); it('should filter by category', () => { - const sortedData = sortTable({ data, direction: 'asc', col: 'name', search: '', category: 'automation' }); + const sortedData = sortTable({ data, direction: 'asc', col: 'id', search: '', category: 'automation' }); expect(sortedData).toEqual([app2]); }); diff --git a/src/client/modules/AppStore/helpers/table.helpers.ts b/src/client/modules/AppStore/helpers/table.helpers.ts index 63956030..8144bca0 100644 --- a/src/client/modules/AppStore/helpers/table.helpers.ts +++ b/src/client/modules/AppStore/helpers/table.helpers.ts @@ -1,4 +1,4 @@ -import { AppCategory, AppInfo } from '../../../core/types'; +import { AppCategory, AppInfo } from '@runtipi/shared'; import { AppTableData } from './table.types'; type SortParams = { diff --git a/src/client/modules/AppStore/helpers/table.types.ts b/src/client/modules/AppStore/helpers/table.types.ts index 9b52456e..84b774a2 100644 --- a/src/client/modules/AppStore/helpers/table.types.ts +++ b/src/client/modules/AppStore/helpers/table.types.ts @@ -1,4 +1,4 @@ -import { AppInfo } from '../../../core/types'; +import { AppInfo } from '@runtipi/shared'; export type SortableColumns = keyof Pick; export type SortDirection = 'asc' | 'desc'; diff --git a/src/client/modules/AppStore/state/appStoreState.ts b/src/client/modules/AppStore/state/appStoreState.ts index 200c3945..d7f5cce8 100644 --- a/src/client/modules/AppStore/state/appStoreState.ts +++ b/src/client/modules/AppStore/state/appStoreState.ts @@ -1,5 +1,5 @@ import { create } from 'zustand'; -import { AppCategory } from '../../../core/types'; +import { AppCategory } from '@runtipi/shared'; import { SortableColumns } from '../helpers/table.types'; type Store = { diff --git a/src/client/modules/Apps/components/AppActions/AppActions.test.tsx b/src/client/modules/Apps/components/AppActions/AppActions.test.tsx index 74f4f388..83d3effa 100644 --- a/src/client/modules/Apps/components/AppActions/AppActions.test.tsx +++ b/src/client/modules/Apps/components/AppActions/AppActions.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; +import { AppInfo } from '@runtipi/shared'; import { AppActions } from './AppActions'; import { cleanup, fireEvent, render, screen, waitFor, userEvent } from '../../../../../../tests/test-utils'; -import { AppInfo } from '../../../../core/types'; afterEach(cleanup); diff --git a/src/client/modules/Apps/components/AppDetailsTabs.tsx b/src/client/modules/Apps/components/AppDetailsTabs.tsx index d122d8cf..ee0e405e 100644 --- a/src/client/modules/Apps/components/AppDetailsTabs.tsx +++ b/src/client/modules/Apps/components/AppDetailsTabs.tsx @@ -2,9 +2,9 @@ import { IconExternalLink } from '@tabler/icons-react'; import React from 'react'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useTranslations } from 'next-intl'; +import { AppInfo } from '@runtipi/shared'; import { DataGrid, DataGridItem } from '../../../components/ui/DataGrid'; import Markdown from '../../../components/Markdown/Markdown'; -import { AppInfo } from '../../../core/types'; interface IProps { info: AppInfo; diff --git a/src/client/modules/Apps/components/InstallForm/InstallForm.test.tsx b/src/client/modules/Apps/components/InstallForm/InstallForm.test.tsx index 016ff13b..b13abe4c 100644 --- a/src/client/modules/Apps/components/InstallForm/InstallForm.test.tsx +++ b/src/client/modules/Apps/components/InstallForm/InstallForm.test.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { faker } from '@faker-js/faker'; import { fromPartial } from '@total-typescript/shoehorn'; +import { FormField } from '@runtipi/shared'; import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils'; -import { FormField } from '../../../../core/types'; import { InstallForm } from './InstallForm'; describe('Test: InstallForm', () => { diff --git a/src/client/modules/Apps/components/InstallForm/InstallForm.tsx b/src/client/modules/Apps/components/InstallForm/InstallForm.tsx index 326d6aa5..d9989169 100644 --- a/src/client/modules/Apps/components/InstallForm/InstallForm.tsx +++ b/src/client/modules/Apps/components/InstallForm/InstallForm.tsx @@ -5,11 +5,11 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Tooltip } from 'react-tooltip'; import clsx from 'clsx'; import { useTranslations } from 'next-intl'; +import { type FormField, type AppInfo } from '@runtipi/shared'; import { Button } from '../../../../components/ui/Button'; import { Switch } from '../../../../components/ui/Switch'; import { Input } from '../../../../components/ui/Input'; import { validateAppConfig } from '../../utils/validators'; -import { type FormField, type AppInfo } from '../../../../core/types'; interface IProps { formFields: FormField[]; diff --git a/src/client/modules/Apps/components/InstallModal/InstallModal.test.tsx b/src/client/modules/Apps/components/InstallModal/InstallModal.test.tsx index 27ba1250..7fc9a363 100644 --- a/src/client/modules/Apps/components/InstallModal/InstallModal.test.tsx +++ b/src/client/modules/Apps/components/InstallModal/InstallModal.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; +import { AppInfo } from '@runtipi/shared'; import { InstallModal } from './InstallModal'; import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils'; -import { AppInfo } from '../../../../core/types'; describe('InstallModal', () => { const app = { diff --git a/src/client/modules/Apps/components/InstallModal/InstallModal.tsx b/src/client/modules/Apps/components/InstallModal/InstallModal.tsx index f1e93713..b86d4bf8 100644 --- a/src/client/modules/Apps/components/InstallModal/InstallModal.tsx +++ b/src/client/modules/Apps/components/InstallModal/InstallModal.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { Dialog, DialogContent, DialogDescription, DialogHeader } from '@/components/ui/Dialog'; import { useTranslations } from 'next-intl'; +import { AppInfo } from '@runtipi/shared'; import { InstallForm } from '../InstallForm'; -import { AppInfo } from '../../../../core/types'; import { FormValues } from '../InstallForm/InstallForm'; interface IProps { diff --git a/src/client/modules/Apps/components/StopModal.tsx b/src/client/modules/Apps/components/StopModal.tsx index 8001c51d..4d9f1891 100644 --- a/src/client/modules/Apps/components/StopModal.tsx +++ b/src/client/modules/Apps/components/StopModal.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog'; import { useTranslations } from 'next-intl'; +import { AppInfo } from '@runtipi/shared'; import { Button } from '../../../components/ui/Button'; -import { AppInfo } from '../../../core/types'; interface IProps { info: AppInfo; diff --git a/src/client/modules/Apps/components/UninstallModal.tsx b/src/client/modules/Apps/components/UninstallModal.tsx index 86134046..b3371fa6 100644 --- a/src/client/modules/Apps/components/UninstallModal.tsx +++ b/src/client/modules/Apps/components/UninstallModal.tsx @@ -2,8 +2,8 @@ import { IconAlertTriangle } from '@tabler/icons-react'; import React from 'react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog'; import { useTranslations } from 'next-intl'; +import { AppInfo } from '@runtipi/shared'; import { Button } from '../../../components/ui/Button'; -import { AppInfo } from '../../../core/types'; interface IProps { info: AppInfo; diff --git a/src/client/modules/Apps/components/UpdateModal/UpdateModal.tsx b/src/client/modules/Apps/components/UpdateModal/UpdateModal.tsx index 2df9d567..efce6a8a 100644 --- a/src/client/modules/Apps/components/UpdateModal/UpdateModal.tsx +++ b/src/client/modules/Apps/components/UpdateModal/UpdateModal.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog'; import { useTranslations } from 'next-intl'; +import { AppInfo } from '@runtipi/shared'; import { Button } from '../../../../components/ui/Button'; -import { AppInfo } from '../../../../core/types'; interface IProps { newVersion: string; diff --git a/src/client/modules/Apps/components/UpdateSettingsModal.tsx b/src/client/modules/Apps/components/UpdateSettingsModal.tsx index 86de3840..86a92d47 100644 --- a/src/client/modules/Apps/components/UpdateSettingsModal.tsx +++ b/src/client/modules/Apps/components/UpdateSettingsModal.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { Dialog, DialogContent, DialogDescription, DialogHeader } from '@/components/ui/Dialog'; import { useTranslations } from 'next-intl'; +import { AppInfo } from '@runtipi/shared'; import { InstallForm } from './InstallForm'; -import { AppInfo } from '../../../core/types'; import { FormValues } from './InstallForm/InstallForm'; interface IProps { diff --git a/src/client/modules/Apps/utils/validators/validators.test.tsx b/src/client/modules/Apps/utils/validators/validators.test.tsx index 8d7a1792..fdd554da 100644 --- a/src/client/modules/Apps/utils/validators/validators.test.tsx +++ b/src/client/modules/Apps/utils/validators/validators.test.tsx @@ -1,4 +1,4 @@ -import { FormField } from '../../../../core/types'; +import { FormField } from '@runtipi/shared'; import { validateAppConfig, validateField } from './validators'; describe('Test: validateField', () => { diff --git a/src/client/modules/Apps/utils/validators/validators.ts b/src/client/modules/Apps/utils/validators/validators.ts index 39c267bc..a3b7f104 100644 --- a/src/client/modules/Apps/utils/validators/validators.ts +++ b/src/client/modules/Apps/utils/validators/validators.ts @@ -1,6 +1,6 @@ import validator from 'validator'; import { useUIStore } from '@/client/state/uiStore'; -import type { FormField } from '../../../../core/types'; +import type { FormField } from '@runtipi/shared'; export const validateField = (field: FormField, value: string | undefined | boolean): string | undefined => { const { translator } = useUIStore.getState(); diff --git a/src/client/modules/Auth/containers/ResetPasswordContainer/ResetPasswordContainer.tsx b/src/client/modules/Auth/containers/ResetPasswordContainer/ResetPasswordContainer.tsx index 994217aa..3012a4b0 100644 --- a/src/client/modules/Auth/containers/ResetPasswordContainer/ResetPasswordContainer.tsx +++ b/src/client/modules/Auth/containers/ResetPasswordContainer/ResetPasswordContainer.tsx @@ -57,7 +57,7 @@ export const ResetPasswordContainer: React.FC = ({ isRequested }) => {

{t('auth.reset-password.title')}

{t('auth.reset-password.instructions')}

-          ./scripts/reset-password.sh
+          ./runtipi-cli reset-password
         
); diff --git a/src/client/modules/Dashboard/containers/DashboardContainer.test.tsx b/src/client/modules/Dashboard/containers/DashboardContainer.test.tsx index 3419adaa..d89e1835 100644 --- a/src/client/modules/Dashboard/containers/DashboardContainer.test.tsx +++ b/src/client/modules/Dashboard/containers/DashboardContainer.test.tsx @@ -22,6 +22,6 @@ describe('Test: Dashboard', () => { }, }; - render(); + render(); }); }); diff --git a/src/server/core/TipiConfig/TipiConfig.test.ts b/src/server/core/TipiConfig/TipiConfig.test.ts index 3d950c6d..0755a9e9 100644 --- a/src/server/core/TipiConfig/TipiConfig.test.ts +++ b/src/server/core/TipiConfig/TipiConfig.test.ts @@ -5,37 +5,6 @@ import { readJsonFile } from '../../common/fs.helpers'; jest.mock('fs-extra'); -jest.mock('next/config', () => - jest.fn(() => ({ - serverRuntimeConfig: { - DNS_IP: '1.1.1.1', - }, - })), -); - -// eslint-disable-next-line -import nextConfig from 'next/config'; - -describe('Test: process.env', () => { - it('should return config from .env', () => { - const config = new TipiConfig().getConfig(); - - expect(config).toBeDefined(); - expect(config.dnsIp).toBe('1.1.1.1'); - }); - - it('should throw an error if there are invalid values', () => { - // @ts-expect-error - We are mocking next/config - nextConfig.mockImplementationOnce(() => ({ - serverRuntimeConfig: { - DNS_IP: 'invalid', - }, - })); - - expect(() => new TipiConfig().getConfig()).toThrow(); - }); -}); - describe('Test: getConfig', () => { it('It should return config from .env', () => { // arrange diff --git a/src/server/core/TipiConfig/TipiConfig.ts b/src/server/core/TipiConfig/TipiConfig.ts index 7fbbf883..03423583 100644 --- a/src/server/core/TipiConfig/TipiConfig.ts +++ b/src/server/core/TipiConfig/TipiConfig.ts @@ -1,58 +1,10 @@ import { z } from 'zod'; +import { envSchema, settingsSchema } from '@runtipi/shared'; import fs from 'fs-extra'; import nextConfig from 'next/config'; import { readJsonFile } from '../../common/fs.helpers'; import { Logger } from '../Logger'; -export const ARCHITECTURES = { - ARM: 'arm', - ARM64: 'arm64', - AMD64: 'amd64', -} as const; -export type Architecture = (typeof ARCHITECTURES)[keyof typeof ARCHITECTURES]; - -const configSchema = z.object({ - NODE_ENV: z.union([z.literal('development'), z.literal('production'), z.literal('test')]), - REDIS_HOST: z.string(), - status: z.union([z.literal('RUNNING'), z.literal('UPDATING'), z.literal('RESTARTING')]), - architecture: z.nativeEnum(ARCHITECTURES), - dnsIp: z.string().ip().trim(), - rootFolder: z.string(), - internalIp: z.string(), - version: z.string(), - jwtSecret: z.string(), - appsRepoId: z.string(), - appsRepoUrl: z.string().url().trim(), - domain: z.string().trim(), - localDomain: z.string().trim(), - storagePath: z - .string() - .trim() - .optional() - .transform((value) => { - if (!value) return undefined; - return value?.replace(/\s/g, ''); - }), - postgresHost: z.string(), - postgresDatabase: z.string(), - postgresUsername: z.string(), - postgresPassword: z.string(), - postgresPort: z.number(), - demoMode: z - .string() - .or(z.boolean()) - .optional() - .transform((value) => { - if (typeof value === 'boolean') return value; - return value === 'true'; - }), -}); - -export const settingsSchema = configSchema - .partial() - .pick({ dnsIp: true, internalIp: true, appsRepoUrl: true, domain: true, storagePath: true, localDomain: true }) - .and(z.object({ port: z.number(), sslPort: z.number(), listenIp: z.string().ip().trim() }).partial()); - type TipiSettingsType = z.infer; const formatErrors = (errors: { fieldErrors: Record }) => @@ -64,11 +16,11 @@ const formatErrors = (errors: { fieldErrors: Record }) => export class TipiConfig { private static instance: TipiConfig; - private config: z.infer; + private config: z.infer; constructor() { const conf = { ...process.env, ...nextConfig()?.serverRuntimeConfig }; - const envConfig: z.infer = { + const envConfig: z.infer = { postgresHost: conf.POSTGRES_HOST, postgresDatabase: conf.POSTGRES_DBNAME, postgresUsername: conf.POSTGRES_USERNAME, @@ -92,10 +44,10 @@ export class TipiConfig { }; const fileConfig = readJsonFile('/runtipi/state/settings.json') || {}; - const parsedFileConfig = configSchema.partial().safeParse(fileConfig); + const parsedFileConfig = envSchema.partial().safeParse(fileConfig); if (parsedFileConfig.success) { - const parsedConfig = configSchema.safeParse({ ...envConfig, ...parsedFileConfig.data }); + const parsedConfig = envSchema.safeParse({ ...envConfig, ...parsedFileConfig.data }); if (parsedConfig.success) { this.config = parsedConfig.data; } else { @@ -133,18 +85,18 @@ export class TipiConfig { return this.config; } - public async setConfig(key: T, value: z.infer[T], writeFile = false) { - const newConf: z.infer = { ...this.getConfig() }; + public async setConfig(key: T, value: z.infer[T], writeFile = false) { + const newConf: z.infer = { ...this.getConfig() }; newConf[key] = value; - this.config = configSchema.parse(newConf); + this.config = envSchema.parse(newConf); if (writeFile) { const currentJsonConf = readJsonFile('/runtipi/state/settings.json') || {}; - const parsedConf = configSchema.partial().parse(currentJsonConf); + const parsedConf = envSchema.partial().parse(currentJsonConf); parsedConf[key] = value; - const parsed = configSchema.partial().parse(parsedConf); + const parsed = envSchema.partial().parse(parsedConf); await fs.promises.writeFile('/runtipi/state/settings.json', JSON.stringify(parsed)); } @@ -155,7 +107,7 @@ export class TipiConfig { throw new Error('Cannot update settings in demo mode'); } - const newConf: z.infer = { ...this.getConfig() }; + const newConf: z.infer = { ...this.getConfig() }; const parsed = settingsSchema.safeParse(settings); if (!parsed.success) { @@ -165,11 +117,11 @@ export class TipiConfig { await fs.promises.writeFile('/runtipi/state/settings.json', JSON.stringify(parsed.data)); - this.config = configSchema.parse({ ...newConf, ...parsed.data }); + this.config = envSchema.parse({ ...newConf, ...parsed.data }); } } -export const setConfig = (key: T, value: z.infer[T], writeFile = false) => { +export const setConfig = (key: T, value: z.infer[T], writeFile = false) => { return TipiConfig.getInstance().setConfig(key, value, writeFile); }; diff --git a/src/server/routers/system/system.router.ts b/src/server/routers/system/system.router.ts index 8ce24f52..8f60e5ec 100644 --- a/src/server/routers/system/system.router.ts +++ b/src/server/routers/system/system.router.ts @@ -1,7 +1,7 @@ import { inferRouterOutputs } from '@trpc/server'; +import { settingsSchema } from '@runtipi/shared'; import { router, protectedProcedure, publicProcedure } from '../../trpc'; import { SystemServiceClass } from '../../services/system'; -import { settingsSchema } from '../../core/TipiConfig/TipiConfig'; import * as TipiConfig from '../../core/TipiConfig'; export type SystemRouterOutput = inferRouterOutputs; diff --git a/src/server/services/apps/apps.helpers.test.ts b/src/server/services/apps/apps.helpers.test.ts index 42ed2a24..639f2848 100644 --- a/src/server/services/apps/apps.helpers.test.ts +++ b/src/server/services/apps/apps.helpers.test.ts @@ -1,10 +1,10 @@ import fs from 'fs-extra'; -import { fromAny, fromPartial } from '@total-typescript/shoehorn'; +import { fromAny } from '@total-typescript/shoehorn'; import { faker } from '@faker-js/faker'; import { TestDatabase, clearDatabase, closeDatabase, createDatabase } from '@/server/tests/test-utils'; -import { getAppEnvMap } from '@/server/utils/env-generation'; +import { appInfoSchema } from '@runtipi/shared'; import { setConfig } from '../../core/TipiConfig'; -import { appInfoSchema, checkAppRequirements, checkEnvFile, ensureAppFolder, generateEnvFile, getAppInfo, getAvailableApps, getUpdateInfo } from './apps.helpers'; +import { checkAppRequirements, checkEnvFile, getAppInfo, getAvailableApps, getUpdateInfo } from './apps.helpers'; import { createAppConfig, insertApp } from '../../tests/apps.factory'; let db: TestDatabase; @@ -120,166 +120,6 @@ describe('Test: appInfoSchema', () => { }); }); -describe('Test: generateEnvFile()', () => { - it('Should generate an env file', async () => { - // arrange - const appConfig = createAppConfig({ form_fields: [{ env_variable: 'TEST_FIELD', type: 'text', label: 'test', required: true }] }); - const app = await insertApp({}, appConfig, db); - const fakevalue = faker.string.alphanumeric(10); - - // act - await generateEnvFile(Object.assign(app, { config: { TEST_FIELD: fakevalue } })); - const envmap = await getAppEnvMap(app.id); - - // assert - expect(envmap.get('TEST_FIELD')).toBe(fakevalue); - }); - - it('Should automatically generate value for random field', async () => { - // arrange - const appConfig = createAppConfig({ form_fields: [{ env_variable: 'RANDOM_FIELD', type: 'random', label: 'test', min: 32, max: 32, required: true }] }); - const app = await insertApp({}, appConfig, db); - - // act - await generateEnvFile(app); - const envmap = await getAppEnvMap(app.id); - - // assert - expect(envmap.get('RANDOM_FIELD')).toBeDefined(); - expect(envmap.get('RANDOM_FIELD')).toHaveLength(32); - }); - - it('Should not re-generate random field if it already exists', async () => { - // arrange - const appConfig = createAppConfig({ form_fields: [{ env_variable: 'RANDOM_FIELD', type: 'random', label: 'test', min: 32, max: 32, required: true }] }); - const app = await insertApp({}, appConfig, db); - const randomField = faker.string.alphanumeric(32); - fs.writeFileSync(`/app/storage/app-data/${app.id}/app.env`, `RANDOM_FIELD=${randomField}`); - - // act - await generateEnvFile(app); - const envmap = await getAppEnvMap(app.id); - - // assert - expect(envmap.get('RANDOM_FIELD')).toBe(randomField); - }); - - it('Should throw an error if required field is not provided', async () => { - // arrange - const appConfig = createAppConfig({ form_fields: [{ env_variable: 'TEST_FIELD', type: 'text', label: 'test', required: true }] }); - const app = await insertApp({}, appConfig, db); - - // act & assert - await expect(generateEnvFile(Object.assign(app, { config: { TEST_FIELD: undefined } }))).rejects.toThrowError('Variable test is required'); - }); - - it('Should throw an error if app does not exist', async () => { - // act & assert - await expect(generateEnvFile(fromPartial({ id: 'not-existing-app' }))).rejects.toThrowError('App not-existing-app has invalid config.json file'); - }); - - it('Should add APP_EXPOSED to env file if domain is provided and app is exposed', async () => { - // arrange - const domain = faker.internet.domainName(); - const appConfig = createAppConfig(); - const app = await insertApp({ domain, exposed: true }, appConfig, db); - - // act - await generateEnvFile(app); - const envmap = await getAppEnvMap(app.id); - - // assert - expect(envmap.get('APP_EXPOSED')).toBe('true'); - expect(envmap.get('APP_DOMAIN')).toBe(domain); - }); - - it('Should not add APP_EXPOSED if domain is not provided', async () => { - // arrange - const appConfig = createAppConfig(); - const app = await insertApp({ exposed: true }, appConfig, db); - - // act - await generateEnvFile(app); - const envmap = await getAppEnvMap(app.id); - - // assert - expect(envmap.get('APP_EXPOSED')).toBeUndefined(); - }); - - it('Should not add APP_EXPOSED if app is not exposed', async () => { - // arrange - const appConfig = createAppConfig(); - const app = await insertApp({ exposed: false, domain: faker.internet.domainName() }, appConfig, db); - - // act - await generateEnvFile(app); - const envmap = await getAppEnvMap(app.id); - - // assert - expect(envmap.get('APP_EXPOSED')).toBeUndefined(); - expect(envmap.get('APP_DOMAIN')).toBe(`localhost:${appConfig.port}`); - }); - - it('Should create app folder if it does not exist', async () => { - // arrange - const appConfig = createAppConfig(); - const app = await insertApp({}, appConfig, db); - fs.rmSync(`/app/storage/app-data/${app.id}`, { recursive: true }); - - // act - await generateEnvFile(app); - - // assert - expect(fs.existsSync(`/app/storage/app-data/${app.id}`)).toBe(true); - }); - - it('should generate vapid private and public keys if config has generate_vapid_keys set to true', async () => { - // arrange - const appConfig = createAppConfig({ generate_vapid_keys: true }); - const app = await insertApp({}, appConfig, db); - - // act - await generateEnvFile(app); - const envmap = await getAppEnvMap(app.id); - - // assert - expect(envmap.get('VAPID_PRIVATE_KEY')).toBeDefined(); - expect(envmap.get('VAPID_PUBLIC_KEY')).toBeDefined(); - }); - - it('should not generate vapid private and public keys if config has generate_vapid_keys set to false', async () => { - // arrange - const appConfig = createAppConfig({ generate_vapid_keys: false }); - const app = await insertApp({}, appConfig, db); - - // act - await generateEnvFile(app); - const envmap = await getAppEnvMap(app.id); - - // assert - expect(envmap.get('VAPID_PRIVATE_KEY')).toBeUndefined(); - expect(envmap.get('VAPID_PUBLIC_KEY')).toBeUndefined(); - }); - - it('should not re-generate vapid private and public keys if they already exist', async () => { - // arrange - const appConfig = createAppConfig({ generate_vapid_keys: true }); - const app = await insertApp({}, appConfig, db); - - const vapidPrivateKey = faker.string.alphanumeric(32); - const vapidPublicKey = faker.string.alphanumeric(32); - - // act - fs.writeFileSync(`/app/storage/app-data/${app.id}/app.env`, `VAPID_PRIVATE_KEY=${vapidPrivateKey}\nVAPID_PUBLIC_KEY=${vapidPublicKey}`); - await generateEnvFile(app); - const envmap = await getAppEnvMap(app.id); - - // assert - expect(envmap.get('VAPID_PRIVATE_KEY')).toBe(vapidPrivateKey); - expect(envmap.get('VAPID_PUBLIC_KEY')).toBe(vapidPublicKey); - }); -}); - describe('Test: getAvailableApps()', () => { it('Should return all available apps', async () => { // arrange @@ -359,18 +199,6 @@ describe('Test: getAppInfo()', () => { expect(result).toBeNull(); }); - it('Should throw if something goes wrong', async () => { - // arrange - jest.spyOn(fs, 'existsSync').mockImplementationOnce(() => { - throw new Error('Something went wrong'); - }); - const appConfig = createAppConfig(); - const app = await insertApp({ status: 'missing' }, appConfig, db); - - // act & assert - expect(() => getAppInfo(app.id, app.status)).toThrowError(`Error loading app: ${app.id}`); - }); - it('Should return null if app does not exist', async () => { // arrange const app = getAppInfo(faker.lorem.word()); @@ -413,63 +241,3 @@ describe('Test: getUpdateInfo()', () => { expect(updateInfo).toEqual({ latestVersion: 0, latestDockerVersion: '0.0.0' }); }); }); - -describe('Test: ensureAppFolder()', () => { - it('should copy the folder from repo', async () => { - // arrange - await fs.promises.mkdir('/runtipi/repos/repo-id/apps/test', { recursive: true }); - await fs.promises.writeFile('/runtipi/repos/repo-id/apps/test/test.yml', 'test'); - // act - ensureAppFolder('test'); - - // assert - const files = fs.readdirSync('/runtipi/apps/test'); - expect(files).toEqual(['test.yml']); - }); - - it('should not copy the folder if it already exists', async () => { - // arrange - await fs.promises.mkdir('/runtipi/repos/repo-id/apps/test', { recursive: true }); - await fs.promises.writeFile('/runtipi/repos/repo-id/apps/test/test.yml', 'test'); - await fs.promises.mkdir('/runtipi/apps/test', { recursive: true }); - await fs.promises.writeFile('/runtipi/apps/test/docker-compose.yml', 'test'); - - // act - ensureAppFolder('test'); - - // assert - const files = fs.readdirSync('/runtipi/apps/test'); - expect(files).toEqual(['docker-compose.yml']); - }); - - it('Should overwrite the folder if clean up is true', async () => { - // arrange - await fs.promises.mkdir('/runtipi/repos/repo-id/apps/test', { recursive: true }); - await fs.promises.writeFile('/runtipi/repos/repo-id/apps/test/test.yml', 'test'); - await fs.promises.mkdir('/runtipi/apps/test', { recursive: true }); - await fs.promises.writeFile('/runtipi/apps/test/docker-compose.yml', 'test'); - - // act - ensureAppFolder('test', true); - - // assert - const files = fs.readdirSync('/runtipi/apps/test'); - expect(files).toEqual(['test.yml']); - }); - - it('Should delete folder if it exists but has no docker-compose.yml file', async () => { - // arrange - const randomFileName = `${faker.lorem.word()}.yml`; - await fs.promises.mkdir('/runtipi/repos/repo-id/apps/test', { recursive: true }); - await fs.promises.writeFile(`/runtipi/repos/repo-id/apps/test/${randomFileName}`, 'test'); - await fs.promises.mkdir('/runtipi/apps/test', { recursive: true }); - await fs.promises.writeFile('/runtipi/apps/test/test.yml', 'test'); - - // act - ensureAppFolder('test'); - - // assert - const files = fs.readdirSync('/runtipi/apps/test'); - expect(files).toEqual([randomFileName]); - }); -}); diff --git a/src/server/services/apps/apps.helpers.ts b/src/server/services/apps/apps.helpers.ts index f300ce83..5443589e 100644 --- a/src/server/services/apps/apps.helpers.ts +++ b/src/server/services/apps/apps.helpers.ts @@ -1,61 +1,11 @@ -import crypto from 'crypto'; import fs from 'fs-extra'; -import { z } from 'zod'; import { App } from '@/server/db/schema'; -import { envMapToString, envStringToMap, generateVapidKeys, getAppEnvMap } from '@/server/utils/env-generation'; -import { deleteFolder, fileExists, getSeed, readdirSync, readFile, readJsonFile } from '../../common/fs.helpers'; -import { APP_CATEGORIES, FIELD_TYPES } from './apps.types'; +import { getAppEnvMap } from '@/server/utils/env-generation'; +import { appInfoSchema } from '@runtipi/shared'; +import { fileExists, readdirSync, readFile, readJsonFile } from '../../common/fs.helpers'; import { getConfig } from '../../core/TipiConfig'; import { Logger } from '../../core/Logger'; import { notEmpty } from '../../common/typescript.helpers'; -import { ARCHITECTURES } from '../../core/TipiConfig/TipiConfig'; - -const formFieldSchema = z.object({ - type: z.nativeEnum(FIELD_TYPES).catch(() => FIELD_TYPES.TEXT), - label: z.string(), - placeholder: z.string().optional(), - max: z.number().optional(), - min: z.number().optional(), - hint: z.string().optional(), - options: z.object({ label: z.string(), value: z.string() }).array().optional(), - required: z.boolean().optional().default(false), - default: z.union([z.boolean(), z.string()]).optional(), - regex: z.string().optional(), - pattern_error: z.string().optional(), - env_variable: z.string(), -}); - -export const appInfoSchema = z.object({ - id: z.string(), - available: z.boolean(), - port: z.number().min(1).max(65535), - name: z.string(), - description: z.string().optional().default(''), - version: z.string().optional().default('latest'), - tipi_version: z.number(), - short_desc: z.string(), - author: z.string(), - source: z.string(), - website: z.string().optional(), - force_expose: z.boolean().optional().default(false), - generate_vapid_keys: z.boolean().optional().default(false), - categories: z - .nativeEnum(APP_CATEGORIES) - .array() - .catch((ctx) => { - Logger.warn(`Invalid categories "${JSON.stringify(ctx.input)}" defaulting to utilities`); - return [APP_CATEGORIES.UTILITIES]; - }), - url_suffix: z.string().optional(), - form_fields: z.array(formFieldSchema).optional().default([]), - https: z.boolean().optional().default(false), - exposable: z.boolean().optional().default(false), - no_gui: z.boolean().optional().default(false), - supported_architectures: z.nativeEnum(ARCHITECTURES).array().optional(), -}); - -export type AppInfo = z.infer; -export type FormField = z.infer; /** * This function checks the requirements for the app with the provided name. @@ -120,186 +70,6 @@ export const checkEnvFile = async (appName: string) => { }); }; -/** - * This function generates a random string of the provided length by using the SHA-256 hash algorithm. - * It takes the provided name and a seed value, concatenates them, and uses them as input for the hash algorithm. - * It then returns a substring of the resulting hash of the provided length. - * - * @param {string} name - A name used as input for the hash algorithm. - * @param {number} length - The desired length of the random string. - */ -const getEntropy = (name: string, length: number) => { - const hash = crypto.createHash('sha256'); - hash.update(name + getSeed()); - return hash.digest('hex').substring(0, length); -}; - -/** - * This function takes an input of unknown type, checks if it is an object and not null, - * and returns it as a record of unknown values, if it is not an object or is null, returns an empty object. - * - * @param {unknown} json - The input of unknown type. - * @returns {Record} - The input as a record of unknown values, or an empty object if the input is not an object or is null. - */ -const castAppConfig = (json: unknown): Record => { - if (typeof json !== 'object' || json === null) { - return {}; - } - return json as Record; -}; - -/** - * This function generates an env file for the provided app. - * It reads the config.json file for the app, parses it, - * and uses the app's form fields and domain to generate the env file - * if the app is exposed and has a domain set, it adds the domain to the env file, - * otherwise, it adds the internal IP address to the env file - * It also creates the app-data folder for the app if it does not exist - * - * @param {App} app - The app for which the env file is generated. - * @throws Will throw an error if the app has an invalid config.json file or if a required variable is missing. - */ -export const generateEnvFile = async (app: App) => { - const configFile = readJsonFile(`/runtipi/apps/${app.id}/config.json`); - const parsedConfig = appInfoSchema.safeParse(configFile); - - if (!parsedConfig.success) { - throw new Error(`App ${app.id} has invalid config.json file`); - } - - const baseEnvFile = readFile('/runtipi/.env').toString(); - const envMap = envStringToMap(baseEnvFile); - - // Default always present env variables - envMap.set('APP_PORT', String(parsedConfig.data.port)); - envMap.set('APP_ID', app.id); - - const existingEnvMap = await getAppEnvMap(app.id); - - if (parsedConfig.data.generate_vapid_keys) { - if (existingEnvMap.has('VAPID_PUBLIC_KEY') && existingEnvMap.has('VAPID_PRIVATE_KEY')) { - envMap.set('VAPID_PUBLIC_KEY', existingEnvMap.get('VAPID_PUBLIC_KEY') as string); - envMap.set('VAPID_PRIVATE_KEY', existingEnvMap.get('VAPID_PRIVATE_KEY') as string); - } else { - const vapidKeys = generateVapidKeys(); - envMap.set('VAPID_PUBLIC_KEY', vapidKeys.publicKey); - envMap.set('VAPID_PRIVATE_KEY', vapidKeys.privateKey); - } - } - - parsedConfig.data.form_fields.forEach((field) => { - const formValue = castAppConfig(app.config)[field.env_variable]; - const envVar = field.env_variable; - - if (formValue || typeof formValue === 'boolean') { - envMap.set(envVar, String(formValue)); - } else if (field.type === 'random') { - if (existingEnvMap.has(envVar)) { - envMap.set(envVar, existingEnvMap.get(envVar) as string); - } else { - const length = field.min || 32; - const randomString = getEntropy(field.env_variable, length); - - envMap.set(envVar, randomString); - } - } else if (field.required) { - throw new Error(`Variable ${field.label || field.env_variable} is required`); - } - }); - - if (app.exposed && app.domain) { - envMap.set('APP_EXPOSED', 'true'); - envMap.set('APP_DOMAIN', app.domain); - envMap.set('APP_PROTOCOL', 'https'); - envMap.set('APP_HOST', app.domain); - } else { - envMap.set('APP_DOMAIN', `${getConfig().internalIp}:${parsedConfig.data.port}`); - envMap.set('APP_HOST', getConfig().internalIp); - } - - // Create app-data folder if it doesn't exist - const appDataDirectoryExists = await fs.promises.stat(`/app/storage/app-data/${app.id}`).catch(() => false); - if (!appDataDirectoryExists) { - await fs.promises.mkdir(`/app/storage/app-data/${app.id}`, { recursive: true }); - } - - await fs.promises.writeFile(`/app/storage/app-data/${app.id}/app.env`, envMapToString(envMap)); -}; - -/** - * Given a template and a map of variables, this function replaces all instances of the variables in the template with their values. - * - * @param {string} template - The template to be rendered. - * @param {Map} envMap - The map of variables and their values. - */ -const renderTemplate = (template: string, envMap: Map) => { - let renderedTemplate = template; - - envMap.forEach((value, key) => { - renderedTemplate = renderedTemplate.replace(new RegExp(`{{${key}}}`, 'g'), value); - }); - - return renderedTemplate; -}; - -/** - * Given an app, this function copies the app's data directory to the app-data folder. - * If a file with an extension of .template is found, it will be copied as a file without the .template extension and the template variables will be replaced - * by the values in the app's env file. - * - * @param {string} id - The id of the app. - */ -export const copyDataDir = async (id: string) => { - const envMap = await getAppEnvMap(id); - - const appDataDirExists = (await fs.promises.lstat(`/runtipi/apps/${id}/data`).catch(() => false)) as fs.Stats; - if (!appDataDirExists || !appDataDirExists.isDirectory()) { - return; - } - - const dataDir = await fs.promises.readdir(`/runtipi/apps/${id}/data`); - - const processFile = async (file: string) => { - if (file.endsWith('.template')) { - const template = await fs.promises.readFile(`/runtipi/apps/${id}/data/${file}`, 'utf-8'); - const renderedTemplate = renderTemplate(template, envMap); - - await fs.promises.writeFile(`/app/storage/app-data/${id}/data/${file.replace('.template', '')}`, renderedTemplate); - } else { - await fs.promises.copyFile(`/runtipi/apps/${id}/data/${file}`, `/app/storage/app-data/${id}/data/${file}`); - } - }; - - const processDir = async (path: string) => { - await fs.promises.mkdir(`/app/storage/app-data/${id}/data/${path}`, { recursive: true }); - const files = await fs.promises.readdir(`/runtipi/apps/${id}/data/${path}`); - - await Promise.all( - files.map(async (file) => { - const fullPath = `/runtipi/apps/${id}/data/${path}/${file}`; - - if ((await fs.promises.lstat(fullPath)).isDirectory()) { - await processDir(`${path}/${file}`); - } else { - await processFile(`${path}/${file}`); - } - }), - ); - }; - - await Promise.all( - dataDir.map(async (file) => { - const fullPath = `/runtipi/apps/${id}/data/${file}`; - - if ((await fs.promises.lstat(fullPath)).isDirectory()) { - await processDir(file); - } else { - await processFile(file); - } - }), - ); -}; - /** This function reads the apps directory and skips certain system files, then reads the config.json and metadata/description.md files for each app, parses the config file, filters out any apps that are not available and returns an array of app information. @@ -394,26 +164,3 @@ export const getAppInfo = (id: string, status?: App['status']) => { throw new Error(`Error loading app: ${id}`); } }; - -/** - * This function ensures that the app folder for the app with the provided name exists. - * If the cleanup parameter is set to true, it deletes the app folder if it exists. - * If the app folder does not exist, it copies the app folder from the apps repository. - * - * @param {string} appName - The name of the app. - * @param {boolean} [cleanup] - A flag indicating whether to cleanup the app folder before ensuring its existence. - * @throws Will throw an error if the app folder cannot be copied from the repository - */ -export const ensureAppFolder = (appName: string, cleanup = false): void => { - if (cleanup && fileExists(`/runtipi/apps/${appName}`)) { - deleteFolder(`/runtipi/apps/${appName}`); - } - - if (!fileExists(`/runtipi/apps/${appName}/docker-compose.yml`)) { - if (fileExists(`/runtipi/apps/${appName}`)) { - deleteFolder(`/runtipi/apps/${appName}`); - } - // Copy from apps repo - fs.copySync(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appName}`, `/runtipi/apps/${appName}`); - } -}; diff --git a/src/server/tests/apps.factory.ts b/src/server/tests/apps.factory.ts index 4762d78d..562bf24b 100644 --- a/src/server/tests/apps.factory.ts +++ b/src/server/tests/apps.factory.ts @@ -1,9 +1,7 @@ import fs from 'fs-extra'; import { faker } from '@faker-js/faker'; import { eq } from 'drizzle-orm'; -import { Architecture } from '../core/TipiConfig/TipiConfig'; -import { AppInfo, appInfoSchema } from '../services/apps/apps.helpers'; -import { APP_CATEGORIES } from '../services/apps/apps.types'; +import { AppInfo, Architecture, appInfoSchema, APP_CATEGORIES } from '@runtipi/shared'; import { TestDatabase } from './test-utils'; import { appTable, AppStatus, App, NewApp } from '../db/schema';