From 62a8e6656498d81b28472c7c0a66ae9fef487d65 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 26 Sep 2024 15:03:16 -0400 Subject: [PATCH] fix(server): use default values when not provided --- server/package-lock.json | 125 -------- server/package.json | 2 - server/src/app.module.ts | 5 +- server/src/config.ts | 94 +----- server/src/constants.ts | 11 +- server/src/cores/storage.core.ts | 10 +- server/src/cores/system-config.core.ts | 5 +- server/src/database.config.ts | 15 +- server/src/dtos/system-config.dto.ts | 2 +- server/src/enum.ts | 9 + server/src/env.ts | 299 ++++++++++++++++++ server/src/interfaces/logger.interface.ts | 4 +- server/src/main.ts | 2 +- server/src/repositories/logger.repository.ts | 6 +- .../repositories/server-info.repository.ts | 3 +- server/src/services/database.service.ts | 3 +- server/src/services/media.service.ts | 3 +- server/src/services/person.service.ts | 3 +- server/src/services/server.service.ts | 6 +- server/src/services/system-config.service.ts | 5 +- server/src/services/version.service.ts | 5 +- server/src/utils/instrumentation.ts | 16 +- server/src/utils/logger.ts | 3 +- server/src/utils/misc.ts | 5 +- server/src/workers/api.ts | 29 +- server/src/workers/microservices.ts | 9 +- 26 files changed, 392 insertions(+), 287 deletions(-) create mode 100644 server/src/env.ts diff --git a/server/package-lock.json b/server/package-lock.json index 57c8dd7146..c1ecdb0fd3 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "@nestjs/bullmq": "^10.0.1", "@nestjs/common": "^10.2.2", - "@nestjs/config": "^3.0.0", "@nestjs/core": "^10.2.2", "@nestjs/event-emitter": "^2.0.4", "@nestjs/platform-express": "^10.2.2", @@ -41,7 +40,6 @@ "handlebars": "^4.7.8", "i18n-iso-countries": "^7.6.0", "ioredis": "^5.3.2", - "joi": "^17.10.0", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "luxon": "^3.4.2", @@ -1323,19 +1321,6 @@ "node": ">=12" } }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -2075,20 +2060,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, - "node_modules/@nestjs/config": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.3.tgz", - "integrity": "sha512-p6yv/CvoBewJ72mBq4NXgOAi2rSQNWx3a+IMJLVKS2uiwFCOQQuiIatGwq6MRjXV3Jr+B41iUO8FIf4xBrZ4/w==", - "dependencies": { - "dotenv": "16.4.5", - "dotenv-expand": "10.0.0", - "lodash": "4.17.21" - }, - "peerDependencies": { - "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", - "rxjs": "^7.1.0" - } - }, "node_modules/@nestjs/core": { "version": "10.4.4", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.4.tgz", @@ -4801,24 +4772,6 @@ "url": "https://ko-fi.com/killymxi" } }, - "node_modules/@sideway/address": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" - }, "node_modules/@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -7926,14 +7879,6 @@ "url": "https://dotenvx.com" } }, - "node_modules/dotenv-expand": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", - "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", - "engines": { - "node": ">=12" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -9834,18 +9779,6 @@ "jiti": "bin/jiti.js" } }, - "node_modules/joi": { - "version": "17.13.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", - "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", - "dependencies": { - "@hapi/hoek": "^9.3.0", - "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.5", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, "node_modules/jose": { "version": "4.15.9", "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", @@ -16111,19 +16044,6 @@ } } }, - "@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" - }, - "@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "requires": { - "@hapi/hoek": "^9.0.0" - } - }, "@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -16528,16 +16448,6 @@ } } }, - "@nestjs/config": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.3.tgz", - "integrity": "sha512-p6yv/CvoBewJ72mBq4NXgOAi2rSQNWx3a+IMJLVKS2uiwFCOQQuiIatGwq6MRjXV3Jr+B41iUO8FIf4xBrZ4/w==", - "requires": { - "dotenv": "16.4.5", - "dotenv-expand": "10.0.0", - "lodash": "4.17.21" - } - }, "@nestjs/core": { "version": "10.4.4", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.4.tgz", @@ -18236,24 +18146,6 @@ "selderee": "^0.11.0" } }, - "@sideway/address": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "requires": { - "@hapi/hoek": "^9.0.0" - } - }, - "@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" - }, - "@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" - }, "@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -20601,11 +20493,6 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" }, - "dotenv-expand": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", - "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==" - }, "eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -21998,18 +21885,6 @@ "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", "peer": true }, - "joi": { - "version": "17.13.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", - "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", - "requires": { - "@hapi/hoek": "^9.3.0", - "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.5", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, "jose": { "version": "4.15.9", "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", diff --git a/server/package.json b/server/package.json index 8ba20f6b3b..dbc8984286 100644 --- a/server/package.json +++ b/server/package.json @@ -37,7 +37,6 @@ "dependencies": { "@nestjs/bullmq": "^10.0.1", "@nestjs/common": "^10.2.2", - "@nestjs/config": "^3.0.0", "@nestjs/core": "^10.2.2", "@nestjs/event-emitter": "^2.0.4", "@nestjs/platform-express": "^10.2.2", @@ -67,7 +66,6 @@ "handlebars": "^4.7.8", "i18n-iso-countries": "^7.6.0", "ioredis": "^5.3.2", - "joi": "^17.10.0", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "luxon": "^3.4.2", diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 9446010127..371af67439 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -1,6 +1,5 @@ import { BullModule } from '@nestjs/bullmq'; import { Inject, Module, OnModuleDestroy, OnModuleInit, ValidationPipe } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE, ModuleRef } from '@nestjs/core'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule'; @@ -9,7 +8,7 @@ import _ from 'lodash'; import { ClsModule } from 'nestjs-cls'; import { OpenTelemetryModule } from 'nestjs-otel'; import { commands } from 'src/commands'; -import { bullConfig, bullQueues, clsConfig, immichAppConfig } from 'src/config'; +import { bullConfig, bullQueues, clsConfig } from 'src/config'; import { controllers } from 'src/controllers'; import { databaseConfig } from 'src/database.config'; import { entities } from 'src/entities'; @@ -41,7 +40,6 @@ const imports = [ BullModule.forRoot(bullConfig), BullModule.registerQueue(...bullQueues), ClsModule.forRoot(clsConfig), - ConfigModule.forRoot(immichAppConfig), EventEmitterModule.forRoot(), OpenTelemetryModule.forRoot(otelConfig), TypeOrmModule.forRootAsync({ @@ -117,7 +115,6 @@ export class ImmichAdminModule {} @Module({ imports: [ - ConfigModule.forRoot(immichAppConfig), EventEmitterModule.forRoot(), TypeOrmModule.forRoot(databaseConfig), TypeOrmModule.forFeature(entities), diff --git a/server/src/config.ts b/server/src/config.ts index 03ea3f111b..db4343bc2d 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -1,12 +1,12 @@ import { RegisterQueueOptions } from '@nestjs/bullmq'; -import { ConfigModuleOptions } from '@nestjs/config'; import { CronExpression } from '@nestjs/schedule'; import { QueueOptions } from 'bullmq'; import { Request, Response } from 'express'; import { RedisOptions } from 'ioredis'; -import Joi, { Root } from 'joi'; import { CLS_ID, ClsModuleOptions } from 'nestjs-cls'; import { ImmichHeader } from 'src/dtos/auth.dto'; +import { LogLevel } from 'src/enum'; +import { envData, ImmichEnv } from 'src/env'; import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface'; export enum TranscodePolicy { @@ -75,15 +75,6 @@ export enum ImageFormat { WEBP = 'webp', } -export enum LogLevel { - VERBOSE = 'verbose', - DEBUG = 'debug', - LOG = 'log', - WARN = 'warn', - ERROR = 'error', - FATAL = 'fatal', -} - export interface SystemConfig { ffmpeg: { crf: number; @@ -265,8 +256,8 @@ export const defaults = Object.freeze({ level: LogLevel.LOG, }, machineLearning: { - enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false', - url: process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003', + enabled: envData.machineLearning.enabled, + url: envData.machineLearning.url, clip: { enabled: true, modelName: 'ViT-B-32__openai', @@ -372,55 +363,8 @@ export const defaults = Object.freeze({ }, }); -const WHEN_DB_URL_SET = Joi.when('DB_URL', { - is: Joi.exist(), - then: Joi.string().optional(), - otherwise: Joi.string().required(), -}); - -export const immichAppConfig: ConfigModuleOptions = { - envFilePath: '.env', - isGlobal: true, - validationSchema: Joi.object({ - IMMICH_ENV: Joi.string().optional().valid('development', 'testing', 'production').default('production'), - IMMICH_LOG_LEVEL: Joi.string() - .optional() - .valid(...Object.values(LogLevel)), - - DB_USERNAME: WHEN_DB_URL_SET, - DB_PASSWORD: WHEN_DB_URL_SET, - DB_DATABASE_NAME: WHEN_DB_URL_SET, - DB_URL: Joi.string().optional(), - DB_VECTOR_EXTENSION: Joi.string().optional().valid('pgvector', 'pgvecto.rs').default('pgvecto.rs'), - DB_SKIP_MIGRATIONS: Joi.boolean().optional().default(false), - - IMMICH_PORT: Joi.number().optional(), - IMMICH_API_METRICS_PORT: Joi.number().optional(), - IMMICH_MICROSERVICES_METRICS_PORT: Joi.number().optional(), - - IMMICH_TRUSTED_PROXIES: Joi.extend((joi: Root) => ({ - type: 'stringArray', - base: joi.array(), - coerce: (value) => (value.split ? value.split(',') : value), - })) - .stringArray() - .single() - .items( - Joi.string().ip({ - version: ['ipv4', 'ipv6'], - cidr: 'optional', - }), - ), - - IMMICH_METRICS: Joi.boolean().optional().default(false), - IMMICH_HOST_METRICS: Joi.boolean().optional().default(false), - IMMICH_API_METRICS: Joi.boolean().optional().default(false), - IMMICH_IO_METRICS: Joi.boolean().optional().default(false), - }), -}; - export function parseRedisConfig(): RedisOptions { - const redisUrl = process.env.REDIS_URL; + const redisUrl = envData.redis.url; if (redisUrl && redisUrl.startsWith('ioredis://')) { try { const decodedString = Buffer.from(redisUrl.slice(10), 'base64').toString(); @@ -430,12 +374,12 @@ export function parseRedisConfig(): RedisOptions { } } return { - host: process.env.REDIS_HOSTNAME || 'redis', - port: Number.parseInt(process.env.REDIS_PORT || '6379'), - db: Number.parseInt(process.env.REDIS_DBINDEX || '0'), - username: process.env.REDIS_USERNAME || undefined, - password: process.env.REDIS_PASSWORD || undefined, - path: process.env.REDIS_SOCKET || undefined, + host: envData.redis.hostname, + port: envData.redis.port, + db: envData.redis.dbIndex, + username: envData.redis.username, + password: envData.redis.password, + path: envData.redis.socket, }; } @@ -465,18 +409,6 @@ export const clsConfig: ClsModuleOptions = { }, }; -export const getBuildMetadata = () => ({ - build: process.env.IMMICH_BUILD, - buildUrl: process.env.IMMICH_BUILD_URL, - buildImage: process.env.IMMICH_BUILD_IMAGE, - buildImageUrl: process.env.IMMICH_BUILD_IMAGE_URL, - repository: process.env.IMMICH_REPOSITORY, - repositoryUrl: process.env.IMMICH_REPOSITORY_URL, - sourceRef: process.env.IMMICH_SOURCE_REF, - sourceCommit: process.env.IMMICH_SOURCE_COMMIT, - sourceUrl: process.env.IMMICH_SOURCE_URL, -}); - const clientLicensePublicKeyProd = 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF2LzdTMzJjUkE1KysxTm5WRHNDTQpzcFAvakpISU1xT0pYRm5oNE53QTJPcHorUk1mZGNvOTJQc09naCt3d1FlRXYxVTJjMnBqelRpUS8ybHJLcS9rCnpKUmxYd2M0Y1Vlc1FETUpPRitQMnFPTlBiQUprWHZDWFlCVUxpdENJa29Md2ZoU0dOanlJS2FSRGhkL3ROeU4KOCtoTlJabllUMWhTSWo5U0NrS3hVQ096YXRQVjRtQ0RlclMrYkUrZ0VVZVdwOTlWOWF6dkYwRkltblRXcFFTdwpjOHdFWmdPTWg0c3ZoNmFpY3dkemtQQ3dFTGFrMFZhQkgzMUJFVUNRTGI5K0FJdEhBVXRKQ0t4aGI1V2pzMXM5CmJyWGZpMHZycGdjWi82RGFuWTJxZlNQem5PbXZEMkZycmxTMXE0SkpOM1ZvN1d3LzBZeS95TWNtelRXWmhHdWgKVVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tDQo='; @@ -484,7 +416,7 @@ const clientLicensePublicKeyStaging = 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFuSUNyTm5jbGpPSC9JdTNtWVVaRQp0dGJLV1c3OGRuajl5M0U2ekk3dU1NUndEckdYWFhkTGhkUDFxSWtlZHh0clVVeUpCMWR4R04yQW91S082MlNGCldrbU9PTmNGQlRBWFZTdjhUNVY0S0VwWnFQYWEwaXpNaGxMaE5sRXEvY1ZKdllrWlh1Z2x6b1o3cG1nbzFSdHgKam1iRm5NNzhrYTFRUUJqOVdLaEw2eWpWRUl2MDdVS0lKWHBNTnNuS2g1V083MjZhYmMzSE9udTlETjY5VnFFRQo3dGZrUnRWNmx2U1NzMkFVMngzT255cHA4ek53b0lPTWRibGsyb09aWWROZzY0Y3l2SzJoU0FlU3NVMFRyOVc5Ckgra0Y5QlNCNlk0QXl0QlVkSmkrK2pMSW5HM2Q5cU9ieFVzTlYrN05mRkF5NjJkL0xNR0xSOC9OUFc0U0s3c0MKRlFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tDQo='; export const getClientLicensePublicKey = (): string => { - if (process.env.IMMICH_ENV === 'production') { + if (envData.environment === ImmichEnv.PRODUCTION) { return clientLicensePublicKeyProd; } return clientLicensePublicKeyStaging; @@ -497,7 +429,7 @@ const serverLicensePublicKeyStaging = 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUE3Sy8yd3ZLUS9NdU8ydi9MUm5saAoyUy9zTHhDOGJiTEw1UUlKOGowQ3BVZW40YURlY2dYMUpKUmtGNlpUVUtpNTdTbEhtS3RSM2JOTzJmdTBUUVg5Ck5WMEJzVzllZVB0MmlTMWl4VVFmTzRObjdvTjZzbEtac01qd29RNGtGRGFmM3VHTlZJc0dMb3UxVWRLUVhpeDEKUlRHcXVTb3NZVjNWRlk3Q1hGYTVWaENBL3poVXNsNGFuVXp3eEF6M01jUFVlTXBaenYvbVZiQlRKVzBPSytWZgpWQUJvMXdYMkVBanpBekVHVzQ3Vko4czhnMnQrNHNPaHFBNStMQjBKVzlORUg5QUpweGZzWE4zSzVtM00yNUJVClZXcTlRYStIdHRENnJ0bnAvcUFweXVkWUdwZk9HYTRCUlZTR1MxMURZM0xrb2FlRzYwUEU5NHpoYjduOHpMWkgKelFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tDQo='; export const getServerLicensePublicKey = (): string => { - if (process.env.IMMICH_ENV === 'production') { + if (envData.environment === ImmichEnv.PRODUCTION) { return serverLicensePublicKeyProd; } return serverLicensePublicKeyStaging; diff --git a/server/src/constants.ts b/server/src/constants.ts index 6cfcc41d89..989c042be0 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -2,6 +2,7 @@ import { Duration } from 'luxon'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { SemVer } from 'semver'; +import { envData } from 'src/env'; export const POSTGRES_VERSION_RANGE = '>=14.0.0'; export const VECTORS_VERSION_RANGE = '>=0.2 <0.4'; @@ -19,17 +20,11 @@ export const serverVersion = new SemVer(version); export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 }); export const ONE_HOUR = Duration.fromObject({ hours: 1 }); - -export const envName = (process.env.IMMICH_ENV || 'production').toUpperCase(); -export const isDev = () => process.env.IMMICH_ENV === 'development'; -export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload'; -export const WEB_ROOT = process.env.IMMICH_WEB_ROOT || '/usr/src/app/www'; -const HOST_SERVER_PORT = process.env.IMMICH_PORT || '2283'; -export const DEFAULT_EXTERNAL_DOMAIN = 'http://localhost:' + HOST_SERVER_PORT; +export const DEFAULT_EXTERNAL_DOMAIN = 'http://localhost:2283'; export const citiesFile = 'cities500.txt'; -const buildFolder = process.env.IMMICH_BUILD_DATA || '/build'; +const { buildFolder } = envData; const folders = { geodata: join(buildFolder, 'geodata'), diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index e20a0c658d..a7909a7577 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -1,12 +1,12 @@ import { randomUUID } from 'node:crypto'; import { dirname, join, resolve } from 'node:path'; import { ImageFormat } from 'src/config'; -import { APP_MEDIA_LOCATION } from 'src/constants'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { AssetEntity } from 'src/entities/asset.entity'; import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity'; import { PersonEntity } from 'src/entities/person.entity'; import { AssetFileType } from 'src/enum'; +import { envData } from 'src/env'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; @@ -24,8 +24,8 @@ export enum StorageFolder { THUMBNAILS = 'thumbs', } -export const THUMBNAIL_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.THUMBNAILS)); -export const ENCODED_VIDEO_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.ENCODED_VIDEO)); +export const THUMBNAIL_DIR = resolve(join(envData.mediaLocation, StorageFolder.THUMBNAILS)); +export const ENCODED_VIDEO_DIR = resolve(join(envData.mediaLocation, StorageFolder.ENCODED_VIDEO)); export interface MoveRequest { entityId: string; @@ -94,7 +94,7 @@ export class StorageCore { } static getBaseFolder(folder: StorageFolder) { - return join(APP_MEDIA_LOCATION, folder); + return join(envData.mediaLocation, folder); } static getPersonThumbnailPath(person: PersonEntity) { @@ -119,7 +119,7 @@ export class StorageCore { static isImmichPath(path: string) { const resolvedPath = resolve(path); - const resolvedAppMediaLocation = resolve(APP_MEDIA_LOCATION); + const resolvedAppMediaLocation = resolve(envData.mediaLocation); const normalizedPath = resolvedPath.endsWith('/') ? resolvedPath : resolvedPath + '/'; const normalizedAppMediaLocation = resolvedAppMediaLocation.endsWith('/') ? resolvedAppMediaLocation diff --git a/server/src/cores/system-config.core.ts b/server/src/cores/system-config.core.ts index 8ed53344cc..d24d670a56 100644 --- a/server/src/cores/system-config.core.ts +++ b/server/src/cores/system-config.core.ts @@ -8,6 +8,7 @@ import { Subject } from 'rxjs'; import { SystemConfig, defaults } from 'src/config'; import { SystemConfigDto } from 'src/dtos/system-config.dto'; import { SystemMetadataKey } from 'src/enum'; +import { envData } from 'src/env'; import { DatabaseLock } from 'src/interfaces/database.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; @@ -85,13 +86,13 @@ export class SystemConfigCore { } isUsingConfigFile() { - return !!process.env.IMMICH_CONFIG_FILE; + return !!envData.configFile; } private async buildConfig() { // load partial const partial = this.isUsingConfigFile() - ? await this.loadFromFile(process.env.IMMICH_CONFIG_FILE as string) + ? await this.loadFromFile(envData.configFile as string) : await this.repository.get(SystemMetadataKey.SYSTEM_CONFIG); // merge with defaults diff --git a/server/src/database.config.ts b/server/src/database.config.ts index 9cc317a734..3a63f20048 100644 --- a/server/src/database.config.ts +++ b/server/src/database.config.ts @@ -1,16 +1,17 @@ +import { envData } from 'src/env'; import { DatabaseExtension } from 'src/interfaces/database.interface'; import { DataSource } from 'typeorm'; import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js'; -const url = process.env.DB_URL; +const url = envData.database.url; const urlOrParts = url ? { url } : { - host: process.env.DB_HOSTNAME || 'database', - port: Number.parseInt(process.env.DB_PORT || '5432'), - username: process.env.DB_USERNAME || 'postgres', - password: process.env.DB_PASSWORD || 'postgres', - database: process.env.DB_DATABASE_NAME || 'immich', + host: envData.database.hostname, + port: envData.database.port, + username: envData.database.username, + password: envData.database.password, + database: envData.database.name, }; /* eslint unicorn/prefer-module: "off" -- We can fix this when migrating to ESM*/ @@ -34,4 +35,4 @@ export const databaseConfig: PostgresConnectionOptions = { export const dataSource = new DataSource({ ...databaseConfig, host: 'localhost' }); export const getVectorExtension = () => - process.env.DB_VECTOR_EXTENSION === 'pgvector' ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS; + envData.database.vectorExtension === 'pgvector' ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS; diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 336f50f39b..6f4c0f2836 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -23,7 +23,6 @@ import { CQMode, Colorspace, ImageFormat, - LogLevel, SystemConfig, ToneMapping, TranscodeHWAccel, @@ -32,6 +31,7 @@ import { VideoContainer, } from 'src/config'; import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto'; +import { LogLevel } from 'src/enum'; import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface'; import { ValidateBoolean, validateCronExpression } from 'src/validation'; diff --git a/server/src/enum.ts b/server/src/enum.ts index 027b3160a7..fa0eb0e790 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -31,6 +31,15 @@ export enum EntityType { ALBUM = 'ALBUM', } +export enum LogLevel { + VERBOSE = 'verbose', + DEBUG = 'debug', + LOG = 'log', + WARN = 'warn', + ERROR = 'error', + FATAL = 'fatal', +} + export enum MemoryType { /** pictures taken on this day X years ago */ ON_THIS_DAY = 'on_this_day', diff --git a/server/src/env.ts b/server/src/env.ts new file mode 100644 index 0000000000..b1a77cf9e3 --- /dev/null +++ b/server/src/env.ts @@ -0,0 +1,299 @@ +import { plainToInstance, Transform, Type } from 'class-transformer'; +import { + buildMessage, + IsBoolean, + IsEnum, + IsInt, + IsIpVersion, + IsOptional, + IsString, + ValidateBy, + ValidateNested, + validateSync, + ValidationOptions, +} from 'class-validator'; +import { LogLevel } from 'src/enum'; +import { Optional } from 'src/validation'; +import { isIPRange } from 'validator'; + +export enum ImmichEnv { + DEVELOPMENT = 'development', + TESTING = 'testing', + PRODUCTION = 'production', +} + +enum VectorExtension { + PG_VECTOR = 'pgvector', + PG_VECTORS = 'pgvecto.rs', +} + +function IsIPRange(version?: IsIpVersion, validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'isIpRange', + constraints: [version], + validator: { + validate: (value, args): boolean => isIPRange(value, args?.constraints[0]), + defaultMessage: buildMessage((eachPrefix) => eachPrefix + '$property must be an ip address', validationOptions), + }, + }, + validationOptions, + ); +} + +class BuildMetadata { + @IsString() + build!: string; + + @IsString() + buildUrl!: string; + + @IsString() + buildImage!: string; + + @IsString() + buildImageUrl!: string; + + @IsString() + repository!: string; + + @IsString() + repositoryUrl!: string; + + @IsString() + sourceRef!: string; + + @IsString() + sourceCommit!: string; + + @IsString() + sourceUrl!: string; +} + +class Database { + @IsString() + @IsOptional() + url?: string; + + @IsString() + username!: string; + + @IsString() + password!: string; + + @IsString() + hostname!: string; + + @IsInt() + @Type(() => Number) + port!: number; + + @IsString() + name!: string; + + @IsEnum(VectorExtension) + vectorExtension!: VectorExtension; + + @IsBoolean() + @Type(() => Boolean) + skipMigrations!: boolean; +} + +class MachineLearning { + @IsBoolean() + enabled!: boolean; + + @IsString() + url!: string; +} + +class Metrics { + @IsInt() + @Type(() => Number) + apiPort!: number; + + @IsInt() + @Type(() => Number) + microservicesPort!: number; + + @IsBoolean() + @Type(() => Boolean) + enabled!: boolean; + + @IsBoolean() + @Type(() => Boolean) + hostEnabled!: boolean; + + @IsBoolean() + @Type(() => Boolean) + apiEnabled!: boolean; + + @IsBoolean() + @Type(() => Boolean) + ioEnabled!: boolean; + + @IsBoolean() + @Type(() => Boolean) + repoEnabled!: boolean; + + @IsBoolean() + @Type(() => Boolean) + jobEnabled!: boolean; +} + +class Redis { + @IsString() + @Optional() + url?: string; + + @IsString() + hostname = 'redis'; + + @IsInt() + @Type(() => Number) + port = 6379; + + dbIndex = 0; + + username?: string; + password?: string; + socket?: string; +} + +export class EnvData { + @IsString() + @Optional() + configFile?: string; + + @IsEnum(ImmichEnv) + environment!: ImmichEnv; + + @IsEnum(LogLevel) + @Optional() + logLevel?: LogLevel; + + @IsString() + mediaLocation!: string; + + @IsString() + buildFolder!: string; + + @IsString() + @Optional() + host?: string; + + @IsInt() + @Type(() => Number) + port!: number; + + @IsBoolean() + @Type(() => Boolean) + processInvalidImages!: boolean; + + @IsIPRange(undefined, { each: true }) + @Transform(({ value }) => (typeof value === 'string' ? value.split(',') : value)) + @Optional() + trustedProxies!: string[]; + + @IsString() + @Optional() + nodeVersion?: string; + + @IsBoolean() + @Optional() + noColor?: boolean; + + @ValidateNested() + @Type(() => BuildMetadata) + buildMetadata!: BuildMetadata; + + @ValidateNested() + @Type(() => Database) + database!: Database; + + @ValidateNested() + @Type(() => MachineLearning) + machineLearning!: MachineLearning; + + @ValidateNested() + @Type(() => Metrics) + metrics!: Metrics; + + @ValidateNested() + @Type(() => Redis) + redis!: Redis; +} + +const env = plainToInstance(EnvData, { + host: process.env.HOST, + port: process.env.IMMICH_PORT || 3001, + + environment: process.env.IMMICH_ENV || ImmichEnv.PRODUCTION, + configFile: process.env.IMMICH_CONFIG_FILE, + logLevel: process.env.IMMICH_LOG_LEVEL, + mediaLocation: process.env.IMMICH_MEDIA_LOCATION || './upload', + trustedProxies: process.env.IMMICH_TRUSTED_PROXIES || [], + buildFolder: process.env.IMMICH_BUILD_DATA || '/build', + + // TODO move to system config + processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES ?? false, + + nodeVersion: process.env.NODE_VERSION, + noColor: !!process.env.NO_COLOR, + + database: { + url: process.env.DB_URL, + username: process.env.DB_USERNAME || 'postgres', + password: process.env.DB_PASSWORD || 'postgres', + hostname: process.env.DB_HOSTNAME || 'immich', + name: process.env.DB_DATABASE_NAME || 'immich', + port: process.env.DB_PORT || 5432, + vectorExtension: process.env.DB_VECTOR_EXTENSION || VectorExtension.PG_VECTORS, + skipMigrations: process.env.DB_SKIP_MIGRATIONS ?? false, + }, + + machineLearning: { + enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED ?? true, + url: process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003', + }, + + metadata: { + build: process.env.IMMICH_BUILD, + buildUrl: process.env.IMMICH_BUILD_URL, + buildImage: process.env.IMMICH_BUILD_IMAGE, + buildImageUrl: process.env.IMMICH_BUILD_IMAGE_URL, + repository: process.env.IMMICH_REPOSITORY, + repositoryUrl: process.env.IMMICH_REPOSITORY_URL, + sourceRef: process.env.IMMICH_SOURCE_REF, + sourceCommit: process.env.IMMICH_SOURCE_COMMIT, + sourceUrl: process.env.IMMICH_SOURCE_URL, + }, + + metrics: { + enabled: process.env.IMMICH_METRICS === 'true', + apiPort: process.env.IMMICH_METRICS_API_PORT || 8081, + microservicesPort: process.env.IMMICH_METRICS_MICROSERVICES_PORT || 8082, + hostEnabled: process.env.IMMICH_HOST_METRICS === 'true', + apiEnabled: process.env.IMMICH_HOST_METRICS === 'true', + ioEnabled: process.env.IMMICH_IO_METRICS === 'true', + repoEnabled: process.env.IMMICH_REPO_METRICS === 'true', + jobEnabled: process.env.IMMICH_JOB_METRICS === 'true', + }, + + redis: { + url: process.env.REDIS_URL, + hostname: process.env.REDIS_HOSTNAME || 'redis', + port: process.env.REDIS_PORT || 6379, + dbIndex: process.env.REDIS_DBINDEX || 0, + username: process.env.REDIS_USERNAME || undefined, + password: process.env.REDIS_PASSWORD || undefined, + socket: process.env.REDIS_SOCKET || undefined, + }, +}); +const errors = validateSync(env, {}); +if (errors.length > 0) { + console.error(errors); + throw new Error('Invalid environment variables'); +} + +export const envData = env; diff --git a/server/src/interfaces/logger.interface.ts b/server/src/interfaces/logger.interface.ts index f0afdce2a5..60dbeab814 100644 --- a/server/src/interfaces/logger.interface.ts +++ b/server/src/interfaces/logger.interface.ts @@ -1,11 +1,11 @@ -import { LogLevel } from 'src/config'; +import { LogLevel } from 'src/enum'; export const ILoggerRepository = 'ILoggerRepository'; export interface ILoggerRepository { setAppName(name: string): void; setContext(message: string): void; - setLogLevel(level: LogLevel): void; + setLogLevel(level: LogLevel | false): void; verbose(message: any, ...args: any): void; debug(message: any, ...args: any): void; diff --git a/server/src/main.ts b/server/src/main.ts index e32c3e43ac..48ce179e88 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -2,7 +2,7 @@ import { CommandFactory } from 'nest-commander'; import { fork } from 'node:child_process'; import { Worker } from 'node:worker_threads'; import { ImmichAdminModule } from 'src/app.module'; -import { LogLevel } from 'src/config'; +import { LogLevel } from 'src/enum'; import { getWorkers } from 'src/utils/workers'; const immichApp = process.argv[2] || process.env.IMMICH_APP; diff --git a/server/src/repositories/logger.repository.ts b/server/src/repositories/logger.repository.ts index 1e0c7b74d9..fac46f60fc 100644 --- a/server/src/repositories/logger.repository.ts +++ b/server/src/repositories/logger.repository.ts @@ -1,7 +1,7 @@ import { ConsoleLogger, Injectable, Scope } from '@nestjs/common'; import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-enabled.util'; import { ClsService } from 'nestjs-cls'; -import { LogLevel } from 'src/config'; +import { LogLevel } from 'src/enum'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { LogColor } from 'src/utils/logger'; @@ -25,8 +25,8 @@ export class LoggerRepository extends ConsoleLogger implements ILoggerRepository return isLogLevelEnabled(level, LoggerRepository.logLevels); } - setLogLevel(level: LogLevel): void { - LoggerRepository.logLevels = LOG_LEVELS.slice(LOG_LEVELS.indexOf(level)); + setLogLevel(level: LogLevel | false): void { + LoggerRepository.logLevels = level === false ? [] : LOG_LEVELS.slice(LOG_LEVELS.indexOf(level)); } protected formatContext(context: string): string { diff --git a/server/src/repositories/server-info.repository.ts b/server/src/repositories/server-info.repository.ts index f74eb7dd0d..d3e2dc9f4b 100644 --- a/server/src/repositories/server-info.repository.ts +++ b/server/src/repositories/server-info.repository.ts @@ -5,6 +5,7 @@ import { readFile } from 'node:fs/promises'; import { promisify } from 'node:util'; import sharp from 'sharp'; import { resourcePaths } from 'src/constants'; +import { envData } from 'src/env'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { GitHubRelease, IServerInfoRepository, ServerBuildVersions } from 'src/interfaces/server-info.interface'; import { Instrumentation } from 'src/utils/instrumentation'; @@ -67,7 +68,7 @@ export class ServerInfoRepository implements IServerInfoRepository { .catch(() => this.logger.warn(`Failed to read ${resourcePaths.lockFile}`)); return { - nodejs: nodejsOutput || process.env.NODE_VERSION || '', + nodejs: nodejsOutput || envData.nodeVersion || '', exiftool: await exiftool.version(), ffmpeg: getLockfileVersion('ffmpeg', lockfile) || ffmpegOutput.replaceAll('ffmpeg version', '') || '', libvips: getLockfileVersion('libvips', lockfile) || sharp.versions.vips, diff --git a/server/src/services/database.service.ts b/server/src/services/database.service.ts index a5280ff28b..ef91be79ba 100644 --- a/server/src/services/database.service.ts +++ b/server/src/services/database.service.ts @@ -3,6 +3,7 @@ import { Duration } from 'luxon'; import semver from 'semver'; import { getVectorExtension } from 'src/database.config'; import { OnEmit } from 'src/decorators'; +import { envData } from 'src/env'; import { DatabaseExtension, DatabaseLock, @@ -116,7 +117,7 @@ export class DatabaseService { await this.checkReindexing(); - if (process.env.DB_SKIP_MIGRATIONS !== 'true') { + if (!envData.database.skipMigrations) { await this.databaseRepository.runMigrations(); } }); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index e74335bdc3..2af96ef62a 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -16,6 +16,7 @@ import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { AssetPathType } from 'src/entities/move.entity'; import { AssetFileType, AssetType } from 'src/enum'; +import { envData } from 'src/env'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { @@ -224,7 +225,7 @@ export class MediaService { size, colorspace, quality: image.quality, - processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true', + processInvalidImages: envData.processInvalidImages, }; const outputPath = useExtracted ? extractedPath : asset.originalPath; diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index dd4a4cecf2..ad8f46d233 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -26,6 +26,7 @@ import { AssetEntity } from 'src/entities/asset.entity'; import { PersonPathType } from 'src/entities/move.entity'; import { PersonEntity } from 'src/entities/person.entity'; import { AssetType, Permission, SourceType, SystemMetadataKey } from 'src/enum'; +import { envData } from 'src/env'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; @@ -570,7 +571,7 @@ export class PersonService { colorspace: image.colorspace, quality: image.quality, crop: this.getCrop({ old: { width: oldWidth, height: oldHeight }, new: { width, height } }, { x1, y1, x2, y2 }), - processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true', + processInvalidImages: envData.processInvalidImages, } as const; await this.mediaRepository.generateThumbnail(inputPath, thumbnailPath, thumbnailOptions); diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index 9db90e41b3..788a99f119 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -1,5 +1,5 @@ import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; -import { getBuildMetadata, getServerLicensePublicKey } from 'src/config'; +import { getServerLicensePublicKey } from 'src/config'; import { serverVersion } from 'src/constants'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; @@ -16,6 +16,7 @@ import { UsageByUserDto, } from 'src/dtos/server.dto'; import { SystemMetadataKey } from 'src/enum'; +import { envData } from 'src/env'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; @@ -55,7 +56,6 @@ export class ServerService { async getAboutInfo(): Promise { const version = `v${serverVersion.toString()}`; - const buildMetadata = getBuildMetadata(); const buildVersions = await this.serverInfoRepository.getBuildVersions(); const licensed = await this.systemMetadataRepository.get(SystemMetadataKey.LICENSE); @@ -63,7 +63,7 @@ export class ServerService { version, versionUrl: `https://github.com/immich-app/immich/releases/tag/${version}`, licensed: !!licensed, - ...buildMetadata, + ...envData.buildMetadata, ...buildVersions, }; } diff --git a/server/src/services/system-config.service.ts b/server/src/services/system-config.service.ts index 5ec9ab7a5d..a6059a1163 100644 --- a/server/src/services/system-config.service.ts +++ b/server/src/services/system-config.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { instanceToPlain } from 'class-transformer'; import _ from 'lodash'; -import { LogLevel, SystemConfig, defaults } from 'src/config'; +import { SystemConfig, defaults } from 'src/config'; import { supportedDayTokens, supportedHourTokens, @@ -15,6 +15,7 @@ import { import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnEmit, OnServerEvent } from 'src/decorators'; import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto'; +import { envData } from 'src/env'; import { ArgOf, ClientEvent, IEventRepository, ServerEvent } from 'src/interfaces/event.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; @@ -114,6 +115,6 @@ export class SystemConfigService { } private getEnvLogLevel() { - return process.env.IMMICH_LOG_LEVEL as LogLevel; + return envData.logLevel; } } diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index 468e8c9bdd..d3f5ecf1e1 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -1,12 +1,13 @@ import { Inject, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; import semver, { SemVer } from 'semver'; -import { isDev, serverVersion } from 'src/constants'; +import { serverVersion } from 'src/constants'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnEmit, OnServerEvent } from 'src/decorators'; import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; import { VersionCheckMetadata } from 'src/entities/system-metadata.entity'; import { SystemMetadataKey } from 'src/enum'; +import { envData, ImmichEnv } from 'src/env'; import { ClientEvent, IEventRepository, ServerEvent, ServerEventMap } from 'src/interfaces/event.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; @@ -54,7 +55,7 @@ export class VersionService { try { this.logger.debug('Running version check'); - if (isDev()) { + if (envData.environment === ImmichEnv.DEVELOPMENT) { return JobStatus.SKIPPED; } diff --git a/server/src/utils/instrumentation.ts b/server/src/utils/instrumentation.ts index 484ba5901c..d6a599b7b2 100644 --- a/server/src/utils/instrumentation.ts +++ b/server/src/utils/instrumentation.ts @@ -12,16 +12,14 @@ import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetr import { performance } from 'node:perf_hooks'; import { excludePaths, serverVersion } from 'src/constants'; import { DecorateAll } from 'src/decorators'; +import { envData } from 'src/env'; -let metricsEnabled = process.env.IMMICH_METRICS === 'true'; -export const hostMetrics = - process.env.IMMICH_HOST_METRICS == null ? metricsEnabled : process.env.IMMICH_HOST_METRICS === 'true'; -export const apiMetrics = - process.env.IMMICH_API_METRICS == null ? metricsEnabled : process.env.IMMICH_API_METRICS === 'true'; -export const repoMetrics = - process.env.IMMICH_IO_METRICS == null ? metricsEnabled : process.env.IMMICH_IO_METRICS === 'true'; -export const jobMetrics = - process.env.IMMICH_JOB_METRICS == null ? metricsEnabled : process.env.IMMICH_JOB_METRICS === 'true'; +let metricsEnabled = envData.metrics.enabled; + +export const hostMetrics = metricsEnabled && envData.metrics.hostEnabled; +export const apiMetrics = metricsEnabled && envData.metrics.apiEnabled; +export const repoMetrics = metricsEnabled && envData.metrics.repoEnabled; +export const jobMetrics = metricsEnabled && envData.metrics.jobEnabled; metricsEnabled ||= hostMetrics || apiMetrics || repoMetrics || jobMetrics; if (!metricsEnabled && process.env.OTEL_SDK_DISABLED === undefined) { diff --git a/server/src/utils/logger.ts b/server/src/utils/logger.ts index d4eb02ead2..2351e0ccb5 100644 --- a/server/src/utils/logger.ts +++ b/server/src/utils/logger.ts @@ -1,10 +1,11 @@ import { HttpException } from '@nestjs/common'; +import { envData } from 'src/env'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { TypeORMError } from 'typeorm'; type ColorTextFn = (text: string) => string; -const isColorAllowed = () => !process.env.NO_COLOR; +const isColorAllowed = () => !envData.noColor; const colorIfAllowed = (colorFn: ColorTextFn) => (text: string) => (isColorAllowed() ? colorFn(text) : text); export const LogColor = { diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index 47f3f552c4..e4c5eac20b 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -11,8 +11,9 @@ import _ from 'lodash'; import { writeFileSync } from 'node:fs'; import path from 'node:path'; import { SystemConfig } from 'src/config'; -import { CLIP_MODEL_INFO, isDev, serverVersion } from 'src/constants'; +import { CLIP_MODEL_INFO, serverVersion } from 'src/constants'; import { ImmichCookie, ImmichHeader } from 'src/dtos/auth.dto'; +import { envData, ImmichEnv } from 'src/env'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { Metadata } from 'src/middleware/auth.guard'; @@ -230,7 +231,7 @@ export const useSwagger = (app: INestApplication, force = false) => { SwaggerModule.setup('doc', app, specification, customOptions); - if (isDev() || force) { + if (envData.environment === ImmichEnv.DEVELOPMENT || force) { // Generate API Documentation only in development mode const outputPath = path.resolve(process.cwd(), '../open-api/immich-openapi-specs.json'); writeFileSync(outputPath, JSON.stringify(patchOpenAPI(specification), null, 2), { encoding: 'utf8' }); diff --git a/server/src/workers/api.ts b/server/src/workers/api.ts index 629c50c653..43b04086c5 100644 --- a/server/src/workers/api.ts +++ b/server/src/workers/api.ts @@ -5,7 +5,8 @@ import cookieParser from 'cookie-parser'; import { existsSync } from 'node:fs'; import sirv from 'sirv'; import { ApiModule } from 'src/app.module'; -import { envName, excludePaths, isDev, resourcePaths, serverVersion } from 'src/constants'; +import { excludePaths, resourcePaths, serverVersion } from 'src/constants'; +import { envData, ImmichEnv } from 'src/env'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { WebSocketAdapter } from 'src/middleware/websocket.adapter'; import { ApiService } from 'src/services/api.service'; @@ -13,35 +14,24 @@ import { isStartUpError } from 'src/utils/events'; import { otelStart } from 'src/utils/instrumentation'; import { useSwagger } from 'src/utils/misc'; -const host = process.env.HOST; - -function parseTrustedProxy(input?: string) { - if (!input) { - return []; - } - // Split on ',' char to allow multiple IPs - return input.split(','); -} - async function bootstrap() { process.title = 'immich-api'; - const otelPort = Number.parseInt(process.env.IMMICH_API_METRICS_PORT ?? '8081'); - const trustedProxies = parseTrustedProxy(process.env.IMMICH_TRUSTED_PROXIES ?? ''); - otelStart(otelPort); + const { port, metrics } = envData; + + otelStart(metrics.apiPort); - const port = Number(process.env.IMMICH_PORT) || 3001; const app = await NestFactory.create(ApiModule, { bufferLogs: true }); const logger = await app.resolve(ILoggerRepository); logger.setAppName('Api'); logger.setContext('Bootstrap'); app.useLogger(logger); - app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal', ...trustedProxies]); + app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal', ...envData.trustedProxies]); app.set('etag', 'strong'); app.use(cookieParser()); app.use(json({ limit: '10mb' })); - if (isDev()) { + if (envData.environment === ImmichEnv.DEVELOPMENT) { app.enableCors(); } app.useWebSocketAdapter(new WebSocketAdapter(app)); @@ -67,10 +57,13 @@ async function bootstrap() { } app.use(app.get(ApiService).ssr(excludePaths)); + const { host } = envData; const server = await (host ? app.listen(port, host) : app.listen(port)); server.requestTimeout = 30 * 60 * 1000; - logger.log(`Immich Server is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `); + logger.log( + `Immich Server is listening on ${await app.getUrl()} [v${serverVersion}] [${envData.environment.toUpperCase()}] `, + ); } bootstrap().catch((error) => { diff --git a/server/src/workers/microservices.ts b/server/src/workers/microservices.ts index 789b6f5287..5bfe7d5a3f 100644 --- a/server/src/workers/microservices.ts +++ b/server/src/workers/microservices.ts @@ -1,16 +1,15 @@ import { NestFactory } from '@nestjs/core'; import { isMainThread } from 'node:worker_threads'; import { MicroservicesModule } from 'src/app.module'; -import { envName, serverVersion } from 'src/constants'; +import { serverVersion } from 'src/constants'; +import { envData } from 'src/env'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { WebSocketAdapter } from 'src/middleware/websocket.adapter'; import { isStartUpError } from 'src/utils/events'; import { otelStart } from 'src/utils/instrumentation'; export async function bootstrap() { - const otelPort = Number.parseInt(process.env.IMMICH_MICROSERVICES_METRICS_PORT ?? '8082'); - - otelStart(otelPort); + otelStart(envData.metrics.microservicesPort); const app = await NestFactory.create(MicroservicesModule, { bufferLogs: true }); const logger = await app.resolve(ILoggerRepository); @@ -21,7 +20,7 @@ export async function bootstrap() { await app.listen(0); - logger.log(`Immich Microservices is running [v${serverVersion}] [${envName}] `); + logger.log(`Immich Microservices is running [v${serverVersion}] [${envData.environment.toUpperCase()}] `); } if (!isMainThread) {