diff --git a/server/src/bin/sync-open-api.ts b/server/src/bin/sync-open-api.ts index 70e2bb8c35..d5316b34cf 100644 --- a/server/src/bin/sync-open-api.ts +++ b/server/src/bin/sync-open-api.ts @@ -7,7 +7,7 @@ import { useSwagger } from 'src/utils/misc'; const sync = async () => { const app = await NestFactory.create(ApiModule, { preview: true }); - useSwagger(app, true); + useSwagger(app, { write: true }); await app.close(); }; diff --git a/server/src/config.ts b/server/src/config.ts index 2e11f740d3..d207d6763c 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -12,6 +12,7 @@ import { Colorspace, CQMode, ImageFormat, + ImmichEnvironment, LogLevel, ToneMapping, TranscodeHWAccel, @@ -322,7 +323,10 @@ export const immichAppConfig: ConfigModuleOptions = { envFilePath: '.env', isGlobal: true, validationSchema: Joi.object({ - IMMICH_ENV: Joi.string().optional().valid('development', 'testing', 'production').default('production'), + IMMICH_ENV: Joi.string() + .optional() + .valid(...Object.values(ImmichEnvironment)) + .default(ImmichEnvironment.PRODUCTION), IMMICH_LOG_LEVEL: Joi.string() .optional() .valid(...Object.values(LogLevel)), diff --git a/server/src/constants.ts b/server/src/constants.ts index 8115101ca0..c62c06ffa2 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -21,7 +21,6 @@ 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'; const HOST_SERVER_PORT = process.env.IMMICH_PORT || '2283'; export const DEFAULT_EXTERNAL_DOMAIN = 'http://localhost:' + HOST_SERVER_PORT; diff --git a/server/src/enum.ts b/server/src/enum.ts index 757291b118..d1a76573d1 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -328,3 +328,9 @@ export enum PaginationMode { LIMIT_OFFSET = 'limit-offset', SKIP_TAKE = 'skip-take', } + +export enum ImmichEnvironment { + DEVELOPMENT = 'development', + TESTING = 'testing', + PRODUCTION = 'production', +} diff --git a/server/src/interfaces/config.interface.ts b/server/src/interfaces/config.interface.ts index b81c4fa48e..9d785e316a 100644 --- a/server/src/interfaces/config.interface.ts +++ b/server/src/interfaces/config.interface.ts @@ -1,16 +1,23 @@ +import { ImmichEnvironment, LogLevel } from 'src/enum'; import { VectorExtension } from 'src/interfaces/database.interface'; export const IConfigRepository = 'IConfigRepository'; export interface EnvData { + environment: ImmichEnvironment; configFile?: string; + logLevel?: LogLevel; + database: { skipMigrations: boolean; vectorExtension: VectorExtension; }; + storage: { ignoreMountCheckErrors: boolean; }; + + nodeVersion?: string; } export interface IConfigRepository { diff --git a/server/src/interfaces/logger.interface.ts b/server/src/interfaces/logger.interface.ts index 42523afa6b..5a4f1ad9d7 100644 --- a/server/src/interfaces/logger.interface.ts +++ b/server/src/interfaces/logger.interface.ts @@ -5,7 +5,7 @@ export const ILoggerRepository = 'ILoggerRepository'; export interface ILoggerRepository { setAppName(name: string): void; setContext(message: string): void; - setLogLevel(level: LogLevel): void; + setLogLevel(level: LogLevel | false): void; isLevelEnabled(level: LogLevel): boolean; verbose(message: any, ...args: any): void; diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index fe952c4f77..0d8e3f76c1 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -1,12 +1,17 @@ import { Injectable } from '@nestjs/common'; import { getVectorExtension } from 'src/database.config'; +import { ImmichEnvironment, LogLevel } from 'src/enum'; import { EnvData, IConfigRepository } from 'src/interfaces/config.interface'; +// TODO replace src/config validation with class-validator, here + @Injectable() export class ConfigRepository implements IConfigRepository { getEnv(): EnvData { return { + environment: process.env.IMMICH_ENV as ImmichEnvironment, configFile: process.env.IMMICH_CONFIG_FILE, + logLevel: process.env.IMMICH_LOG_LEVEL as LogLevel, database: { skipMigrations: process.env.DB_SKIP_MIGRATIONS === 'true', vectorExtension: getVectorExtension(), diff --git a/server/src/repositories/logger.repository.ts b/server/src/repositories/logger.repository.ts index 1d7e734e73..08fb6e7973 100644 --- a/server/src/repositories/logger.repository.ts +++ b/server/src/repositories/logger.repository.ts @@ -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 ? 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..ae04f600c0 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 { IConfigRepository } from 'src/interfaces/config.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { GitHubRelease, IServerInfoRepository, ServerBuildVersions } from 'src/interfaces/server-info.interface'; import { Instrumentation } from 'src/utils/instrumentation'; @@ -37,7 +38,10 @@ const getLockfileVersion = (name: string, lockfile?: BuildLockfile) => { @Instrumentation() @Injectable() export class ServerInfoRepository implements IServerInfoRepository { - constructor(@Inject(ILoggerRepository) private logger: ILoggerRepository) { + constructor( + @Inject(IConfigRepository) private configRepository: IConfigRepository, + @Inject(ILoggerRepository) private logger: ILoggerRepository, + ) { this.logger.setContext(ServerInfoRepository.name); } @@ -56,6 +60,8 @@ export class ServerInfoRepository implements IServerInfoRepository { } async getBuildVersions(): Promise { + const { nodeVersion } = this.configRepository.getEnv(); + const [nodejsOutput, ffmpegOutput, magickOutput] = await Promise.all([ maybeFirstLine('node --version'), maybeFirstLine('ffmpeg -version'), @@ -67,7 +73,7 @@ export class ServerInfoRepository implements IServerInfoRepository { .catch(() => this.logger.warn(`Failed to read ${resourcePaths.lockFile}`)); return { - nodejs: nodejsOutput || process.env.NODE_VERSION || '', + nodejs: nodejsOutput || 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/system-config.service.ts b/server/src/services/system-config.service.ts index 017dce9894..ff749f1105 100644 --- a/server/src/services/system-config.service.ts +++ b/server/src/services/system-config.service.ts @@ -14,7 +14,6 @@ import { } from 'src/constants'; import { OnEvent } from 'src/decorators'; import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto'; -import { LogLevel } from 'src/enum'; import { IConfigRepository } from 'src/interfaces/config.interface'; import { ArgOf, IEventRepository } from 'src/interfaces/event.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; @@ -52,7 +51,7 @@ export class SystemConfigService extends BaseService { @OnEvent({ name: 'config.update', server: true }) onConfigUpdate({ newConfig: { logging } }: ArgOf<'config.update'>) { - const envLevel = this.getEnvLogLevel(); + const { logLevel: envLevel } = this.configRepository.getEnv(); const configLevel = logging.enabled ? logging.level : false; const level = envLevel ?? configLevel; this.logger.setLogLevel(level); @@ -63,7 +62,8 @@ export class SystemConfigService extends BaseService { @OnEvent({ name: 'config.validate' }) onConfigValidate({ newConfig, oldConfig }: ArgOf<'config.validate'>) { - if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && this.getEnvLogLevel()) { + const { logLevel } = this.configRepository.getEnv(); + if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && logLevel) { throw new Error('Logging cannot be changed while the environment variable IMMICH_LOG_LEVEL is set.'); } } @@ -109,8 +109,4 @@ export class SystemConfigService extends BaseService { const { theme } = await this.getConfig({ withCache: false }); return theme.customCss; } - - private getEnvLogLevel() { - return process.env.IMMICH_LOG_LEVEL as LogLevel; - } } diff --git a/server/src/services/version.service.spec.ts b/server/src/services/version.service.spec.ts index a71538ea09..ebc5d4b232 100644 --- a/server/src/services/version.service.spec.ts +++ b/server/src/services/version.service.spec.ts @@ -1,6 +1,6 @@ import { DateTime } from 'luxon'; import { serverVersion } from 'src/constants'; -import { SystemMetadataKey } from 'src/enum'; +import { ImmichEnvironment, SystemMetadataKey } from 'src/enum'; import { IConfigRepository } from 'src/interfaces/config.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; @@ -10,7 +10,7 @@ import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; import { VersionService } from 'src/services/version.service'; -import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; +import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; @@ -111,11 +111,11 @@ describe(VersionService.name, () => { describe('handVersionCheck', () => { beforeEach(() => { - process.env.IMMICH_ENV = 'production'; + configMock.getEnv.mockReturnValue(mockEnvData({ environment: ImmichEnvironment.PRODUCTION })); }); it('should not run in dev mode', async () => { - process.env.IMMICH_ENV = 'development'; + configMock.getEnv.mockReturnValue(mockEnvData({ environment: ImmichEnvironment.DEVELOPMENT })); await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.SKIPPED); }); diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index 91b473f128..60ea388e5d 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -1,11 +1,11 @@ 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 { OnEvent } 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 { ImmichEnvironment, SystemMetadataKey } from 'src/enum'; import { IConfigRepository } from 'src/interfaces/config.interface'; import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; import { ArgOf, IEventRepository } from 'src/interfaces/event.interface'; @@ -71,7 +71,8 @@ export class VersionService extends BaseService { try { this.logger.debug('Running version check'); - if (isDev()) { + const { environment } = this.configRepository.getEnv(); + if (environment === ImmichEnvironment.DEVELOPMENT) { return JobStatus.SKIPPED; } diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index 2cef33d4f5..9bea4e9585 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -11,7 +11,7 @@ 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 { MetadataKey } from 'src/enum'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; @@ -193,7 +193,7 @@ const patchOpenAPI = (document: OpenAPIObject) => { return document; }; -export const useSwagger = (app: INestApplication, force = false) => { +export const useSwagger = (app: INestApplication, { write }: { write: boolean }) => { const config = new DocumentBuilder() .setTitle('Immich') .setDescription('Immich API') @@ -230,7 +230,7 @@ export const useSwagger = (app: INestApplication, force = false) => { SwaggerModule.setup('doc', app, specification, customOptions); - if (isDev() || force) { + if (write) { // 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..7e7384f95f 100644 --- a/server/src/workers/api.ts +++ b/server/src/workers/api.ts @@ -5,7 +5,9 @@ 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 { envName, excludePaths, resourcePaths, serverVersion } from 'src/constants'; +import { ImmichEnvironment } from 'src/enum'; +import { IConfigRepository } from 'src/interfaces/config.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { WebSocketAdapter } from 'src/middleware/websocket.adapter'; import { ApiService } from 'src/services/api.service'; @@ -33,6 +35,10 @@ async function bootstrap() { const port = Number(process.env.IMMICH_PORT) || 3001; const app = await NestFactory.create(ApiModule, { bufferLogs: true }); const logger = await app.resolve(ILoggerRepository); + const configRepository = app.get(IConfigRepository); + + const { environment } = configRepository.getEnv(); + const isDev = environment === ImmichEnvironment.DEVELOPMENT; logger.setAppName('Api'); logger.setContext('Bootstrap'); @@ -41,11 +47,11 @@ async function bootstrap() { app.set('etag', 'strong'); app.use(cookieParser()); app.use(json({ limit: '10mb' })); - if (isDev()) { + if (isDev) { app.enableCors(); } app.useWebSocketAdapter(new WebSocketAdapter(app)); - useSwagger(app); + useSwagger(app, { write: isDev }); app.setGlobalPrefix('api', { exclude: excludePaths }); if (existsSync(resourcePaths.web.root)) { diff --git a/server/test/repositories/config.repository.mock.ts b/server/test/repositories/config.repository.mock.ts index d18aa1b981..e61185225f 100644 --- a/server/test/repositories/config.repository.mock.ts +++ b/server/test/repositories/config.repository.mock.ts @@ -1,12 +1,16 @@ +import { ImmichEnvironment } from 'src/enum'; import { EnvData, IConfigRepository } from 'src/interfaces/config.interface'; import { DatabaseExtension } from 'src/interfaces/database.interface'; import { Mocked, vitest } from 'vitest'; const envData: EnvData = { + environment: ImmichEnvironment.PRODUCTION, + database: { skipMigrations: false, vectorExtension: DatabaseExtension.VECTORS, }, + storage: { ignoreMountCheckErrors: false, },