|
@@ -10,10 +10,13 @@ import {
|
|
|
VideoCodec,
|
|
|
} from '@app/infra/entities';
|
|
|
import { BadRequestException, ForbiddenException, Injectable, Logger } from '@nestjs/common';
|
|
|
+import { plainToClass } from 'class-transformer';
|
|
|
+import { validate } from 'class-validator';
|
|
|
import * as _ from 'lodash';
|
|
|
import { Subject } from 'rxjs';
|
|
|
import { DeepPartial } from 'typeorm';
|
|
|
import { QueueName } from '../job/job.constants';
|
|
|
+import { SystemConfigDto } from './dto';
|
|
|
import { ISystemConfigRepository } from './system-config.repository';
|
|
|
|
|
|
export type SystemConfigValidator = (config: SystemConfig) => void | Promise<void>;
|
|
@@ -87,6 +90,7 @@ export enum FeatureFlag {
|
|
|
OAUTH = 'oauth',
|
|
|
OAUTH_AUTO_LAUNCH = 'oauthAutoLaunch',
|
|
|
PASSWORD_LOGIN = 'passwordLogin',
|
|
|
+ CONFIG_FILE = 'configFile',
|
|
|
}
|
|
|
|
|
|
export type FeatureFlags = Record<FeatureFlag, boolean>;
|
|
@@ -97,6 +101,7 @@ const singleton = new Subject<SystemConfig>();
|
|
|
export class SystemConfigCore {
|
|
|
private logger = new Logger(SystemConfigCore.name);
|
|
|
private validators: SystemConfigValidator[] = [];
|
|
|
+ private configCache: SystemConfig | null = null;
|
|
|
|
|
|
public config$ = singleton;
|
|
|
|
|
@@ -120,6 +125,8 @@ export class SystemConfigCore {
|
|
|
throw new BadRequestException('OAuth is not enabled');
|
|
|
case FeatureFlag.PASSWORD_LOGIN:
|
|
|
throw new BadRequestException('Password login is not enabled');
|
|
|
+ case FeatureFlag.CONFIG_FILE:
|
|
|
+ throw new BadRequestException('Config file is not set');
|
|
|
default:
|
|
|
throw new ForbiddenException(`Missing required feature: ${feature}`);
|
|
|
}
|
|
@@ -146,6 +153,7 @@ export class SystemConfigCore {
|
|
|
[FeatureFlag.OAUTH]: config.oauth.enabled,
|
|
|
[FeatureFlag.OAUTH_AUTO_LAUNCH]: config.oauth.autoLaunch,
|
|
|
[FeatureFlag.PASSWORD_LOGIN]: config.passwordLogin.enabled,
|
|
|
+ [FeatureFlag.CONFIG_FILE]: !!process.env.IMMICH_CONFIG_FILE,
|
|
|
};
|
|
|
}
|
|
|
|
|
@@ -157,18 +165,16 @@ export class SystemConfigCore {
|
|
|
this.validators.push(validator);
|
|
|
}
|
|
|
|
|
|
- public async getConfig() {
|
|
|
- const overrides = await this.repository.load();
|
|
|
- const config: DeepPartial<SystemConfig> = {};
|
|
|
- for (const { key, value } of overrides) {
|
|
|
- // set via dot notation
|
|
|
- _.set(config, key, value);
|
|
|
- }
|
|
|
-
|
|
|
- return _.defaultsDeep(config, defaults) as SystemConfig;
|
|
|
+ public getConfig(force = false): Promise<SystemConfig> {
|
|
|
+ const configFilePath = process.env.IMMICH_CONFIG_FILE;
|
|
|
+ return configFilePath ? this.loadFromFile(configFilePath, force) : this.loadFromDatabase();
|
|
|
}
|
|
|
|
|
|
public async updateConfig(config: SystemConfig): Promise<SystemConfig> {
|
|
|
+ if (await this.hasFeature(FeatureFlag.CONFIG_FILE)) {
|
|
|
+ throw new BadRequestException('Cannot update configuration while IMMICH_CONFIG_FILE is in use');
|
|
|
+ }
|
|
|
+
|
|
|
try {
|
|
|
for (const validator of this.validators) {
|
|
|
await validator(config);
|
|
@@ -211,8 +217,45 @@ export class SystemConfigCore {
|
|
|
}
|
|
|
|
|
|
public async refreshConfig() {
|
|
|
- const newConfig = await this.getConfig();
|
|
|
+ const newConfig = await this.getConfig(true);
|
|
|
|
|
|
this.config$.next(newConfig);
|
|
|
}
|
|
|
+
|
|
|
+ private async loadFromDatabase() {
|
|
|
+ const config: DeepPartial<SystemConfig> = {};
|
|
|
+ const overrides = await this.repository.load();
|
|
|
+ for (const { key, value } of overrides) {
|
|
|
+ // set via dot notation
|
|
|
+ _.set(config, key, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ return _.defaultsDeep(config, defaults) as SystemConfig;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async loadFromFile(filepath: string, force = false) {
|
|
|
+ if (force || !this.configCache) {
|
|
|
+ try {
|
|
|
+ const overrides = JSON.parse((await this.repository.readFile(filepath)).toString());
|
|
|
+ const config = plainToClass(SystemConfigDto, _.defaultsDeep(overrides, defaults));
|
|
|
+
|
|
|
+ const errors = await validate(config, {
|
|
|
+ whitelist: true,
|
|
|
+ forbidNonWhitelisted: true,
|
|
|
+ forbidUnknownValues: true,
|
|
|
+ });
|
|
|
+ if (errors.length > 0) {
|
|
|
+ this.logger.error('Validation error', errors);
|
|
|
+ throw new Error(`Invalid value(s) in file: ${errors}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ this.configCache = config;
|
|
|
+ } catch (error: Error | any) {
|
|
|
+ this.logger.error(`Unable to load configuration file: ${filepath} due to ${error}`, error?.stack);
|
|
|
+ throw new Error('Invalid configuration file');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return this.configCache;
|
|
|
+ }
|
|
|
}
|