refactor(server): config service (#13066)

* refactor(server): config service

* fix: function renaming

---------

Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
This commit is contained in:
Jason Rasmussen 2024-09-30 17:31:21 -04:00 committed by GitHub
parent f63d251490
commit a019fb670e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 327 additions and 361 deletions

View file

@ -1,7 +1,6 @@
import { randomUUID } from 'node:crypto';
import { dirname, join, resolve } from 'node:path';
import { APP_MEDIA_LOCATION } from 'src/constants';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { AssetEntity } from 'src/entities/asset.entity';
import { PersonEntity } from 'src/entities/person.entity';
import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum';
@ -13,6 +12,7 @@ import { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { getAssetFiles } from 'src/utils/asset.util';
import { getConfig } from 'src/utils/config';
export const THUMBNAIL_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.THUMBNAILS));
export const ENCODED_VIDEO_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.ENCODED_VIDEO));
@ -34,18 +34,15 @@ export type GeneratedAssetType = GeneratedImageType | AssetPathType.ENCODED_VIDE
let instance: StorageCore | null;
export class StorageCore {
private configCore;
private constructor(
private assetRepository: IAssetRepository,
private cryptoRepository: ICryptoRepository,
private moveRepository: IMoveRepository,
private personRepository: IPersonRepository,
private storageRepository: IStorageRepository,
systemMetadataRepository: ISystemMetadataRepository,
private systemMetadataRepository: ISystemMetadataRepository,
private logger: ILoggerRepository,
) {
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
}
) {}
static create(
assetRepository: IAssetRepository,
@ -248,7 +245,8 @@ export class StorageCore {
this.logger.warn(`Unable to complete move. File size mismatch: ${newPathSize} !== ${oldPathSize}`);
return false;
}
const config = await this.configCore.getConfig({ withCache: true });
const repos = { metadataRepo: this.systemMetadataRepository, logger: this.logger };
const config = await getConfig(repos, { withCache: true });
if (assetInfo && config.storageTemplate.hashVerificationEnabled) {
const { checksum } = assetInfo;
const newChecksum = await this.cryptoRepository.hashFile(newPath);

View file

@ -1,143 +0,0 @@
import { Injectable } from '@nestjs/common';
import AsyncLock from 'async-lock';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { load as loadYaml } from 'js-yaml';
import * as _ from 'lodash';
import { SystemConfig, defaults } from 'src/config';
import { SystemConfigDto } from 'src/dtos/system-config.dto';
import { SystemMetadataKey } from 'src/enum';
import { DatabaseLock } from 'src/interfaces/database.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { getKeysDeep, unsetDeep } from 'src/utils/misc';
import { DeepPartial } from 'typeorm';
export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise<void>;
let instance: SystemConfigCore | null;
@Injectable()
export class SystemConfigCore {
private readonly asyncLock = new AsyncLock();
private config: SystemConfig | null = null;
private lastUpdated: number | null = null;
private constructor(
private repository: ISystemMetadataRepository,
private logger: ILoggerRepository,
) {}
static create(repository: ISystemMetadataRepository, logger: ILoggerRepository) {
if (!instance) {
instance = new SystemConfigCore(repository, logger);
}
return instance;
}
static reset() {
instance = null;
}
invalidateCache() {
this.config = null;
this.lastUpdated = null;
}
async getConfig({ withCache }: { withCache: boolean }): Promise<SystemConfig> {
if (!withCache || !this.config) {
const lastUpdated = this.lastUpdated;
await this.asyncLock.acquire(DatabaseLock[DatabaseLock.GetSystemConfig], async () => {
if (lastUpdated === this.lastUpdated) {
this.config = await this.buildConfig();
this.lastUpdated = Date.now();
}
});
}
return this.config!;
}
async updateConfig(newConfig: SystemConfig): Promise<SystemConfig> {
// get the difference between the new config and the default config
const partialConfig: DeepPartial<SystemConfig> = {};
for (const property of getKeysDeep(defaults)) {
const newValue = _.get(newConfig, property);
const isEmpty = newValue === undefined || newValue === null || newValue === '';
const defaultValue = _.get(defaults, property);
const isEqual = newValue === defaultValue || _.isEqual(newValue, defaultValue);
if (isEmpty || isEqual) {
continue;
}
_.set(partialConfig, property, newValue);
}
await this.repository.set(SystemMetadataKey.SYSTEM_CONFIG, partialConfig);
return this.getConfig({ withCache: false });
}
isUsingConfigFile() {
return !!process.env.IMMICH_CONFIG_FILE;
}
private async buildConfig() {
// load partial
const partial = this.isUsingConfigFile()
? await this.loadFromFile(process.env.IMMICH_CONFIG_FILE as string)
: await this.repository.get(SystemMetadataKey.SYSTEM_CONFIG);
// merge with defaults
const config = _.cloneDeep(defaults);
for (const property of getKeysDeep(partial)) {
_.set(config, property, _.get(partial, property));
}
// check for extra properties
const unknownKeys = _.cloneDeep(config);
for (const property of getKeysDeep(defaults)) {
unsetDeep(unknownKeys, property);
}
if (!_.isEmpty(unknownKeys)) {
this.logger.warn(`Unknown keys found: ${JSON.stringify(unknownKeys, null, 2)}`);
}
// validate full config
const errors = await validate(plainToInstance(SystemConfigDto, config));
if (errors.length > 0) {
if (this.isUsingConfigFile()) {
throw new Error(`Invalid value(s) in file: ${errors}`);
} else {
this.logger.error('Validation error', errors);
}
}
if (config.server.externalDomain.length > 0) {
config.server.externalDomain = new URL(config.server.externalDomain).origin;
}
if (!config.ffmpeg.acceptedVideoCodecs.includes(config.ffmpeg.targetVideoCodec)) {
config.ffmpeg.acceptedVideoCodecs.push(config.ffmpeg.targetVideoCodec);
}
if (!config.ffmpeg.acceptedAudioCodecs.includes(config.ffmpeg.targetAudioCodec)) {
config.ffmpeg.acceptedAudioCodecs.push(config.ffmpeg.targetAudioCodec);
}
return config;
}
private async loadFromFile(filepath: string) {
try {
const file = await this.repository.readFile(filepath);
return loadYaml(file.toString()) as unknown;
} catch (error: Error | any) {
this.logger.error(`Unable to load configuration file: ${filepath}`);
this.logger.error(error);
throw error;
}
}
}