mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 04:06:26 -04:00
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:
parent
f63d251490
commit
a019fb670e
@ -66,7 +66,7 @@ export class ServerInfoController {
|
|||||||
@Get('config')
|
@Get('config')
|
||||||
@EndpointLifecycle({ deprecatedAt: 'v1.107.0' })
|
@EndpointLifecycle({ deprecatedAt: 'v1.107.0' })
|
||||||
getServerConfig(): Promise<ServerConfigDto> {
|
getServerConfig(): Promise<ServerConfigDto> {
|
||||||
return this.service.getConfig();
|
return this.service.getSystemConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('statistics')
|
@Get('statistics')
|
||||||
|
@ -58,7 +58,7 @@ export class ServerController {
|
|||||||
|
|
||||||
@Get('config')
|
@Get('config')
|
||||||
getServerConfig(): Promise<ServerConfigDto> {
|
getServerConfig(): Promise<ServerConfigDto> {
|
||||||
return this.service.getConfig();
|
return this.service.getSystemConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('statistics')
|
@Get('statistics')
|
||||||
|
@ -13,7 +13,7 @@ export class SystemConfigController {
|
|||||||
@Get()
|
@Get()
|
||||||
@Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true })
|
@Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true })
|
||||||
getConfig(): Promise<SystemConfigDto> {
|
getConfig(): Promise<SystemConfigDto> {
|
||||||
return this.service.getConfig();
|
return this.service.getSystemConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('defaults')
|
@Get('defaults')
|
||||||
@ -25,7 +25,7 @@ export class SystemConfigController {
|
|||||||
@Put()
|
@Put()
|
||||||
@Authenticated({ permission: Permission.SYSTEM_CONFIG_UPDATE, admin: true })
|
@Authenticated({ permission: Permission.SYSTEM_CONFIG_UPDATE, admin: true })
|
||||||
updateConfig(@Body() dto: SystemConfigDto): Promise<SystemConfigDto> {
|
updateConfig(@Body() dto: SystemConfigDto): Promise<SystemConfigDto> {
|
||||||
return this.service.updateConfig(dto);
|
return this.service.updateSystemConfig(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('storage-template-options')
|
@Get('storage-template-options')
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { dirname, join, resolve } from 'node:path';
|
import { dirname, join, resolve } from 'node:path';
|
||||||
import { APP_MEDIA_LOCATION } from 'src/constants';
|
import { APP_MEDIA_LOCATION } from 'src/constants';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { PersonEntity } from 'src/entities/person.entity';
|
import { PersonEntity } from 'src/entities/person.entity';
|
||||||
import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum';
|
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 { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { getAssetFiles } from 'src/utils/asset.util';
|
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 THUMBNAIL_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.THUMBNAILS));
|
||||||
export const ENCODED_VIDEO_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.ENCODED_VIDEO));
|
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;
|
let instance: StorageCore | null;
|
||||||
|
|
||||||
export class StorageCore {
|
export class StorageCore {
|
||||||
private configCore;
|
|
||||||
private constructor(
|
private constructor(
|
||||||
private assetRepository: IAssetRepository,
|
private assetRepository: IAssetRepository,
|
||||||
private cryptoRepository: ICryptoRepository,
|
private cryptoRepository: ICryptoRepository,
|
||||||
private moveRepository: IMoveRepository,
|
private moveRepository: IMoveRepository,
|
||||||
private personRepository: IPersonRepository,
|
private personRepository: IPersonRepository,
|
||||||
private storageRepository: IStorageRepository,
|
private storageRepository: IStorageRepository,
|
||||||
systemMetadataRepository: ISystemMetadataRepository,
|
private systemMetadataRepository: ISystemMetadataRepository,
|
||||||
private logger: ILoggerRepository,
|
private logger: ILoggerRepository,
|
||||||
) {
|
) {}
|
||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
static create(
|
static create(
|
||||||
assetRepository: IAssetRepository,
|
assetRepository: IAssetRepository,
|
||||||
@ -248,7 +245,8 @@ export class StorageCore {
|
|||||||
this.logger.warn(`Unable to complete move. File size mismatch: ${newPathSize} !== ${oldPathSize}`);
|
this.logger.warn(`Unable to complete move. File size mismatch: ${newPathSize} !== ${oldPathSize}`);
|
||||||
return false;
|
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) {
|
if (assetInfo && config.storageTemplate.hashVerificationEnabled) {
|
||||||
const { checksum } = assetInfo;
|
const { checksum } = assetInfo;
|
||||||
const newChecksum = await this.cryptoRepository.hashFile(newPath);
|
const newChecksum = await this.cryptoRepository.hashFile(newPath);
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,6 @@
|
|||||||
import { BadRequestException, Inject } from '@nestjs/common';
|
import { BadRequestException, Inject } from '@nestjs/common';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { DateTime, Duration } from 'luxon';
|
import { DateTime, Duration } from 'luxon';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import {
|
import {
|
||||||
AssetResponseDto,
|
AssetResponseDto,
|
||||||
MemoryLaneResponseDto,
|
MemoryLaneResponseDto,
|
||||||
@ -38,13 +37,12 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
|||||||
import { IStackRepository } from 'src/interfaces/stack.interface';
|
import { IStackRepository } from 'src/interfaces/stack.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { requireAccess } from 'src/utils/access';
|
import { requireAccess } from 'src/utils/access';
|
||||||
import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util';
|
import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
|
|
||||||
export class AssetService {
|
export class AssetService extends BaseService {
|
||||||
private configCore: SystemConfigCore;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
@ -54,10 +52,10 @@ export class AssetService {
|
|||||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||||
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
||||||
@Inject(IStackRepository) private stackRepository: IStackRepository,
|
@Inject(IStackRepository) private stackRepository: IStackRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
|
super(systemMetadataRepository, logger);
|
||||||
this.logger.setContext(AssetService.name);
|
this.logger.setContext(AssetService.name);
|
||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMemoryLane(auth: AuthDto, dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
|
async getMemoryLane(auth: AuthDto, dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
|
||||||
@ -214,7 +212,7 @@ export class AssetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleAssetDeletionCheck(): Promise<JobStatus> {
|
async handleAssetDeletionCheck(): Promise<JobStatus> {
|
||||||
const config = await this.configCore.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
const trashedDays = config.trash.enabled ? config.trash.days : 0;
|
const trashedDays = config.trash.enabled ? config.trash.days : 0;
|
||||||
const trashedBefore = DateTime.now()
|
const trashedBefore = DateTime.now()
|
||||||
.minus(Duration.fromObject({ days: trashedDays }))
|
.minus(Duration.fromObject({ days: trashedDays }))
|
||||||
|
@ -13,7 +13,6 @@ import { IncomingHttpHeaders } from 'node:http';
|
|||||||
import { Issuer, UserinfoResponse, custom, generators } from 'openid-client';
|
import { Issuer, UserinfoResponse, custom, generators } from 'openid-client';
|
||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
|
import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import {
|
import {
|
||||||
AuthDto,
|
AuthDto,
|
||||||
ChangePasswordDto,
|
ChangePasswordDto,
|
||||||
@ -39,6 +38,7 @@ import { ISessionRepository } from 'src/interfaces/session.interface';
|
|||||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { isGranted } from 'src/utils/access';
|
import { isGranted } from 'src/utils/access';
|
||||||
import { HumanReadableSize } from 'src/utils/bytes';
|
import { HumanReadableSize } from 'src/utils/bytes';
|
||||||
import { createUser } from 'src/utils/user';
|
import { createUser } from 'src/utils/user';
|
||||||
@ -70,27 +70,25 @@ export type ValidateRequest = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService extends BaseService {
|
||||||
private configCore: SystemConfigCore;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
@Inject(ISessionRepository) private sessionRepository: ISessionRepository,
|
@Inject(ISessionRepository) private sessionRepository: ISessionRepository,
|
||||||
@Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
|
@Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
|
||||||
@Inject(IKeyRepository) private keyRepository: IKeyRepository,
|
@Inject(IKeyRepository) private keyRepository: IKeyRepository,
|
||||||
) {
|
) {
|
||||||
|
super(systemMetadataRepository, logger);
|
||||||
this.logger.setContext(AuthService.name);
|
this.logger.setContext(AuthService.name);
|
||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
|
|
||||||
|
|
||||||
custom.setHttpOptionsDefaults({ timeout: 30_000 });
|
custom.setHttpOptionsDefaults({ timeout: 30_000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(dto: LoginCredentialDto, details: LoginDetails) {
|
async login(dto: LoginCredentialDto, details: LoginDetails) {
|
||||||
const config = await this.configCore.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
if (!config.passwordLogin.enabled) {
|
if (!config.passwordLogin.enabled) {
|
||||||
throw new UnauthorizedException('Password login has been disabled');
|
throw new UnauthorizedException('Password login has been disabled');
|
||||||
}
|
}
|
||||||
@ -212,7 +210,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async authorize(dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
|
async authorize(dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
|
||||||
const config = await this.configCore.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
if (!config.oauth.enabled) {
|
if (!config.oauth.enabled) {
|
||||||
throw new BadRequestException('OAuth is not enabled');
|
throw new BadRequestException('OAuth is not enabled');
|
||||||
}
|
}
|
||||||
@ -228,7 +226,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async callback(dto: OAuthCallbackDto, loginDetails: LoginDetails) {
|
async callback(dto: OAuthCallbackDto, loginDetails: LoginDetails) {
|
||||||
const config = await this.configCore.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
const profile = await this.getOAuthProfile(config, dto.url);
|
const profile = await this.getOAuthProfile(config, dto.url);
|
||||||
const { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim } = config.oauth;
|
const { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim } = config.oauth;
|
||||||
this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
|
this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
|
||||||
@ -288,7 +286,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async link(auth: AuthDto, dto: OAuthCallbackDto): Promise<UserAdminResponseDto> {
|
async link(auth: AuthDto, dto: OAuthCallbackDto): Promise<UserAdminResponseDto> {
|
||||||
const config = await this.configCore.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
const { sub: oauthId } = await this.getOAuthProfile(config, dto.url);
|
const { sub: oauthId } = await this.getOAuthProfile(config, dto.url);
|
||||||
const duplicate = await this.userRepository.getByOAuthId(oauthId);
|
const duplicate = await this.userRepository.getByOAuthId(oauthId);
|
||||||
if (duplicate && duplicate.id !== auth.user.id) {
|
if (duplicate && duplicate.id !== auth.user.id) {
|
||||||
@ -310,7 +308,7 @@ export class AuthService {
|
|||||||
return LOGIN_URL;
|
return LOGIN_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await this.configCore.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
if (!config.oauth.enabled) {
|
if (!config.oauth.enabled) {
|
||||||
return LOGIN_URL;
|
return LOGIN_URL;
|
||||||
}
|
}
|
||||||
|
32
server/src/services/base.service.ts
Normal file
32
server/src/services/base.service.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Inject } from '@nestjs/common';
|
||||||
|
import { SystemConfig } from 'src/config';
|
||||||
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
import { getConfig, updateConfig } from 'src/utils/config';
|
||||||
|
|
||||||
|
export class BaseService {
|
||||||
|
constructor(
|
||||||
|
@Inject(ISystemMetadataRepository) protected systemMetadataRepository: ISystemMetadataRepository,
|
||||||
|
@Inject(ILoggerRepository) protected logger: ILoggerRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getConfig(options: { withCache: boolean }) {
|
||||||
|
return getConfig(
|
||||||
|
{
|
||||||
|
metadataRepo: this.systemMetadataRepository,
|
||||||
|
logger: this.logger,
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConfig(newConfig: SystemConfig) {
|
||||||
|
return updateConfig(
|
||||||
|
{
|
||||||
|
metadataRepo: this.systemMetadataRepository,
|
||||||
|
logger: this.logger,
|
||||||
|
},
|
||||||
|
newConfig,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,24 +1,22 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { SALT_ROUNDS } from 'src/constants';
|
import { SALT_ROUNDS } from 'src/constants';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CliService {
|
export class CliService extends BaseService {
|
||||||
private configCore: SystemConfigCore;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
|
super(systemMetadataRepository, logger);
|
||||||
this.logger.setContext(CliService.name);
|
this.logger.setContext(CliService.name);
|
||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async listUsers(): Promise<UserAdminResponseDto[]> {
|
async listUsers(): Promise<UserAdminResponseDto[]> {
|
||||||
@ -42,26 +40,26 @@ export class CliService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async disablePasswordLogin(): Promise<void> {
|
async disablePasswordLogin(): Promise<void> {
|
||||||
const config = await this.configCore.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
config.passwordLogin.enabled = false;
|
config.passwordLogin.enabled = false;
|
||||||
await this.configCore.updateConfig(config);
|
await this.updateConfig(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
async enablePasswordLogin(): Promise<void> {
|
async enablePasswordLogin(): Promise<void> {
|
||||||
const config = await this.configCore.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
config.passwordLogin.enabled = true;
|
config.passwordLogin.enabled = true;
|
||||||
await this.configCore.updateConfig(config);
|
await this.updateConfig(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
async disableOAuthLogin(): Promise<void> {
|
async disableOAuthLogin(): Promise<void> {
|
||||||
const config = await this.configCore.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
config.oauth.enabled = false;
|
config.oauth.enabled = false;
|
||||||
await this.configCore.updateConfig(config);
|
await this.updateConfig(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
async enableOAuthLogin(): Promise<void> {
|
async enableOAuthLogin(): Promise<void> {
|
||||||
const config = await this.configCore.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
config.oauth.enabled = true;
|
config.oauth.enabled = true;
|
||||||
await this.configCore.updateConfig(config);
|
await this.updateConfig(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { DuplicateResponseDto, mapDuplicateResponse } from 'src/dtos/duplicate.dto';
|
import { DuplicateResponseDto, mapDuplicateResponse } from 'src/dtos/duplicate.dto';
|
||||||
@ -17,24 +16,23 @@ import {
|
|||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { AssetDuplicateResult, ISearchRepository } from 'src/interfaces/search.interface';
|
import { AssetDuplicateResult, ISearchRepository } from 'src/interfaces/search.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { getAssetFiles } from 'src/utils/asset.util';
|
import { getAssetFiles } from 'src/utils/asset.util';
|
||||||
import { isDuplicateDetectionEnabled } from 'src/utils/misc';
|
import { isDuplicateDetectionEnabled } from 'src/utils/misc';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DuplicateService {
|
export class DuplicateService extends BaseService {
|
||||||
private configCore: SystemConfigCore;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(ISearchRepository) private searchRepository: ISearchRepository,
|
@Inject(ISearchRepository) private searchRepository: ISearchRepository,
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
) {
|
) {
|
||||||
|
super(systemMetadataRepository, logger);
|
||||||
this.logger.setContext(DuplicateService.name);
|
this.logger.setContext(DuplicateService.name);
|
||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDuplicates(auth: AuthDto): Promise<DuplicateResponseDto[]> {
|
async getDuplicates(auth: AuthDto): Promise<DuplicateResponseDto[]> {
|
||||||
@ -44,7 +42,7 @@ export class DuplicateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueSearchDuplicates({ force }: IBaseJob): Promise<JobStatus> {
|
async handleQueueSearchDuplicates({ force }: IBaseJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
|
const { machineLearning } = await this.getConfig({ withCache: false });
|
||||||
if (!isDuplicateDetectionEnabled(machineLearning)) {
|
if (!isDuplicateDetectionEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
@ -65,7 +63,7 @@ export class DuplicateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleSearchDuplicates({ id }: IEntityJob): Promise<JobStatus> {
|
async handleSearchDuplicates({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig({ withCache: true });
|
const { machineLearning } = await this.getConfig({ withCache: true });
|
||||||
if (!isDuplicateDetectionEnabled(machineLearning)) {
|
if (!isDuplicateDetectionEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||||
import { snakeCase } from 'lodash';
|
import { snakeCase } from 'lodash';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
|
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
|
||||||
@ -22,6 +21,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
|||||||
import { IMetricRepository } from 'src/interfaces/metric.interface';
|
import { IMetricRepository } from 'src/interfaces/metric.interface';
|
||||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
|
||||||
const asJobItem = (dto: JobCreateDto): JobItem => {
|
const asJobItem = (dto: JobCreateDto): JobItem => {
|
||||||
switch (dto.name) {
|
switch (dto.name) {
|
||||||
@ -44,8 +44,7 @@ const asJobItem = (dto: JobCreateDto): JobItem => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JobService {
|
export class JobService extends BaseService {
|
||||||
private configCore: SystemConfigCore;
|
|
||||||
private isMicroservices = false;
|
private isMicroservices = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -55,10 +54,10 @@ export class JobService {
|
|||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(IPersonRepository) private personRepository: IPersonRepository,
|
@Inject(IPersonRepository) private personRepository: IPersonRepository,
|
||||||
@Inject(IMetricRepository) private metricRepository: IMetricRepository,
|
@Inject(IMetricRepository) private metricRepository: IMetricRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
|
super(systemMetadataRepository, logger);
|
||||||
this.logger.setContext(JobService.name);
|
this.logger.setContext(JobService.name);
|
||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'app.bootstrap' })
|
@OnEvent({ name: 'app.bootstrap' })
|
||||||
@ -198,7 +197,7 @@ export class JobService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init(jobHandlers: Record<JobName, JobHandler>) {
|
async init(jobHandlers: Record<JobName, JobHandler>) {
|
||||||
const config = await this.configCore.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
for (const queueName of Object.values(QueueName)) {
|
for (const queueName of Object.values(QueueName)) {
|
||||||
let concurrency = 1;
|
let concurrency = 1;
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ import { R_OK } from 'node:constants';
|
|||||||
import path, { basename, parse } from 'node:path';
|
import path, { basename, parse } from 'node:path';
|
||||||
import picomatch from 'picomatch';
|
import picomatch from 'picomatch';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import {
|
import {
|
||||||
CreateLibraryDto,
|
CreateLibraryDto,
|
||||||
@ -35,14 +34,14 @@ import { ILibraryRepository } from 'src/interfaces/library.interface';
|
|||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
import { handlePromiseError } from 'src/utils/misc';
|
import { handlePromiseError } from 'src/utils/misc';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
import { validateCronExpression } from 'src/validation';
|
import { validateCronExpression } from 'src/validation';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LibraryService {
|
export class LibraryService extends BaseService {
|
||||||
private configCore: SystemConfigCore;
|
|
||||||
private watchLibraries = false;
|
private watchLibraries = false;
|
||||||
private watchLock = false;
|
private watchLock = false;
|
||||||
private watchers: Record<string, () => Promise<void>> = {};
|
private watchers: Record<string, () => Promise<void>> = {};
|
||||||
@ -55,15 +54,15 @@ export class LibraryService {
|
|||||||
@Inject(ILibraryRepository) private repository: ILibraryRepository,
|
@Inject(ILibraryRepository) private repository: ILibraryRepository,
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
|
super(systemMetadataRepository, logger);
|
||||||
this.logger.setContext(LibraryService.name);
|
this.logger.setContext(LibraryService.name);
|
||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'app.bootstrap' })
|
@OnEvent({ name: 'app.bootstrap' })
|
||||||
async onBootstrap() {
|
async onBootstrap() {
|
||||||
const config = await this.configCore.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
|
|
||||||
const { watch, scan } = config.library;
|
const { watch, scan } = config.library;
|
||||||
|
|
||||||
|
@ -1,34 +1,26 @@
|
|||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
|
||||||
import { IMapRepository } from 'src/interfaces/map.interface';
|
import { IMapRepository } from 'src/interfaces/map.interface';
|
||||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
|
||||||
import { MapService } from 'src/services/map.service';
|
import { MapService } from 'src/services/map.service';
|
||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
|
||||||
import { newMapRepositoryMock } from 'test/repositories/map.repository.mock';
|
import { newMapRepositoryMock } from 'test/repositories/map.repository.mock';
|
||||||
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
|
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
|
||||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
|
||||||
import { Mocked } from 'vitest';
|
import { Mocked } from 'vitest';
|
||||||
|
|
||||||
describe(MapService.name, () => {
|
describe(MapService.name, () => {
|
||||||
let sut: MapService;
|
let sut: MapService;
|
||||||
let albumMock: Mocked<IAlbumRepository>;
|
let albumMock: Mocked<IAlbumRepository>;
|
||||||
let loggerMock: Mocked<ILoggerRepository>;
|
|
||||||
let partnerMock: Mocked<IPartnerRepository>;
|
let partnerMock: Mocked<IPartnerRepository>;
|
||||||
let mapMock: Mocked<IMapRepository>;
|
let mapMock: Mocked<IMapRepository>;
|
||||||
let systemMetadataMock: Mocked<ISystemMetadataRepository>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
albumMock = newAlbumRepositoryMock();
|
albumMock = newAlbumRepositoryMock();
|
||||||
loggerMock = newLoggerRepositoryMock();
|
|
||||||
partnerMock = newPartnerRepositoryMock();
|
partnerMock = newPartnerRepositoryMock();
|
||||||
mapMock = newMapRepositoryMock();
|
mapMock = newMapRepositoryMock();
|
||||||
systemMetadataMock = newSystemMetadataRepositoryMock();
|
|
||||||
|
|
||||||
sut = new MapService(albumMock, loggerMock, partnerMock, mapMock, systemMetadataMock);
|
sut = new MapService(albumMock, partnerMock, mapMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getMapMarkers', () => {
|
describe('getMapMarkers', () => {
|
||||||
|
@ -1,27 +1,17 @@
|
|||||||
import { Inject } from '@nestjs/common';
|
import { Inject } from '@nestjs/common';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { MapMarkerDto, MapMarkerResponseDto, MapReverseGeocodeDto } from 'src/dtos/map.dto';
|
import { MapMarkerDto, MapMarkerResponseDto, MapReverseGeocodeDto } from 'src/dtos/map.dto';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
|
||||||
import { IMapRepository } from 'src/interfaces/map.interface';
|
import { IMapRepository } from 'src/interfaces/map.interface';
|
||||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
|
||||||
import { getMyPartnerIds } from 'src/utils/asset.util';
|
import { getMyPartnerIds } from 'src/utils/asset.util';
|
||||||
|
|
||||||
export class MapService {
|
export class MapService {
|
||||||
private configCore: SystemConfigCore;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
|
||||||
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
||||||
@Inject(IMapRepository) private mapRepository: IMapRepository,
|
@Inject(IMapRepository) private mapRepository: IMapRepository,
|
||||||
@Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository,
|
) {}
|
||||||
) {
|
|
||||||
this.logger.setContext(MapService.name);
|
|
||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMapMarkers(auth: AuthDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
|
async getMapMarkers(auth: AuthDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
|
||||||
const userIds = [auth.user.id];
|
const userIds = [auth.user.id];
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
|
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import {
|
import {
|
||||||
@ -43,14 +41,14 @@ import { IMoveRepository } from 'src/interfaces/move.interface';
|
|||||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { getAssetFiles } from 'src/utils/asset.util';
|
import { getAssetFiles } from 'src/utils/asset.util';
|
||||||
import { BaseConfig, ThumbnailConfig } from 'src/utils/media';
|
import { BaseConfig, ThumbnailConfig } from 'src/utils/media';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MediaService {
|
export class MediaService extends BaseService {
|
||||||
private configCore: SystemConfigCore;
|
|
||||||
private storageCore: StorageCore;
|
private storageCore: StorageCore;
|
||||||
private maliOpenCL?: boolean;
|
private maliOpenCL?: boolean;
|
||||||
private devices?: string[];
|
private devices?: string[];
|
||||||
@ -64,10 +62,10 @@ export class MediaService {
|
|||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
||||||
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
|
super(systemMetadataRepository, logger);
|
||||||
this.logger.setContext(MediaService.name);
|
this.logger.setContext(MediaService.name);
|
||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
|
||||||
this.storageCore = StorageCore.create(
|
this.storageCore = StorageCore.create(
|
||||||
assetRepository,
|
assetRepository,
|
||||||
cryptoRepository,
|
cryptoRepository,
|
||||||
@ -161,7 +159,7 @@ export class MediaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleAssetMigration({ id }: IEntityJob): Promise<JobStatus> {
|
async handleAssetMigration({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const { image } = await this.configCore.getConfig({ withCache: true });
|
const { image } = await this.getConfig({ withCache: true });
|
||||||
const [asset] = await this.assetRepository.getByIds([id], { files: true });
|
const [asset] = await this.assetRepository.getByIds([id], { files: true });
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
@ -235,7 +233,7 @@ export class MediaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async generateImageThumbnails(asset: AssetEntity) {
|
private async generateImageThumbnails(asset: AssetEntity) {
|
||||||
const { image } = await this.configCore.getConfig({ withCache: true });
|
const { image } = await this.getConfig({ withCache: true });
|
||||||
const previewPath = StorageCore.getImagePath(asset, AssetPathType.PREVIEW, image.preview.format);
|
const previewPath = StorageCore.getImagePath(asset, AssetPathType.PREVIEW, image.preview.format);
|
||||||
const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format);
|
const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format);
|
||||||
this.storageCore.ensureFolders(previewPath);
|
this.storageCore.ensureFolders(previewPath);
|
||||||
@ -269,7 +267,7 @@ export class MediaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async generateVideoThumbnails(asset: AssetEntity) {
|
private async generateVideoThumbnails(asset: AssetEntity) {
|
||||||
const { image, ffmpeg } = await this.configCore.getConfig({ withCache: true });
|
const { image, ffmpeg } = await this.getConfig({ withCache: true });
|
||||||
const previewPath = StorageCore.getImagePath(asset, AssetPathType.PREVIEW, image.preview.format);
|
const previewPath = StorageCore.getImagePath(asset, AssetPathType.PREVIEW, image.preview.format);
|
||||||
const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format);
|
const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format);
|
||||||
this.storageCore.ensureFolders(previewPath);
|
this.storageCore.ensureFolders(previewPath);
|
||||||
@ -339,7 +337,7 @@ export class MediaService {
|
|||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { ffmpeg } = await this.configCore.getConfig({ withCache: true });
|
const { ffmpeg } = await this.getConfig({ withCache: true });
|
||||||
const target = this.getTranscodeTarget(ffmpeg, mainVideoStream, mainAudioStream);
|
const target = this.getTranscodeTarget(ffmpeg, mainVideoStream, mainAudioStream);
|
||||||
if (target === TranscodeTarget.NONE && !this.isRemuxRequired(ffmpeg, format)) {
|
if (target === TranscodeTarget.NONE && !this.isRemuxRequired(ffmpeg, format)) {
|
||||||
if (asset.encodedVideoPath) {
|
if (asset.encodedVideoPath) {
|
||||||
|
@ -7,7 +7,6 @@ import { constants } from 'node:fs/promises';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
@ -39,6 +38,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
|
|||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { ITagRepository } from 'src/interfaces/tag.interface';
|
import { ITagRepository } from 'src/interfaces/tag.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { isFaceImportEnabled } from 'src/utils/misc';
|
import { isFaceImportEnabled } from 'src/utils/misc';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
import { upsertTags } from 'src/utils/tag';
|
import { upsertTags } from 'src/utils/tag';
|
||||||
@ -97,9 +97,8 @@ const validateRange = (value: number | undefined, min: number, max: number): Non
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MetadataService {
|
export class MetadataService extends BaseService {
|
||||||
private storageCore: StorageCore;
|
private storageCore: StorageCore;
|
||||||
private configCore: SystemConfigCore;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||||
@ -117,10 +116,10 @@ export class MetadataService {
|
|||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(ITagRepository) private tagRepository: ITagRepository,
|
@Inject(ITagRepository) private tagRepository: ITagRepository,
|
||||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
|
super(systemMetadataRepository, logger);
|
||||||
this.logger.setContext(MetadataService.name);
|
this.logger.setContext(MetadataService.name);
|
||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
|
||||||
this.storageCore = StorageCore.create(
|
this.storageCore = StorageCore.create(
|
||||||
assetRepository,
|
assetRepository,
|
||||||
cryptoRepository,
|
cryptoRepository,
|
||||||
@ -137,7 +136,7 @@ export class MetadataService {
|
|||||||
if (app !== 'microservices') {
|
if (app !== 'microservices') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const config = await this.configCore.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
await this.init(config);
|
await this.init(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,7 +221,7 @@ export class MetadataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleMetadataExtraction({ id }: IEntityJob): Promise<JobStatus> {
|
async handleMetadataExtraction({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const { metadata, reverseGeocoding } = await this.configCore.getConfig({ withCache: true });
|
const { metadata, reverseGeocoding } = await this.getConfig({ withCache: true });
|
||||||
const [asset] = await this.assetRepository.getByIds([id]);
|
const [asset] = await this.assetRepository.getByIds([id]);
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||||
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
|
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
||||||
import { AlbumEntity } from 'src/entities/album.entity';
|
import { AlbumEntity } from 'src/entities/album.entity';
|
||||||
@ -20,27 +19,26 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
|||||||
import { EmailImageAttachment, EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface';
|
import { EmailImageAttachment, EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { getAssetFiles } from 'src/utils/asset.util';
|
import { getAssetFiles } from 'src/utils/asset.util';
|
||||||
import { getFilenameExtension } from 'src/utils/file';
|
import { getFilenameExtension } from 'src/utils/file';
|
||||||
import { isEqualObject } from 'src/utils/object';
|
import { isEqualObject } from 'src/utils/object';
|
||||||
import { getPreferences } from 'src/utils/preferences';
|
import { getPreferences } from 'src/utils/preferences';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationService {
|
export class NotificationService extends BaseService {
|
||||||
private configCore: SystemConfigCore;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(INotificationRepository) private notificationRepository: INotificationRepository,
|
@Inject(INotificationRepository) private notificationRepository: INotificationRepository,
|
||||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||||
) {
|
) {
|
||||||
|
super(systemMetadataRepository, logger);
|
||||||
this.logger.setContext(NotificationService.name);
|
this.logger.setContext(NotificationService.name);
|
||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'config.update' })
|
@OnEvent({ name: 'config.update' })
|
||||||
@ -149,7 +147,7 @@ export class NotificationService {
|
|||||||
throw new BadRequestException('Failed to verify SMTP configuration', { cause: error });
|
throw new BadRequestException('Failed to verify SMTP configuration', { cause: error });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { server } = await this.configCore.getConfig({ withCache: false });
|
const { server } = await this.getConfig({ withCache: false });
|
||||||
const { html, text } = await this.notificationRepository.renderEmail({
|
const { html, text } = await this.notificationRepository.renderEmail({
|
||||||
template: EmailTemplate.TEST_EMAIL,
|
template: EmailTemplate.TEST_EMAIL,
|
||||||
data: {
|
data: {
|
||||||
@ -177,7 +175,7 @@ export class NotificationService {
|
|||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { server } = await this.configCore.getConfig({ withCache: true });
|
const { server } = await this.getConfig({ withCache: true });
|
||||||
const { html, text } = await this.notificationRepository.renderEmail({
|
const { html, text } = await this.notificationRepository.renderEmail({
|
||||||
template: EmailTemplate.WELCOME,
|
template: EmailTemplate.WELCOME,
|
||||||
data: {
|
data: {
|
||||||
@ -220,7 +218,7 @@ export class NotificationService {
|
|||||||
|
|
||||||
const attachment = await this.getAlbumThumbnailAttachment(album);
|
const attachment = await this.getAlbumThumbnailAttachment(album);
|
||||||
|
|
||||||
const { server } = await this.configCore.getConfig({ withCache: false });
|
const { server } = await this.getConfig({ withCache: false });
|
||||||
const { html, text } = await this.notificationRepository.renderEmail({
|
const { html, text } = await this.notificationRepository.renderEmail({
|
||||||
template: EmailTemplate.ALBUM_INVITE,
|
template: EmailTemplate.ALBUM_INVITE,
|
||||||
data: {
|
data: {
|
||||||
@ -262,7 +260,7 @@ export class NotificationService {
|
|||||||
const recipients = [...album.albumUsers.map((user) => user.user), owner].filter((user) => user.id !== senderId);
|
const recipients = [...album.albumUsers.map((user) => user.user), owner].filter((user) => user.id !== senderId);
|
||||||
const attachment = await this.getAlbumThumbnailAttachment(album);
|
const attachment = await this.getAlbumThumbnailAttachment(album);
|
||||||
|
|
||||||
const { server } = await this.configCore.getConfig({ withCache: false });
|
const { server } = await this.getConfig({ withCache: false });
|
||||||
|
|
||||||
for (const recipient of recipients) {
|
for (const recipient of recipients) {
|
||||||
const user = await this.userRepository.get(recipient.id, { withDeleted: false });
|
const user = await this.userRepository.get(recipient.id, { withDeleted: false });
|
||||||
@ -303,7 +301,7 @@ export class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleSendEmail(data: IEmailJob): Promise<JobStatus> {
|
async handleSendEmail(data: IEmailJob): Promise<JobStatus> {
|
||||||
const { notifications } = await this.configCore.getConfig({ withCache: false });
|
const { notifications } = await this.getConfig({ withCache: false });
|
||||||
if (!notifications.smtp.enabled) {
|
if (!notifications.smtp.enabled) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { FACE_THUMBNAIL_SIZE } from 'src/constants';
|
import { FACE_THUMBNAIL_SIZE } from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
@ -55,6 +54,7 @@ import { IPersonRepository, UpdateFacesData } from 'src/interfaces/person.interf
|
|||||||
import { ISearchRepository } from 'src/interfaces/search.interface';
|
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { checkAccess, requireAccess } from 'src/utils/access';
|
import { checkAccess, requireAccess } from 'src/utils/access';
|
||||||
import { getAssetFiles } from 'src/utils/asset.util';
|
import { getAssetFiles } from 'src/utils/asset.util';
|
||||||
import { ImmichFileResponse } from 'src/utils/file';
|
import { ImmichFileResponse } from 'src/utils/file';
|
||||||
@ -64,8 +64,7 @@ import { usePagination } from 'src/utils/pagination';
|
|||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PersonService {
|
export class PersonService extends BaseService {
|
||||||
private configCore: SystemConfigCore;
|
|
||||||
private storageCore: StorageCore;
|
private storageCore: StorageCore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -75,15 +74,15 @@ export class PersonService {
|
|||||||
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
||||||
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
||||||
@Inject(IPersonRepository) private repository: IPersonRepository,
|
@Inject(IPersonRepository) private repository: IPersonRepository,
|
||||||
@Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
@Inject(ISearchRepository) private smartInfoRepository: ISearchRepository,
|
@Inject(ISearchRepository) private smartInfoRepository: ISearchRepository,
|
||||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
|
super(systemMetadataRepository, logger);
|
||||||
this.logger.setContext(PersonService.name);
|
this.logger.setContext(PersonService.name);
|
||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
|
||||||
this.storageCore = StorageCore.create(
|
this.storageCore = StorageCore.create(
|
||||||
assetRepository,
|
assetRepository,
|
||||||
cryptoRepository,
|
cryptoRepository,
|
||||||
@ -102,7 +101,7 @@ export class PersonService {
|
|||||||
skip: (page - 1) * size,
|
skip: (page - 1) * size,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
|
const { machineLearning } = await this.getConfig({ withCache: false });
|
||||||
const { items, hasNextPage } = await this.repository.getAllForUser(pagination, auth.user.id, {
|
const { items, hasNextPage } = await this.repository.getAllForUser(pagination, auth.user.id, {
|
||||||
minimumFaceCount: machineLearning.facialRecognition.minFaces,
|
minimumFaceCount: machineLearning.facialRecognition.minFaces,
|
||||||
withHidden,
|
withHidden,
|
||||||
@ -283,7 +282,7 @@ export class PersonService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueDetectFaces({ force }: IBaseJob): Promise<JobStatus> {
|
async handleQueueDetectFaces({ force }: IBaseJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
|
const { machineLearning } = await this.getConfig({ withCache: false });
|
||||||
if (!isFacialRecognitionEnabled(machineLearning)) {
|
if (!isFacialRecognitionEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
@ -314,7 +313,7 @@ export class PersonService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleDetectFaces({ id }: IEntityJob): Promise<JobStatus> {
|
async handleDetectFaces({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig({ withCache: true });
|
const { machineLearning } = await this.getConfig({ withCache: true });
|
||||||
if (!isFacialRecognitionEnabled(machineLearning)) {
|
if (!isFacialRecognitionEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
@ -375,7 +374,7 @@ export class PersonService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueRecognizeFaces({ force, nightly }: INightlyJob): Promise<JobStatus> {
|
async handleQueueRecognizeFaces({ force, nightly }: INightlyJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
|
const { machineLearning } = await this.getConfig({ withCache: false });
|
||||||
if (!isFacialRecognitionEnabled(machineLearning)) {
|
if (!isFacialRecognitionEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
@ -425,7 +424,7 @@ export class PersonService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleRecognizeFaces({ id, deferred }: IDeferrableJob): Promise<JobStatus> {
|
async handleRecognizeFaces({ id, deferred }: IDeferrableJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig({ withCache: true });
|
const { machineLearning } = await this.getConfig({ withCache: true });
|
||||||
if (!isFacialRecognitionEnabled(machineLearning)) {
|
if (!isFacialRecognitionEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
@ -519,7 +518,7 @@ export class PersonService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleGeneratePersonThumbnail(data: IEntityJob): Promise<JobStatus> {
|
async handleGeneratePersonThumbnail(data: IEntityJob): Promise<JobStatus> {
|
||||||
const { machineLearning, metadata, image } = await this.configCore.getConfig({ withCache: true });
|
const { machineLearning, metadata, image } = await this.getConfig({ withCache: true });
|
||||||
if (!isFacialRecognitionEnabled(machineLearning) && !isFaceImportEnabled(metadata)) {
|
if (!isFacialRecognitionEnabled(machineLearning) && !isFaceImportEnabled(metadata)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { AssetMapOptions, AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
import { AssetMapOptions, AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { PersonResponseDto } from 'src/dtos/person.dto';
|
import { PersonResponseDto } from 'src/dtos/person.dto';
|
||||||
@ -24,13 +23,12 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
|||||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||||
import { ISearchRepository, SearchExploreItem } from 'src/interfaces/search.interface';
|
import { ISearchRepository, SearchExploreItem } from 'src/interfaces/search.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { getMyPartnerIds } from 'src/utils/asset.util';
|
import { getMyPartnerIds } from 'src/utils/asset.util';
|
||||||
import { isSmartSearchEnabled } from 'src/utils/misc';
|
import { isSmartSearchEnabled } from 'src/utils/misc';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SearchService {
|
export class SearchService extends BaseService {
|
||||||
private configCore: SystemConfigCore;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
|
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
|
||||||
@ -38,10 +36,10 @@ export class SearchService {
|
|||||||
@Inject(ISearchRepository) private searchRepository: ISearchRepository,
|
@Inject(ISearchRepository) private searchRepository: ISearchRepository,
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
|
super(systemMetadataRepository, logger);
|
||||||
this.logger.setContext(SearchService.name);
|
this.logger.setContext(SearchService.name);
|
||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchPerson(auth: AuthDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
|
async searchPerson(auth: AuthDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
|
||||||
@ -101,7 +99,7 @@ export class SearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise<SearchResponseDto> {
|
async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise<SearchResponseDto> {
|
||||||
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
|
const { machineLearning } = await this.getConfig({ withCache: false });
|
||||||
if (!isSmartSearchEnabled(machineLearning)) {
|
if (!isSmartSearchEnabled(machineLearning)) {
|
||||||
throw new BadRequestException('Smart search is not enabled');
|
throw new BadRequestException('Smart search is not enabled');
|
||||||
}
|
}
|
||||||
|
@ -176,9 +176,9 @@ describe(ServerService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getConfig', () => {
|
describe('getSystemConfig', () => {
|
||||||
it('should respond the server configuration', async () => {
|
it('should respond the server configuration', async () => {
|
||||||
await expect(sut.getConfig()).resolves.toEqual({
|
await expect(sut.getSystemConfig()).resolves.toEqual({
|
||||||
loginPageMessage: '',
|
loginPageMessage: '',
|
||||||
oauthButtonText: 'Login with OAuth',
|
oauthButtonText: 'Login with OAuth',
|
||||||
trashDays: 30,
|
trashDays: 30,
|
||||||
|
@ -2,7 +2,6 @@ import { BadRequestException, Inject, Injectable, NotFoundException } from '@nes
|
|||||||
import { getBuildMetadata, getServerLicensePublicKey } from 'src/config';
|
import { getBuildMetadata, getServerLicensePublicKey } from 'src/config';
|
||||||
import { serverVersion } from 'src/constants';
|
import { serverVersion } from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||||
import {
|
import {
|
||||||
@ -22,24 +21,24 @@ import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
|||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.interface';
|
import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.interface';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { asHumanReadable } from 'src/utils/bytes';
|
import { asHumanReadable } from 'src/utils/bytes';
|
||||||
|
import { isUsingConfigFile } from 'src/utils/config';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
import { isDuplicateDetectionEnabled, isFacialRecognitionEnabled, isSmartSearchEnabled } from 'src/utils/misc';
|
import { isDuplicateDetectionEnabled, isFacialRecognitionEnabled, isSmartSearchEnabled } from 'src/utils/misc';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ServerService {
|
export class ServerService extends BaseService {
|
||||||
private configCore: SystemConfigCore;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
@Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(IServerInfoRepository) private serverInfoRepository: IServerInfoRepository,
|
@Inject(IServerInfoRepository) private serverInfoRepository: IServerInfoRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
) {
|
) {
|
||||||
|
super(systemMetadataRepository, logger);
|
||||||
this.logger.setContext(ServerService.name);
|
this.logger.setContext(ServerService.name);
|
||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'app.bootstrap' })
|
@OnEvent({ name: 'app.bootstrap' })
|
||||||
@ -91,7 +90,7 @@ export class ServerService {
|
|||||||
|
|
||||||
async getFeatures(): Promise<ServerFeaturesDto> {
|
async getFeatures(): Promise<ServerFeaturesDto> {
|
||||||
const { reverseGeocoding, metadata, map, machineLearning, trash, oauth, passwordLogin, notifications } =
|
const { reverseGeocoding, metadata, map, machineLearning, trash, oauth, passwordLogin, notifications } =
|
||||||
await this.configCore.getConfig({ withCache: false });
|
await this.getConfig({ withCache: false });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
smartSearch: isSmartSearchEnabled(machineLearning),
|
smartSearch: isSmartSearchEnabled(machineLearning),
|
||||||
@ -106,18 +105,18 @@ export class ServerService {
|
|||||||
oauth: oauth.enabled,
|
oauth: oauth.enabled,
|
||||||
oauthAutoLaunch: oauth.autoLaunch,
|
oauthAutoLaunch: oauth.autoLaunch,
|
||||||
passwordLogin: passwordLogin.enabled,
|
passwordLogin: passwordLogin.enabled,
|
||||||
configFile: this.configCore.isUsingConfigFile(),
|
configFile: isUsingConfigFile(),
|
||||||
email: notifications.smtp.enabled,
|
email: notifications.smtp.enabled,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTheme() {
|
async getTheme() {
|
||||||
const { theme } = await this.configCore.getConfig({ withCache: false });
|
const { theme } = await this.getConfig({ withCache: false });
|
||||||
return theme;
|
return theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getConfig(): Promise<ServerConfigDto> {
|
async getSystemConfig(): Promise<ServerConfigDto> {
|
||||||
const config = await this.configCore.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
const isInitialized = await this.userRepository.hasAdmin();
|
const isInitialized = await this.userRepository.hasAdmin();
|
||||||
const onboarding = await this.systemMetadataRepository.get(SystemMetadataKey.ADMIN_ONBOARDING);
|
const onboarding = await this.systemMetadataRepository.get(SystemMetadataKey.ADMIN_ONBOARDING);
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
|
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
|
import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AssetIdsDto } from 'src/dtos/asset.dto';
|
import { AssetIdsDto } from 'src/dtos/asset.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
@ -20,22 +19,21 @@ import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
|||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { checkAccess, requireAccess } from 'src/utils/access';
|
import { checkAccess, requireAccess } from 'src/utils/access';
|
||||||
import { OpenGraphTags } from 'src/utils/misc';
|
import { OpenGraphTags } from 'src/utils/misc';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SharedLinkService {
|
export class SharedLinkService extends BaseService {
|
||||||
private configCore: SystemConfigCore;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
@Inject(ISharedLinkRepository) private repository: ISharedLinkRepository,
|
@Inject(ISharedLinkRepository) private repository: ISharedLinkRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
) {
|
) {
|
||||||
|
super(systemMetadataRepository, logger);
|
||||||
this.logger.setContext(SharedLinkService.name);
|
this.logger.setContext(SharedLinkService.name);
|
||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAll(auth: AuthDto): Promise<SharedLinkResponseDto[]> {
|
getAll(auth: AuthDto): Promise<SharedLinkResponseDto[]> {
|
||||||
@ -195,7 +193,7 @@ export class SharedLinkService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await this.configCore.getConfig({ withCache: true });
|
const config = await this.getConfig({ withCache: true });
|
||||||
const sharedLink = await this.findOrFail(auth.sharedLink.userId, auth.sharedLink.id);
|
const sharedLink = await this.findOrFail(auth.sharedLink.userId, auth.sharedLink.id);
|
||||||
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
|
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
|
||||||
const assetCount = sharedLink.assets.length > 0 ? sharedLink.assets.length : sharedLink.album?.assets.length || 0;
|
const assetCount = sharedLink.assets.length > 0 ? sharedLink.assets.length : sharedLink.album?.assets.length || 0;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
@ -18,14 +17,13 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
|||||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||||
import { ISearchRepository } from 'src/interfaces/search.interface';
|
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { getAssetFiles } from 'src/utils/asset.util';
|
import { getAssetFiles } from 'src/utils/asset.util';
|
||||||
import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc';
|
import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SmartInfoService {
|
export class SmartInfoService extends BaseService {
|
||||||
private configCore: SystemConfigCore;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||||
@ -33,10 +31,10 @@ export class SmartInfoService {
|
|||||||
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
|
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
|
||||||
@Inject(ISearchRepository) private repository: ISearchRepository,
|
@Inject(ISearchRepository) private repository: ISearchRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
|
super(systemMetadataRepository, logger);
|
||||||
this.logger.setContext(SmartInfoService.name);
|
this.logger.setContext(SmartInfoService.name);
|
||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'app.bootstrap' })
|
@OnEvent({ name: 'app.bootstrap' })
|
||||||
@ -45,7 +43,7 @@ export class SmartInfoService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await this.configCore.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
await this.init(config);
|
await this.init(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +104,7 @@ export class SmartInfoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueEncodeClip({ force }: IBaseJob): Promise<JobStatus> {
|
async handleQueueEncodeClip({ force }: IBaseJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
|
const { machineLearning } = await this.getConfig({ withCache: false });
|
||||||
if (!isSmartSearchEnabled(machineLearning)) {
|
if (!isSmartSearchEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
@ -131,7 +129,7 @@ export class SmartInfoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleEncodeClip({ id }: IEntityJob): Promise<JobStatus> {
|
async handleEncodeClip({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig({ withCache: true });
|
const { machineLearning } = await this.getConfig({ withCache: true });
|
||||||
if (!isSmartSearchEnabled(machineLearning)) {
|
if (!isSmartSearchEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,6 @@ import {
|
|||||||
supportedYearTokens,
|
supportedYearTokens,
|
||||||
} from 'src/constants';
|
} from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
|
import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
|
||||||
@ -29,6 +28,7 @@ import { IPersonRepository } from 'src/interfaces/person.interface';
|
|||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { getLivePhotoMotionFilename } from 'src/utils/file';
|
import { getLivePhotoMotionFilename } from 'src/utils/file';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
|
|
||||||
@ -45,8 +45,7 @@ interface RenderMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StorageTemplateService {
|
export class StorageTemplateService extends BaseService {
|
||||||
private configCore: SystemConfigCore;
|
|
||||||
private storageCore: StorageCore;
|
private storageCore: StorageCore;
|
||||||
private _template: {
|
private _template: {
|
||||||
compiled: HandlebarsTemplateDelegate<any>;
|
compiled: HandlebarsTemplateDelegate<any>;
|
||||||
@ -71,10 +70,10 @@ export class StorageTemplateService {
|
|||||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
||||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
|
super(systemMetadataRepository, logger);
|
||||||
this.logger.setContext(StorageTemplateService.name);
|
this.logger.setContext(StorageTemplateService.name);
|
||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
|
||||||
this.storageCore = StorageCore.create(
|
this.storageCore = StorageCore.create(
|
||||||
assetRepository,
|
assetRepository,
|
||||||
cryptoRepository,
|
cryptoRepository,
|
||||||
@ -117,7 +116,7 @@ export class StorageTemplateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleMigrationSingle({ id }: IEntityJob): Promise<JobStatus> {
|
async handleMigrationSingle({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const config = await this.configCore.getConfig({ withCache: true });
|
const config = await this.getConfig({ withCache: true });
|
||||||
const storageTemplateEnabled = config.storageTemplate.enabled;
|
const storageTemplateEnabled = config.storageTemplate.enabled;
|
||||||
if (!storageTemplateEnabled) {
|
if (!storageTemplateEnabled) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
@ -147,7 +146,7 @@ export class StorageTemplateService {
|
|||||||
|
|
||||||
async handleMigration(): Promise<JobStatus> {
|
async handleMigration(): Promise<JobStatus> {
|
||||||
this.logger.log('Starting storage template migration');
|
this.logger.log('Starting storage template migration');
|
||||||
const { storageTemplate } = await this.configCore.getConfig({ withCache: true });
|
const { storageTemplate } = await this.getConfig({ withCache: true });
|
||||||
const { enabled } = storageTemplate;
|
const { enabled } = storageTemplate;
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
this.logger.log('Storage template migration disabled, skipping');
|
this.logger.log('Storage template migration disabled, skipping');
|
||||||
|
@ -216,7 +216,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
it('should return the default config', async () => {
|
it('should return the default config', async () => {
|
||||||
systemMock.get.mockResolvedValue({});
|
systemMock.get.mockResolvedValue({});
|
||||||
|
|
||||||
await expect(sut.getConfig()).resolves.toEqual(defaults);
|
await expect(sut.getSystemConfig()).resolves.toEqual(defaults);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should merge the overrides', async () => {
|
it('should merge the overrides', async () => {
|
||||||
@ -227,7 +227,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
user: { deleteDelay: 15 },
|
user: { deleteDelay: 15 },
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
|
await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load the config from a json file', async () => {
|
it('should load the config from a json file', async () => {
|
||||||
@ -235,7 +235,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
|
|
||||||
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||||
|
|
||||||
await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
|
await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig);
|
||||||
|
|
||||||
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
|
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
|
||||||
});
|
});
|
||||||
@ -245,7 +245,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
|
|
||||||
systemMock.readFile.mockResolvedValue(`{ "ffmpeg2": true, "ffmpeg2": true }`);
|
systemMock.readFile.mockResolvedValue(`{ "ffmpeg2": true, "ffmpeg2": true }`);
|
||||||
|
|
||||||
await expect(sut.getConfig()).rejects.toBeInstanceOf(Error);
|
await expect(sut.getSystemConfig()).rejects.toBeInstanceOf(Error);
|
||||||
|
|
||||||
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
|
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
|
||||||
expect(loggerMock.error).toHaveBeenCalledTimes(2);
|
expect(loggerMock.error).toHaveBeenCalledTimes(2);
|
||||||
@ -269,7 +269,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
`;
|
`;
|
||||||
systemMock.readFile.mockResolvedValue(partialConfig);
|
systemMock.readFile.mockResolvedValue(partialConfig);
|
||||||
|
|
||||||
await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
|
await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig);
|
||||||
|
|
||||||
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.yaml');
|
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.yaml');
|
||||||
});
|
});
|
||||||
@ -278,7 +278,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
||||||
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
|
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
|
||||||
|
|
||||||
await expect(sut.getConfig()).resolves.toEqual(defaults);
|
await expect(sut.getSystemConfig()).resolves.toEqual(defaults);
|
||||||
|
|
||||||
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
|
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
|
||||||
});
|
});
|
||||||
@ -288,7 +288,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
const partialConfig = { machineLearning: { url: 'immich_machine_learning' } };
|
const partialConfig = { machineLearning: { url: 'immich_machine_learning' } };
|
||||||
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||||
|
|
||||||
const config = await sut.getConfig();
|
const config = await sut.getSystemConfig();
|
||||||
expect(config.machineLearning.url).toEqual('immich_machine_learning');
|
expect(config.machineLearning.url).toEqual('immich_machine_learning');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -304,7 +304,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
const partialConfig = { server: { externalDomain } };
|
const partialConfig = { server: { externalDomain } };
|
||||||
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||||
|
|
||||||
const config = await sut.getConfig();
|
const config = await sut.getSystemConfig();
|
||||||
expect(config.server.externalDomain).toEqual(result ?? 'https://demo.immich.app');
|
expect(config.server.externalDomain).toEqual(result ?? 'https://demo.immich.app');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -316,7 +316,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
`;
|
`;
|
||||||
systemMock.readFile.mockResolvedValue(partialConfig);
|
systemMock.readFile.mockResolvedValue(partialConfig);
|
||||||
|
|
||||||
await sut.getConfig();
|
await sut.getSystemConfig();
|
||||||
expect(loggerMock.warn).toHaveBeenCalled();
|
expect(loggerMock.warn).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -335,10 +335,10 @@ describe(SystemConfigService.name, () => {
|
|||||||
systemMock.readFile.mockResolvedValue(JSON.stringify(test.config));
|
systemMock.readFile.mockResolvedValue(JSON.stringify(test.config));
|
||||||
|
|
||||||
if (test.warn) {
|
if (test.warn) {
|
||||||
await sut.getConfig();
|
await sut.getSystemConfig();
|
||||||
expect(loggerMock.warn).toHaveBeenCalled();
|
expect(loggerMock.warn).toHaveBeenCalled();
|
||||||
} else {
|
} else {
|
||||||
await expect(sut.getConfig()).rejects.toBeInstanceOf(Error);
|
await expect(sut.getSystemConfig()).rejects.toBeInstanceOf(Error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -382,7 +382,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
describe('updateConfig', () => {
|
describe('updateConfig', () => {
|
||||||
it('should update the config and emit an event', async () => {
|
it('should update the config and emit an event', async () => {
|
||||||
systemMock.get.mockResolvedValue(partialConfig);
|
systemMock.get.mockResolvedValue(partialConfig);
|
||||||
await expect(sut.updateConfig(updatedConfig)).resolves.toEqual(updatedConfig);
|
await expect(sut.updateSystemConfig(updatedConfig)).resolves.toEqual(updatedConfig);
|
||||||
expect(eventMock.emit).toHaveBeenCalledWith(
|
expect(eventMock.emit).toHaveBeenCalledWith(
|
||||||
'config.update',
|
'config.update',
|
||||||
expect.objectContaining({ oldConfig: expect.any(Object), newConfig: updatedConfig }),
|
expect.objectContaining({ oldConfig: expect.any(Object), newConfig: updatedConfig }),
|
||||||
@ -392,7 +392,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
it('should throw an error if a config file is in use', async () => {
|
it('should throw an error if a config file is in use', async () => {
|
||||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
||||||
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
|
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
|
||||||
await expect(sut.updateConfig(defaults)).rejects.toBeInstanceOf(BadRequestException);
|
await expect(sut.updateSystemConfig(defaults)).rejects.toBeInstanceOf(BadRequestException);
|
||||||
expect(systemMock.set).not.toHaveBeenCalled();
|
expect(systemMock.set).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -12,36 +12,35 @@ import {
|
|||||||
supportedWeekTokens,
|
supportedWeekTokens,
|
||||||
supportedYearTokens,
|
supportedYearTokens,
|
||||||
} from 'src/constants';
|
} from 'src/constants';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto';
|
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto';
|
||||||
import { LogLevel } from 'src/enum';
|
import { LogLevel } from 'src/enum';
|
||||||
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
|
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
import { clearConfigCache, isUsingConfigFile } from 'src/utils/config';
|
||||||
import { toPlainObject } from 'src/utils/object';
|
import { toPlainObject } from 'src/utils/object';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SystemConfigService {
|
export class SystemConfigService extends BaseService {
|
||||||
private core: SystemConfigCore;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(ISystemMetadataRepository) repository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
|
super(systemMetadataRepository, logger);
|
||||||
this.logger.setContext(SystemConfigService.name);
|
this.logger.setContext(SystemConfigService.name);
|
||||||
this.core = SystemConfigCore.create(repository, this.logger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'app.bootstrap', priority: -100 })
|
@OnEvent({ name: 'app.bootstrap', priority: -100 })
|
||||||
async onBootstrap() {
|
async onBootstrap() {
|
||||||
const config = await this.core.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
await this.eventRepository.emit('config.update', { newConfig: config });
|
await this.eventRepository.emit('config.update', { newConfig: config });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getConfig(): Promise<SystemConfigDto> {
|
async getSystemConfig(): Promise<SystemConfigDto> {
|
||||||
const config = await this.core.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
return mapConfig(config);
|
return mapConfig(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +56,7 @@ export class SystemConfigService {
|
|||||||
this.logger.setLogLevel(level);
|
this.logger.setLogLevel(level);
|
||||||
this.logger.log(`LogLevel=${level} ${envLevel ? '(set via IMMICH_LOG_LEVEL)' : '(set via system config)'}`);
|
this.logger.log(`LogLevel=${level} ${envLevel ? '(set via IMMICH_LOG_LEVEL)' : '(set via system config)'}`);
|
||||||
// TODO only do this if the event is a socket.io event
|
// TODO only do this if the event is a socket.io event
|
||||||
this.core.invalidateCache();
|
clearConfigCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'config.validate' })
|
@OnEvent({ name: 'config.validate' })
|
||||||
@ -67,12 +66,12 @@ export class SystemConfigService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateConfig(dto: SystemConfigDto): Promise<SystemConfigDto> {
|
async updateSystemConfig(dto: SystemConfigDto): Promise<SystemConfigDto> {
|
||||||
if (this.core.isUsingConfigFile()) {
|
if (isUsingConfigFile()) {
|
||||||
throw new BadRequestException('Cannot update configuration while IMMICH_CONFIG_FILE is in use');
|
throw new BadRequestException('Cannot update configuration while IMMICH_CONFIG_FILE is in use');
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldConfig = await this.core.getConfig({ withCache: false });
|
const oldConfig = await this.getConfig({ withCache: false });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.eventRepository.emit('config.validate', { newConfig: toPlainObject(dto), oldConfig });
|
await this.eventRepository.emit('config.validate', { newConfig: toPlainObject(dto), oldConfig });
|
||||||
@ -81,7 +80,7 @@ export class SystemConfigService {
|
|||||||
throw new BadRequestException(error instanceof Error ? error.message : error);
|
throw new BadRequestException(error instanceof Error ? error.message : error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newConfig = await this.core.updateConfig(dto);
|
const newConfig = await this.updateConfig(dto);
|
||||||
|
|
||||||
await this.eventRepository.emit('config.update', { newConfig, oldConfig });
|
await this.eventRepository.emit('config.update', { newConfig, oldConfig });
|
||||||
|
|
||||||
@ -104,7 +103,7 @@ export class SystemConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getCustomCss(): Promise<string> {
|
async getCustomCss(): Promise<string> {
|
||||||
const { theme } = await this.core.getConfig({ withCache: false });
|
const { theme } = await this.getConfig({ withCache: false });
|
||||||
return theme.customCss;
|
return theme.customCss;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ import { DateTime } from 'luxon';
|
|||||||
import { getClientLicensePublicKey, getServerLicensePublicKey } from 'src/config';
|
import { getClientLicensePublicKey, getServerLicensePublicKey } from 'src/config';
|
||||||
import { SALT_ROUNDS } from 'src/constants';
|
import { SALT_ROUNDS } from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
|
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
|
||||||
@ -19,13 +18,12 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
|||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
|
import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { ImmichFileResponse } from 'src/utils/file';
|
import { ImmichFileResponse } from 'src/utils/file';
|
||||||
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
|
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService extends BaseService {
|
||||||
private configCore: SystemConfigCore;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
@ -33,10 +31,10 @@ export class UserService {
|
|||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
|
super(systemMetadataRepository, logger);
|
||||||
this.logger.setContext(UserService.name);
|
this.logger.setContext(UserService.name);
|
||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(): Promise<UserResponseDto[]> {
|
async search(): Promise<UserResponseDto[]> {
|
||||||
@ -189,7 +187,7 @@ export class UserService {
|
|||||||
|
|
||||||
async handleUserDeleteCheck(): Promise<JobStatus> {
|
async handleUserDeleteCheck(): Promise<JobStatus> {
|
||||||
const users = await this.userRepository.getDeletedUsers();
|
const users = await this.userRepository.getDeletedUsers();
|
||||||
const config = await this.configCore.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
await this.jobRepository.queueAll(
|
await this.jobRepository.queueAll(
|
||||||
users.flatMap((user) =>
|
users.flatMap((user) =>
|
||||||
this.isReadyForDeletion(user, config.user.deleteDelay)
|
this.isReadyForDeletion(user, config.user.deleteDelay)
|
||||||
@ -201,7 +199,7 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleUserDelete({ id, force }: IEntityJob): Promise<JobStatus> {
|
async handleUserDelete({ id, force }: IEntityJob): Promise<JobStatus> {
|
||||||
const config = await this.configCore.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
const user = await this.userRepository.get(id, { withDeleted: true });
|
const user = await this.userRepository.get(id, { withDeleted: true });
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
|
@ -2,7 +2,6 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import semver, { SemVer } from 'semver';
|
import semver, { SemVer } from 'semver';
|
||||||
import { isDev, serverVersion } from 'src/constants';
|
import { isDev, serverVersion } from 'src/constants';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||||
import { VersionCheckMetadata } from 'src/entities/system-metadata.entity';
|
import { VersionCheckMetadata } from 'src/entities/system-metadata.entity';
|
||||||
@ -12,6 +11,7 @@ import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface
|
|||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
|
||||||
const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => {
|
const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => {
|
||||||
return {
|
return {
|
||||||
@ -23,18 +23,16 @@ const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): Re
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VersionService {
|
export class VersionService extends BaseService {
|
||||||
private configCore: SystemConfigCore;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
@Inject(IServerInfoRepository) private repository: IServerInfoRepository,
|
@Inject(IServerInfoRepository) private repository: IServerInfoRepository,
|
||||||
@Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
|
super(systemMetadataRepository, logger);
|
||||||
this.logger.setContext(VersionService.name);
|
this.logger.setContext(VersionService.name);
|
||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'app.bootstrap' })
|
@OnEvent({ name: 'app.bootstrap' })
|
||||||
@ -58,7 +56,7 @@ export class VersionService {
|
|||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { newVersionCheck } = await this.configCore.getConfig({ withCache: true });
|
const { newVersionCheck } = await this.getConfig({ withCache: true });
|
||||||
if (!newVersionCheck.enabled) {
|
if (!newVersionCheck.enabled) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
129
server/src/utils/config.ts
Normal file
129
server/src/utils/config.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
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>;
|
||||||
|
|
||||||
|
type RepoDeps = {
|
||||||
|
metadataRepo: ISystemMetadataRepository;
|
||||||
|
logger: ILoggerRepository;
|
||||||
|
};
|
||||||
|
|
||||||
|
const asyncLock = new AsyncLock();
|
||||||
|
let config: SystemConfig | null = null;
|
||||||
|
let lastUpdated: number | null = null;
|
||||||
|
|
||||||
|
export const clearConfigCache = () => {
|
||||||
|
config = null;
|
||||||
|
lastUpdated = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isUsingConfigFile = () => {
|
||||||
|
return !!process.env.IMMICH_CONFIG_FILE;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getConfig = async (repos: RepoDeps, { withCache }: { withCache: boolean }): Promise<SystemConfig> => {
|
||||||
|
if (!withCache || !config) {
|
||||||
|
const timestamp = lastUpdated;
|
||||||
|
await asyncLock.acquire(DatabaseLock[DatabaseLock.GetSystemConfig], async () => {
|
||||||
|
if (timestamp === lastUpdated) {
|
||||||
|
config = await buildConfig(repos);
|
||||||
|
lastUpdated = Date.now();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return config!;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateConfig = async (repos: RepoDeps, newConfig: SystemConfig): Promise<SystemConfig> => {
|
||||||
|
const { metadataRepo } = repos;
|
||||||
|
// 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 metadataRepo.set(SystemMetadataKey.SYSTEM_CONFIG, partialConfig);
|
||||||
|
|
||||||
|
return getConfig(repos, { withCache: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadFromFile = async ({ metadataRepo, logger }: RepoDeps, filepath: string) => {
|
||||||
|
try {
|
||||||
|
const file = await metadataRepo.readFile(filepath);
|
||||||
|
return loadYaml(file.toString()) as unknown;
|
||||||
|
} catch (error: Error | any) {
|
||||||
|
logger.error(`Unable to load configuration file: ${filepath}`);
|
||||||
|
logger.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildConfig = async (repos: RepoDeps) => {
|
||||||
|
const { metadataRepo, logger } = repos;
|
||||||
|
|
||||||
|
// load partial
|
||||||
|
const partial = isUsingConfigFile()
|
||||||
|
? await loadFromFile(repos, process.env.IMMICH_CONFIG_FILE as string)
|
||||||
|
: await metadataRepo.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)) {
|
||||||
|
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 (isUsingConfigFile()) {
|
||||||
|
throw new Error(`Invalid value(s) in file: ${errors}`);
|
||||||
|
} else {
|
||||||
|
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;
|
||||||
|
};
|
@ -1,12 +1,9 @@
|
|||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
import { clearConfigCache } from 'src/utils/config';
|
||||||
import { Mocked, vitest } from 'vitest';
|
import { Mocked, vitest } from 'vitest';
|
||||||
|
|
||||||
export const newSystemMetadataRepositoryMock = (reset = true): Mocked<ISystemMetadataRepository> => {
|
export const newSystemMetadataRepositoryMock = (): Mocked<ISystemMetadataRepository> => {
|
||||||
if (reset) {
|
clearConfigCache();
|
||||||
SystemConfigCore.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get: vitest.fn() as any,
|
get: vitest.fn() as any,
|
||||||
set: vitest.fn(),
|
set: vitest.fn(),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user