From 47821cda35e0df2e5de408b55f590f6f96121c98 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:16:04 -0400 Subject: [PATCH 01/22] chore(deps): bump docker/build-push-action from 6.7.0 to 6.9.0 (#13052) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.7.0 to 6.9.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v6.7.0...v6.9.0) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cli.yml | 2 +- .github/workflows/docker.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 5292075cce902..a86408eea891c 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -88,7 +88,7 @@ jobs: type=raw,value=latest,enable=${{ github.event_name == 'release' }} - name: Build and push image - uses: docker/build-push-action@v6.7.0 + uses: docker/build-push-action@v6.9.0 with: file: cli/Dockerfile platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index bf393bbcf6439..8c7aeb020e7ff 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -173,7 +173,7 @@ jobs: fi - name: Build and push image - uses: docker/build-push-action@v6.7.0 + uses: docker/build-push-action@v6.9.0 with: context: ${{ env.context }} file: ${{ env.file }} @@ -264,7 +264,7 @@ jobs: fi - name: Build and push image - uses: docker/build-push-action@v6.7.0 + uses: docker/build-push-action@v6.9.0 with: context: ${{ env.context }} file: ${{ env.file }} From dfc2d5002b6c560150fd77e1d48b6b54d8315266 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 30 Sep 2024 15:50:34 -0400 Subject: [PATCH 02/22] refactor(server): client events (#13062) --- server/src/interfaces/event.interface.ts | 46 ++++++------------- server/src/repositories/event.repository.ts | 8 ++-- server/src/services/job.service.ts | 10 ++-- .../src/services/notification.service.spec.ts | 12 ++--- server/src/services/notification.service.ts | 24 +++++----- server/src/services/version.service.ts | 8 ++-- .../repositories/event.repository.mock.ts | 4 +- 7 files changed, 48 insertions(+), 64 deletions(-) diff --git a/server/src/interfaces/event.interface.ts b/server/src/interfaces/event.interface.ts index 02027d87e626a..a125e47ada3b4 100644 --- a/server/src/interfaces/event.interface.ts +++ b/server/src/interfaces/event.interface.ts @@ -62,36 +62,20 @@ export type EmitHandler = (...args: ArgsOf) => Promise = EventMap[T][0]; export type ArgsOf = EventMap[T]; -export enum ClientEvent { - UPLOAD_SUCCESS = 'on_upload_success', - USER_DELETE = 'on_user_delete', - ASSET_DELETE = 'on_asset_delete', - ASSET_TRASH = 'on_asset_trash', - ASSET_UPDATE = 'on_asset_update', - ASSET_HIDDEN = 'on_asset_hidden', - ASSET_RESTORE = 'on_asset_restore', - ASSET_STACK_UPDATE = 'on_asset_stack_update', - PERSON_THUMBNAIL = 'on_person_thumbnail', - SERVER_VERSION = 'on_server_version', - CONFIG_UPDATE = 'on_config_update', - NEW_RELEASE = 'on_new_release', - SESSION_DELETE = 'on_session_delete', -} - export interface ClientEventMap { - [ClientEvent.UPLOAD_SUCCESS]: AssetResponseDto; - [ClientEvent.USER_DELETE]: string; - [ClientEvent.ASSET_DELETE]: string; - [ClientEvent.ASSET_TRASH]: string[]; - [ClientEvent.ASSET_UPDATE]: AssetResponseDto; - [ClientEvent.ASSET_HIDDEN]: string; - [ClientEvent.ASSET_RESTORE]: string[]; - [ClientEvent.ASSET_STACK_UPDATE]: string[]; - [ClientEvent.PERSON_THUMBNAIL]: string; - [ClientEvent.SERVER_VERSION]: ServerVersionResponseDto; - [ClientEvent.CONFIG_UPDATE]: Record; - [ClientEvent.NEW_RELEASE]: ReleaseNotification; - [ClientEvent.SESSION_DELETE]: string; + on_upload_success: [AssetResponseDto]; + on_user_delete: [string]; + on_asset_delete: [string]; + on_asset_trash: [string[]]; + on_asset_update: [AssetResponseDto]; + on_asset_hidden: [string]; + on_asset_restore: [string[]]; + on_asset_stack_update: string[]; + on_person_thumbnail: [string]; + on_server_version: [ServerVersionResponseDto]; + on_config_update: []; + on_new_release: [ReleaseNotification]; + on_session_delete: [string]; } export type EventItem = { @@ -107,11 +91,11 @@ export interface IEventRepository { /** * Send to connected clients for a specific user */ - clientSend(event: E, room: string, data: ClientEventMap[E]): void; + clientSend(event: E, room: string, ...data: ClientEventMap[E]): void; /** * Send to all connected clients */ - clientBroadcast(event: E, data: ClientEventMap[E]): void; + clientBroadcast(event: E, ...data: ClientEventMap[E]): void; /** * Send to all connected servers */ diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index a8b2fa67c3395..90d8e7bf5d7a8 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -106,12 +106,12 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect } } - clientSend(event: E, room: string, data: ClientEventMap[E]) { - this.server?.to(room).emit(event, data); + clientSend(event: T, room: string, ...data: ClientEventMap[T]) { + this.server?.to(room).emit(event, ...data); } - clientBroadcast(event: E, data: ClientEventMap[E]) { - this.server?.emit(event, data); + clientBroadcast(event: T, ...data: ClientEventMap[T]) { + this.server?.emit(event, ...data); } serverSend(event: T, ...args: ArgsOf): void { diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index 68da13a8e4d4f..159efdf023a82 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -6,7 +6,7 @@ import { mapAsset } from 'src/dtos/asset-response.dto'; import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto'; import { AssetType, ManualJobName } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ArgOf, ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { ArgOf, IEventRepository } from 'src/interfaces/event.interface'; import { ConcurrentQueueName, IJobRepository, @@ -279,7 +279,7 @@ export class JobService { if (item.data.source === 'sidecar-write') { const [asset] = await this.assetRepository.getByIdsWithAllRelations([item.data.id]); if (asset) { - this.eventRepository.clientSend(ClientEvent.ASSET_UPDATE, asset.ownerId, mapAsset(asset)); + this.eventRepository.clientSend('on_asset_update', asset.ownerId, mapAsset(asset)); } } await this.jobRepository.queue({ name: JobName.LINK_LIVE_PHOTOS, data: item.data }); @@ -302,7 +302,7 @@ export class JobService { const { id } = item.data; const person = await this.personRepository.getById(id); if (person) { - this.eventRepository.clientSend(ClientEvent.PERSON_THUMBNAIL, person.ownerId, person.id); + this.eventRepository.clientSend('on_person_thumbnail', person.ownerId, person.id); } break; } @@ -331,7 +331,7 @@ export class JobService { await this.jobRepository.queueAll(jobs); if (asset.isVisible) { - this.eventRepository.clientSend(ClientEvent.UPLOAD_SUCCESS, asset.ownerId, mapAsset(asset)); + this.eventRepository.clientSend('on_upload_success', asset.ownerId, mapAsset(asset)); } break; @@ -345,7 +345,7 @@ export class JobService { } case JobName.USER_DELETION: { - this.eventRepository.clientBroadcast(ClientEvent.USER_DELETE, item.data.id); + this.eventRepository.clientBroadcast('on_user_delete', item.data.id); break; } } diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 106f0be08248d..5fba38d1eb6c3 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -6,7 +6,7 @@ import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetFileType, UserMetadataKey } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface'; @@ -104,7 +104,7 @@ describe(NotificationService.name, () => { it('should emit client and server events', () => { const update = { newConfig: defaults }; expect(sut.onConfigUpdate(update)).toBeUndefined(); - expect(eventMock.clientBroadcast).toHaveBeenCalledWith(ClientEvent.CONFIG_UPDATE, {}); + expect(eventMock.clientBroadcast).toHaveBeenCalledWith('on_config_update'); expect(eventMock.serverSend).toHaveBeenCalledWith('config.update', update); }); }); @@ -236,28 +236,28 @@ describe(NotificationService.name, () => { describe('onStackCreate', () => { it('should send connected clients an event', () => { sut.onStackCreate({ stackId: 'stack-id', userId: 'user-id' }); - expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id'); }); }); describe('onStackUpdate', () => { it('should send connected clients an event', () => { sut.onStackUpdate({ stackId: 'stack-id', userId: 'user-id' }); - expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id'); }); }); describe('onStackDelete', () => { it('should send connected clients an event', () => { sut.onStackDelete({ stackId: 'stack-id', userId: 'user-id' }); - expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id'); }); }); describe('onStacksDelete', () => { it('should send connected clients an event', () => { sut.onStacksDelete({ stackIds: ['stack-id'], userId: 'user-id' }); - expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id'); }); }); diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index 626e536c409e2..a3adfa45656f8 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -6,7 +6,7 @@ import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { AlbumEntity } from 'src/entities/album.entity'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ArgOf, ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { ArgOf, IEventRepository } from 'src/interfaces/event.interface'; import { IEmailJob, IJobRepository, @@ -45,7 +45,7 @@ export class NotificationService { @OnEvent({ name: 'config.update' }) onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) { - this.eventRepository.clientBroadcast(ClientEvent.CONFIG_UPDATE, {}); + this.eventRepository.clientBroadcast('on_config_update'); this.eventRepository.serverSend('config.update', { oldConfig, newConfig }); } @@ -66,7 +66,7 @@ export class NotificationService { @OnEvent({ name: 'asset.hide' }) onAssetHide({ assetId, userId }: ArgOf<'asset.hide'>) { - this.eventRepository.clientSend(ClientEvent.ASSET_HIDDEN, userId, assetId); + this.eventRepository.clientSend('on_asset_hidden', userId, assetId); } @OnEvent({ name: 'asset.show' }) @@ -76,42 +76,42 @@ export class NotificationService { @OnEvent({ name: 'asset.trash' }) onAssetTrash({ assetId, userId }: ArgOf<'asset.trash'>) { - this.eventRepository.clientSend(ClientEvent.ASSET_TRASH, userId, [assetId]); + this.eventRepository.clientSend('on_asset_trash', userId, [assetId]); } @OnEvent({ name: 'asset.delete' }) onAssetDelete({ assetId, userId }: ArgOf<'asset.delete'>) { - this.eventRepository.clientSend(ClientEvent.ASSET_DELETE, userId, assetId); + this.eventRepository.clientSend('on_asset_delete', userId, assetId); } @OnEvent({ name: 'assets.trash' }) onAssetsTrash({ assetIds, userId }: ArgOf<'assets.trash'>) { - this.eventRepository.clientSend(ClientEvent.ASSET_TRASH, userId, assetIds); + this.eventRepository.clientSend('on_asset_trash', userId, assetIds); } @OnEvent({ name: 'assets.restore' }) onAssetsRestore({ assetIds, userId }: ArgOf<'assets.restore'>) { - this.eventRepository.clientSend(ClientEvent.ASSET_RESTORE, userId, assetIds); + this.eventRepository.clientSend('on_asset_restore', userId, assetIds); } @OnEvent({ name: 'stack.create' }) onStackCreate({ userId }: ArgOf<'stack.create'>) { - this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []); + this.eventRepository.clientSend('on_asset_stack_update', userId); } @OnEvent({ name: 'stack.update' }) onStackUpdate({ userId }: ArgOf<'stack.update'>) { - this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []); + this.eventRepository.clientSend('on_asset_stack_update', userId); } @OnEvent({ name: 'stack.delete' }) onStackDelete({ userId }: ArgOf<'stack.delete'>) { - this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []); + this.eventRepository.clientSend('on_asset_stack_update', userId); } @OnEvent({ name: 'stacks.delete' }) onStacksDelete({ userId }: ArgOf<'stacks.delete'>) { - this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []); + this.eventRepository.clientSend('on_asset_stack_update', userId); } @OnEvent({ name: 'user.signup' }) @@ -134,7 +134,7 @@ export class NotificationService { @OnEvent({ name: 'session.delete' }) onSessionDelete({ sessionId }: ArgOf<'session.delete'>) { // after the response is sent - setTimeout(() => this.eventRepository.clientSend(ClientEvent.SESSION_DELETE, sessionId, sessionId), 500); + setTimeout(() => this.eventRepository.clientSend('on_session_delete', sessionId, sessionId), 500); } async sendTestEmail(id: string, dto: SystemConfigSmtpDto) { diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index 0c7ae52cac7a8..1d2785356e3ed 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -7,7 +7,7 @@ import { OnEvent } from 'src/decorators'; import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; import { VersionCheckMetadata } from 'src/entities/system-metadata.entity'; import { SystemMetadataKey } from 'src/enum'; -import { ArgOf, ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { ArgOf, IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; @@ -80,7 +80,7 @@ export class VersionService { if (semver.gt(releaseVersion, serverVersion)) { this.logger.log(`Found ${releaseVersion}, released at ${new Date(publishedAt).toLocaleString()}`); - this.eventRepository.clientBroadcast(ClientEvent.NEW_RELEASE, asNotification(metadata)); + this.eventRepository.clientBroadcast('on_new_release', asNotification(metadata)); } } catch (error: Error | any) { this.logger.warn(`Unable to run version check: ${error}`, error?.stack); @@ -92,10 +92,10 @@ export class VersionService { @OnEvent({ name: 'websocket.connect' }) async onWebsocketConnection({ userId }: ArgOf<'websocket.connect'>) { - this.eventRepository.clientSend(ClientEvent.SERVER_VERSION, userId, serverVersion); + this.eventRepository.clientSend('on_server_version', userId, serverVersion); const metadata = await this.systemMetadataRepository.get(SystemMetadataKey.VERSION_CHECK_STATE); if (metadata) { - this.eventRepository.clientSend(ClientEvent.NEW_RELEASE, userId, asNotification(metadata)); + this.eventRepository.clientSend('on_new_release', userId, asNotification(metadata)); } } } diff --git a/server/test/repositories/event.repository.mock.ts b/server/test/repositories/event.repository.mock.ts index 78c62e95f2c95..6893b29f49a6f 100644 --- a/server/test/repositories/event.repository.mock.ts +++ b/server/test/repositories/event.repository.mock.ts @@ -5,8 +5,8 @@ export const newEventRepositoryMock = (): Mocked => { return { on: vitest.fn() as any, emit: vitest.fn() as any, - clientSend: vitest.fn(), - clientBroadcast: vitest.fn(), + clientSend: vitest.fn() as any, + clientBroadcast: vitest.fn() as any, serverSend: vitest.fn(), }; }; From f63d251490de6d94dc47b853337b2a47801e9cc6 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 30 Sep 2024 16:04:24 -0400 Subject: [PATCH 03/22] refactor(server): user core (#13063) --- server/src/cores/user.core.ts | 51 ------------------- server/src/services/auth.service.ts | 38 +++++++------- server/src/services/user-admin.service.ts | 7 +-- server/src/utils/user.ts | 35 +++++++++++++ .../test/repositories/user.repository.mock.ts | 7 +-- 5 files changed, 59 insertions(+), 79 deletions(-) delete mode 100644 server/src/cores/user.core.ts create mode 100644 server/src/utils/user.ts diff --git a/server/src/cores/user.core.ts b/server/src/cores/user.core.ts deleted file mode 100644 index 153463a9cc692..0000000000000 --- a/server/src/cores/user.core.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { BadRequestException } from '@nestjs/common'; -import sanitize from 'sanitize-filename'; -import { SALT_ROUNDS } from 'src/constants'; -import { UserEntity } from 'src/entities/user.entity'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; -import { IUserRepository } from 'src/interfaces/user.interface'; - -let instance: UserCore | null; - -export class UserCore { - private constructor( - private cryptoRepository: ICryptoRepository, - private userRepository: IUserRepository, - ) {} - - static create(cryptoRepository: ICryptoRepository, userRepository: IUserRepository) { - if (!instance) { - instance = new UserCore(cryptoRepository, userRepository); - } - - return instance; - } - - static reset() { - instance = null; - } - - async createUser(dto: Partial & { email: string }): Promise { - const user = await this.userRepository.getByEmail(dto.email); - if (user) { - throw new BadRequestException('User exists'); - } - - if (!dto.isAdmin) { - const localAdmin = await this.userRepository.getAdmin(); - if (!localAdmin) { - throw new BadRequestException('The first registered account must the administrator.'); - } - } - - const payload: Partial = { ...dto }; - if (payload.password) { - payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS); - } - if (payload.storageLabel) { - payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', '')); - } - - return this.userRepository.create(payload); - } -} diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 6b1e4c512f816..0917fc2198310 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -14,7 +14,6 @@ import { Issuer, UserinfoResponse, custom, generators } from 'openid-client'; import { SystemConfig } from 'src/config'; import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants'; import { SystemConfigCore } from 'src/cores/system-config.core'; -import { UserCore } from 'src/cores/user.core'; import { AuthDto, ChangePasswordDto, @@ -42,6 +41,7 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf import { IUserRepository } from 'src/interfaces/user.interface'; import { isGranted } from 'src/utils/access'; import { HumanReadableSize } from 'src/utils/bytes'; +import { createUser } from 'src/utils/user'; export interface LoginDetails { isSecure: boolean; @@ -72,7 +72,6 @@ export type ValidateRequest = { @Injectable() export class AuthService { private configCore: SystemConfigCore; - private userCore: UserCore; constructor( @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @@ -86,7 +85,6 @@ export class AuthService { ) { this.logger.setContext(AuthService.name); this.configCore = SystemConfigCore.create(systemMetadataRepository, logger); - this.userCore = UserCore.create(cryptoRepository, userRepository); custom.setHttpOptionsDefaults({ timeout: 30_000 }); } @@ -150,13 +148,16 @@ export class AuthService { throw new BadRequestException('The server already has an admin'); } - const admin = await this.userCore.createUser({ - isAdmin: true, - email: dto.email, - name: dto.name, - password: dto.password, - storageLabel: 'admin', - }); + const admin = await createUser( + { userRepo: this.userRepository, cryptoRepo: this.cryptoRepository }, + { + isAdmin: true, + email: dto.email, + name: dto.name, + password: dto.password, + storageLabel: 'admin', + }, + ); return mapUserAdmin(admin); } @@ -271,13 +272,16 @@ export class AuthService { }); const userName = profile.name ?? `${profile.given_name || ''} ${profile.family_name || ''}`; - user = await this.userCore.createUser({ - name: userName, - email: profile.email, - oauthId: profile.sub, - quotaSizeInBytes: storageQuota * HumanReadableSize.GiB || null, - storageLabel: storageLabel || null, - }); + user = await createUser( + { userRepo: this.userRepository, cryptoRepo: this.cryptoRepository }, + { + name: userName, + email: profile.email, + oauthId: profile.sub, + quotaSizeInBytes: storageQuota * HumanReadableSize.GiB || null, + storageLabel: storageLabel || null, + }, + ); } return this.createLoginResponse(user, loginDetails); diff --git a/server/src/services/user-admin.service.ts b/server/src/services/user-admin.service.ts index 6a5b6ea06e520..75dff32f160a9 100644 --- a/server/src/services/user-admin.service.ts +++ b/server/src/services/user-admin.service.ts @@ -1,6 +1,5 @@ import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common'; import { SALT_ROUNDS } from 'src/constants'; -import { UserCore } from 'src/cores/user.core'; import { AuthDto } from 'src/dtos/auth.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto'; import { @@ -19,11 +18,10 @@ import { IJobRepository, JobName } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface'; import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences'; +import { createUser } from 'src/utils/user'; @Injectable() export class UserAdminService { - private userCore: UserCore; - constructor( @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @@ -32,7 +30,6 @@ export class UserAdminService { @Inject(IUserRepository) private userRepository: IUserRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { - this.userCore = UserCore.create(cryptoRepository, userRepository); this.logger.setContext(UserAdminService.name); } @@ -43,7 +40,7 @@ export class UserAdminService { async create(dto: UserAdminCreateDto): Promise { const { notify, ...rest } = dto; - const user = await this.userCore.createUser(rest); + const user = await createUser({ userRepo: this.userRepository, cryptoRepo: this.cryptoRepository }, rest); await this.eventRepository.emit('user.signup', { notify: !!notify, diff --git a/server/src/utils/user.ts b/server/src/utils/user.ts new file mode 100644 index 0000000000000..c7029a1ecae4b --- /dev/null +++ b/server/src/utils/user.ts @@ -0,0 +1,35 @@ +import { BadRequestException } from '@nestjs/common'; +import sanitize from 'sanitize-filename'; +import { SALT_ROUNDS } from 'src/constants'; +import { UserEntity } from 'src/entities/user.entity'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; + +type RepoDeps = { userRepo: IUserRepository; cryptoRepo: ICryptoRepository }; + +export const createUser = async ( + { userRepo, cryptoRepo }: RepoDeps, + dto: Partial & { email: string }, +): Promise => { + const user = await userRepo.getByEmail(dto.email); + if (user) { + throw new BadRequestException('User exists'); + } + + if (!dto.isAdmin) { + const localAdmin = await userRepo.getAdmin(); + if (!localAdmin) { + throw new BadRequestException('The first registered account must the administrator.'); + } + } + + const payload: Partial = { ...dto }; + if (payload.password) { + payload.password = await cryptoRepo.hashBcrypt(payload.password, SALT_ROUNDS); + } + if (payload.storageLabel) { + payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', '')); + } + + return userRepo.create(payload); +}; diff --git a/server/test/repositories/user.repository.mock.ts b/server/test/repositories/user.repository.mock.ts index 6071ae47fa83f..6362ab6a999ff 100644 --- a/server/test/repositories/user.repository.mock.ts +++ b/server/test/repositories/user.repository.mock.ts @@ -1,12 +1,7 @@ -import { UserCore } from 'src/cores/user.core'; import { IUserRepository } from 'src/interfaces/user.interface'; import { Mocked, vitest } from 'vitest'; -export const newUserRepositoryMock = (reset = true): Mocked => { - if (reset) { - UserCore.reset(); - } - +export const newUserRepositoryMock = (): Mocked => { return { get: vitest.fn(), getAdmin: vitest.fn(), From a019fb670e0c1edfea24cd6620e4eaecf54a0600 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 30 Sep 2024 17:31:21 -0400 Subject: [PATCH 04/22] refactor(server): config service (#13066) * refactor(server): config service * fix: function renaming --------- Co-authored-by: Daniel Dietzler --- .../src/controllers/server-info.controller.ts | 2 +- server/src/controllers/server.controller.ts | 2 +- .../controllers/system-config.controller.ts | 4 +- server/src/cores/storage.core.ts | 12 +- server/src/cores/system-config.core.ts | 143 ------------------ server/src/services/asset.service.ts | 12 +- server/src/services/auth.service.ts | 20 ++- server/src/services/base.service.ts | 32 ++++ server/src/services/cli.service.ts | 26 ++-- server/src/services/duplicate.service.ts | 14 +- server/src/services/job.service.ts | 11 +- server/src/services/library.service.ts | 11 +- server/src/services/map.service.spec.ts | 10 +- server/src/services/map.service.ts | 12 +- server/src/services/media.service.ts | 18 +-- server/src/services/metadata.service.ts | 13 +- server/src/services/notification.service.ts | 20 ++- server/src/services/person.service.ts | 23 ++- server/src/services/search.service.ts | 12 +- server/src/services/server.service.spec.ts | 4 +- server/src/services/server.service.ts | 23 ++- server/src/services/shared-link.service.ts | 12 +- server/src/services/smart-info.service.ts | 16 +- .../src/services/storage-template.service.ts | 13 +- .../services/system-config.service.spec.ts | 26 ++-- server/src/services/system-config.service.ts | 31 ++-- server/src/services/user.service.ts | 14 +- server/src/services/version.service.ts | 14 +- server/src/utils/config.ts | 129 ++++++++++++++++ .../system-metadata.repository.mock.ts | 9 +- 30 files changed, 327 insertions(+), 361 deletions(-) delete mode 100644 server/src/cores/system-config.core.ts create mode 100644 server/src/services/base.service.ts create mode 100644 server/src/utils/config.ts diff --git a/server/src/controllers/server-info.controller.ts b/server/src/controllers/server-info.controller.ts index 245bbbd3475a4..36490b71198ba 100644 --- a/server/src/controllers/server-info.controller.ts +++ b/server/src/controllers/server-info.controller.ts @@ -66,7 +66,7 @@ export class ServerInfoController { @Get('config') @EndpointLifecycle({ deprecatedAt: 'v1.107.0' }) getServerConfig(): Promise { - return this.service.getConfig(); + return this.service.getSystemConfig(); } @Get('statistics') diff --git a/server/src/controllers/server.controller.ts b/server/src/controllers/server.controller.ts index 75becfe341615..8fcd93946e4f6 100644 --- a/server/src/controllers/server.controller.ts +++ b/server/src/controllers/server.controller.ts @@ -58,7 +58,7 @@ export class ServerController { @Get('config') getServerConfig(): Promise { - return this.service.getConfig(); + return this.service.getSystemConfig(); } @Get('statistics') diff --git a/server/src/controllers/system-config.controller.ts b/server/src/controllers/system-config.controller.ts index 804c19500facd..f59c8ad66cbeb 100644 --- a/server/src/controllers/system-config.controller.ts +++ b/server/src/controllers/system-config.controller.ts @@ -13,7 +13,7 @@ export class SystemConfigController { @Get() @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true }) getConfig(): Promise { - return this.service.getConfig(); + return this.service.getSystemConfig(); } @Get('defaults') @@ -25,7 +25,7 @@ export class SystemConfigController { @Put() @Authenticated({ permission: Permission.SYSTEM_CONFIG_UPDATE, admin: true }) updateConfig(@Body() dto: SystemConfigDto): Promise { - return this.service.updateConfig(dto); + return this.service.updateSystemConfig(dto); } @Get('storage-template-options') diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index 8ce8f6b67a228..d33d81410ce47 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -1,7 +1,6 @@ import { randomUUID } from 'node:crypto'; import { dirname, join, resolve } from 'node:path'; import { APP_MEDIA_LOCATION } from 'src/constants'; -import { SystemConfigCore } from 'src/cores/system-config.core'; import { AssetEntity } from 'src/entities/asset.entity'; import { PersonEntity } from 'src/entities/person.entity'; import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum'; @@ -13,6 +12,7 @@ import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { getAssetFiles } from 'src/utils/asset.util'; +import { getConfig } from 'src/utils/config'; export const THUMBNAIL_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.THUMBNAILS)); export const ENCODED_VIDEO_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.ENCODED_VIDEO)); @@ -34,18 +34,15 @@ export type GeneratedAssetType = GeneratedImageType | AssetPathType.ENCODED_VIDE let instance: StorageCore | null; export class StorageCore { - private configCore; private constructor( private assetRepository: IAssetRepository, private cryptoRepository: ICryptoRepository, private moveRepository: IMoveRepository, private personRepository: IPersonRepository, private storageRepository: IStorageRepository, - systemMetadataRepository: ISystemMetadataRepository, + private systemMetadataRepository: ISystemMetadataRepository, private logger: ILoggerRepository, - ) { - this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); - } + ) {} static create( assetRepository: IAssetRepository, @@ -248,7 +245,8 @@ export class StorageCore { this.logger.warn(`Unable to complete move. File size mismatch: ${newPathSize} !== ${oldPathSize}`); return false; } - const config = await this.configCore.getConfig({ withCache: true }); + const repos = { metadataRepo: this.systemMetadataRepository, logger: this.logger }; + const config = await getConfig(repos, { withCache: true }); if (assetInfo && config.storageTemplate.hashVerificationEnabled) { const { checksum } = assetInfo; const newChecksum = await this.cryptoRepository.hashFile(newPath); diff --git a/server/src/cores/system-config.core.ts b/server/src/cores/system-config.core.ts deleted file mode 100644 index 816ab004460a9..0000000000000 --- a/server/src/cores/system-config.core.ts +++ /dev/null @@ -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; - -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 { - 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 { - // get the difference between the new config and the default config - const partialConfig: DeepPartial = {}; - 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; - } - } -} diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index aa88eaf9576b4..171005ab74a56 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, Inject } from '@nestjs/common'; import _ from 'lodash'; import { DateTime, Duration } from 'luxon'; -import { SystemConfigCore } from 'src/cores/system-config.core'; import { AssetResponseDto, MemoryLaneResponseDto, @@ -38,13 +37,12 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IStackRepository } from 'src/interfaces/stack.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { BaseService } from 'src/services/base.service'; import { requireAccess } from 'src/utils/access'; import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util'; import { usePagination } from 'src/utils/pagination'; -export class AssetService { - private configCore: SystemConfigCore; - +export class AssetService extends BaseService { constructor( @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @@ -54,10 +52,10 @@ export class AssetService { @Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, @Inject(IStackRepository) private stackRepository: IStackRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, + @Inject(ILoggerRepository) logger: ILoggerRepository, ) { + super(systemMetadataRepository, logger); this.logger.setContext(AssetService.name); - this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } async getMemoryLane(auth: AuthDto, dto: MemoryLaneDto): Promise { @@ -214,7 +212,7 @@ export class AssetService { } async handleAssetDeletionCheck(): Promise { - 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 trashedBefore = DateTime.now() .minus(Duration.fromObject({ days: trashedDays })) diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 0917fc2198310..72d251ce78f38 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -13,7 +13,6 @@ import { IncomingHttpHeaders } from 'node:http'; import { Issuer, UserinfoResponse, custom, generators } from 'openid-client'; import { SystemConfig } from 'src/config'; import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants'; -import { SystemConfigCore } from 'src/cores/system-config.core'; import { AuthDto, ChangePasswordDto, @@ -39,6 +38,7 @@ import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { BaseService } from 'src/services/base.service'; import { isGranted } from 'src/utils/access'; import { HumanReadableSize } from 'src/utils/bytes'; import { createUser } from 'src/utils/user'; @@ -70,27 +70,25 @@ export type ValidateRequest = { }; @Injectable() -export class AuthService { - private configCore: SystemConfigCore; - +export class AuthService extends BaseService { constructor( @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, + @Inject(ILoggerRepository) logger: ILoggerRepository, @Inject(IUserRepository) private userRepository: IUserRepository, @Inject(ISessionRepository) private sessionRepository: ISessionRepository, @Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository, @Inject(IKeyRepository) private keyRepository: IKeyRepository, ) { + super(systemMetadataRepository, logger); this.logger.setContext(AuthService.name); - this.configCore = SystemConfigCore.create(systemMetadataRepository, logger); custom.setHttpOptionsDefaults({ timeout: 30_000 }); } 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) { throw new UnauthorizedException('Password login has been disabled'); } @@ -212,7 +210,7 @@ export class AuthService { } async authorize(dto: OAuthConfigDto): Promise { - const config = await this.configCore.getConfig({ withCache: false }); + const config = await this.getConfig({ withCache: false }); if (!config.oauth.enabled) { throw new BadRequestException('OAuth is not enabled'); } @@ -228,7 +226,7 @@ export class AuthService { } 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 { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim } = config.oauth; this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`); @@ -288,7 +286,7 @@ export class AuthService { } async link(auth: AuthDto, dto: OAuthCallbackDto): Promise { - 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 duplicate = await this.userRepository.getByOAuthId(oauthId); if (duplicate && duplicate.id !== auth.user.id) { @@ -310,7 +308,7 @@ export class AuthService { return LOGIN_URL; } - const config = await this.configCore.getConfig({ withCache: false }); + const config = await this.getConfig({ withCache: false }); if (!config.oauth.enabled) { return LOGIN_URL; } diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts new file mode 100644 index 0000000000000..776858aa1a544 --- /dev/null +++ b/server/src/services/base.service.ts @@ -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, + ); + } +} diff --git a/server/src/services/cli.service.ts b/server/src/services/cli.service.ts index 1c25c306b6843..c7ed510f5d550 100644 --- a/server/src/services/cli.service.ts +++ b/server/src/services/cli.service.ts @@ -1,24 +1,22 @@ import { Inject, Injectable } from '@nestjs/common'; import { SALT_ROUNDS } from 'src/constants'; -import { SystemConfigCore } from 'src/cores/system-config.core'; import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { BaseService } from 'src/services/base.service'; @Injectable() -export class CliService { - private configCore: SystemConfigCore; - +export class CliService extends BaseService { constructor( @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, @Inject(IUserRepository) private userRepository: IUserRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, + @Inject(ILoggerRepository) logger: ILoggerRepository, ) { + super(systemMetadataRepository, logger); this.logger.setContext(CliService.name); - this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } async listUsers(): Promise { @@ -42,26 +40,26 @@ export class CliService { } async disablePasswordLogin(): Promise { - const config = await this.configCore.getConfig({ withCache: false }); + const config = await this.getConfig({ withCache: false }); config.passwordLogin.enabled = false; - await this.configCore.updateConfig(config); + await this.updateConfig(config); } async enablePasswordLogin(): Promise { - const config = await this.configCore.getConfig({ withCache: false }); + const config = await this.getConfig({ withCache: false }); config.passwordLogin.enabled = true; - await this.configCore.updateConfig(config); + await this.updateConfig(config); } async disableOAuthLogin(): Promise { - const config = await this.configCore.getConfig({ withCache: false }); + const config = await this.getConfig({ withCache: false }); config.oauth.enabled = false; - await this.configCore.updateConfig(config); + await this.updateConfig(config); } async enableOAuthLogin(): Promise { - const config = await this.configCore.getConfig({ withCache: false }); + const config = await this.getConfig({ withCache: false }); config.oauth.enabled = true; - await this.configCore.updateConfig(config); + await this.updateConfig(config); } } diff --git a/server/src/services/duplicate.service.ts b/server/src/services/duplicate.service.ts index 35a1a7325bb2f..00f738a613e6c 100644 --- a/server/src/services/duplicate.service.ts +++ b/server/src/services/duplicate.service.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import { SystemConfigCore } from 'src/cores/system-config.core'; import { mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { DuplicateResponseDto, mapDuplicateResponse } from 'src/dtos/duplicate.dto'; @@ -17,24 +16,23 @@ import { import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AssetDuplicateResult, ISearchRepository } from 'src/interfaces/search.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { BaseService } from 'src/services/base.service'; import { getAssetFiles } from 'src/utils/asset.util'; import { isDuplicateDetectionEnabled } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; @Injectable() -export class DuplicateService { - private configCore: SystemConfigCore; - +export class DuplicateService extends BaseService { constructor( @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, @Inject(ISearchRepository) private searchRepository: ISearchRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, + @Inject(ILoggerRepository) logger: ILoggerRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, ) { + super(systemMetadataRepository, logger); this.logger.setContext(DuplicateService.name); - this.configCore = SystemConfigCore.create(systemMetadataRepository, logger); } async getDuplicates(auth: AuthDto): Promise { @@ -44,7 +42,7 @@ export class DuplicateService { } async handleQueueSearchDuplicates({ force }: IBaseJob): Promise { - const { machineLearning } = await this.configCore.getConfig({ withCache: false }); + const { machineLearning } = await this.getConfig({ withCache: false }); if (!isDuplicateDetectionEnabled(machineLearning)) { return JobStatus.SKIPPED; } @@ -65,7 +63,7 @@ export class DuplicateService { } async handleSearchDuplicates({ id }: IEntityJob): Promise { - const { machineLearning } = await this.configCore.getConfig({ withCache: true }); + const { machineLearning } = await this.getConfig({ withCache: true }); if (!isDuplicateDetectionEnabled(machineLearning)) { return JobStatus.SKIPPED; } diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index 159efdf023a82..8bcf5e5622430 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -1,6 +1,5 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { snakeCase } from 'lodash'; -import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnEvent } from 'src/decorators'; import { mapAsset } from 'src/dtos/asset-response.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 { IPersonRepository } from 'src/interfaces/person.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { BaseService } from 'src/services/base.service'; const asJobItem = (dto: JobCreateDto): JobItem => { switch (dto.name) { @@ -44,8 +44,7 @@ const asJobItem = (dto: JobCreateDto): JobItem => { }; @Injectable() -export class JobService { - private configCore: SystemConfigCore; +export class JobService extends BaseService { private isMicroservices = false; constructor( @@ -55,10 +54,10 @@ export class JobService { @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, @Inject(IPersonRepository) private personRepository: IPersonRepository, @Inject(IMetricRepository) private metricRepository: IMetricRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, + @Inject(ILoggerRepository) logger: ILoggerRepository, ) { + super(systemMetadataRepository, logger); this.logger.setContext(JobService.name); - this.configCore = SystemConfigCore.create(systemMetadataRepository, logger); } @OnEvent({ name: 'app.bootstrap' }) @@ -198,7 +197,7 @@ export class JobService { } async init(jobHandlers: Record) { - const config = await this.configCore.getConfig({ withCache: false }); + const config = await this.getConfig({ withCache: false }); for (const queueName of Object.values(QueueName)) { let concurrency = 1; diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index b8b478531ff96..4b296570eb38f 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -3,7 +3,6 @@ import { R_OK } from 'node:constants'; import path, { basename, parse } from 'node:path'; import picomatch from 'picomatch'; import { StorageCore } from 'src/cores/storage.core'; -import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnEvent } from 'src/decorators'; import { CreateLibraryDto, @@ -35,14 +34,14 @@ import { ILibraryRepository } from 'src/interfaces/library.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { BaseService } from 'src/services/base.service'; import { mimeTypes } from 'src/utils/mime-types'; import { handlePromiseError } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; import { validateCronExpression } from 'src/validation'; @Injectable() -export class LibraryService { - private configCore: SystemConfigCore; +export class LibraryService extends BaseService { private watchLibraries = false; private watchLock = false; private watchers: Record Promise> = {}; @@ -55,15 +54,15 @@ export class LibraryService { @Inject(ILibraryRepository) private repository: ILibraryRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, + @Inject(ILoggerRepository) logger: ILoggerRepository, ) { + super(systemMetadataRepository, logger); this.logger.setContext(LibraryService.name); - this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } @OnEvent({ name: 'app.bootstrap' }) async onBootstrap() { - const config = await this.configCore.getConfig({ withCache: false }); + const config = await this.getConfig({ withCache: false }); const { watch, scan } = config.library; diff --git a/server/src/services/map.service.spec.ts b/server/src/services/map.service.spec.ts index f8b73260aff3d..e0127b73efbb9 100644 --- a/server/src/services/map.service.spec.ts +++ b/server/src/services/map.service.spec.ts @@ -1,34 +1,26 @@ import { IAlbumRepository } from 'src/interfaces/album.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMapRepository } from 'src/interfaces/map.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { MapService } from 'src/services/map.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; 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 { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; import { Mocked } from 'vitest'; describe(MapService.name, () => { let sut: MapService; let albumMock: Mocked; - let loggerMock: Mocked; let partnerMock: Mocked; let mapMock: Mocked; - let systemMetadataMock: Mocked; beforeEach(() => { albumMock = newAlbumRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); partnerMock = newPartnerRepositoryMock(); mapMock = newMapRepositoryMock(); - systemMetadataMock = newSystemMetadataRepositoryMock(); - sut = new MapService(albumMock, loggerMock, partnerMock, mapMock, systemMetadataMock); + sut = new MapService(albumMock, partnerMock, mapMock); }); describe('getMapMarkers', () => { diff --git a/server/src/services/map.service.ts b/server/src/services/map.service.ts index 5836505e54893..3b1ee58cf124d 100644 --- a/server/src/services/map.service.ts +++ b/server/src/services/map.service.ts @@ -1,27 +1,17 @@ import { Inject } from '@nestjs/common'; -import { SystemConfigCore } from 'src/cores/system-config.core'; import { AuthDto } from 'src/dtos/auth.dto'; import { MapMarkerDto, MapMarkerResponseDto, MapReverseGeocodeDto } from 'src/dtos/map.dto'; import { IAlbumRepository } from 'src/interfaces/album.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMapRepository } from 'src/interfaces/map.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { getMyPartnerIds } from 'src/utils/asset.util'; export class MapService { - private configCore: SystemConfigCore; - constructor( @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, @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 { const userIds = [auth.user.id]; diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 71f432e040c43..1f72c373f4023 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -1,8 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { dirname } from 'node:path'; - import { StorageCore } from 'src/cores/storage.core'; -import { SystemConfigCore } from 'src/cores/system-config.core'; import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { @@ -43,14 +41,14 @@ import { IMoveRepository } from 'src/interfaces/move.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { BaseService } from 'src/services/base.service'; import { getAssetFiles } from 'src/utils/asset.util'; import { BaseConfig, ThumbnailConfig } from 'src/utils/media'; import { mimeTypes } from 'src/utils/mime-types'; import { usePagination } from 'src/utils/pagination'; @Injectable() -export class MediaService { - private configCore: SystemConfigCore; +export class MediaService extends BaseService { private storageCore: StorageCore; private maliOpenCL?: boolean; private devices?: string[]; @@ -64,10 +62,10 @@ export class MediaService { @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, @Inject(IMoveRepository) moveRepository: IMoveRepository, @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, + @Inject(ILoggerRepository) logger: ILoggerRepository, ) { + super(systemMetadataRepository, logger); this.logger.setContext(MediaService.name); - this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); this.storageCore = StorageCore.create( assetRepository, cryptoRepository, @@ -161,7 +159,7 @@ export class MediaService { } async handleAssetMigration({ id }: IEntityJob): Promise { - const { image } = await this.configCore.getConfig({ withCache: true }); + const { image } = await this.getConfig({ withCache: true }); const [asset] = await this.assetRepository.getByIds([id], { files: true }); if (!asset) { return JobStatus.FAILED; @@ -235,7 +233,7 @@ export class MediaService { } 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 thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format); this.storageCore.ensureFolders(previewPath); @@ -269,7 +267,7 @@ export class MediaService { } 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 thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format); this.storageCore.ensureFolders(previewPath); @@ -339,7 +337,7 @@ export class MediaService { 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); if (target === TranscodeTarget.NONE && !this.isRemuxRequired(ffmpeg, format)) { if (asset.encodedVideoPath) { diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 9499a4bdd99b0..e39e22b92fd61 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -7,7 +7,6 @@ import { constants } from 'node:fs/promises'; import path from 'node:path'; import { SystemConfig } from 'src/config'; import { StorageCore } from 'src/cores/storage.core'; -import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnEvent } from 'src/decorators'; import { AssetFaceEntity } from 'src/entities/asset-face.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 { ITagRepository } from 'src/interfaces/tag.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { BaseService } from 'src/services/base.service'; import { isFaceImportEnabled } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; import { upsertTags } from 'src/utils/tag'; @@ -97,9 +97,8 @@ const validateRange = (value: number | undefined, min: number, max: number): Non }; @Injectable() -export class MetadataService { +export class MetadataService extends BaseService { private storageCore: StorageCore; - private configCore: SystemConfigCore; constructor( @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, @@ -117,10 +116,10 @@ export class MetadataService { @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, @Inject(ITagRepository) private tagRepository: ITagRepository, @Inject(IUserRepository) private userRepository: IUserRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, + @Inject(ILoggerRepository) logger: ILoggerRepository, ) { + super(systemMetadataRepository, logger); this.logger.setContext(MetadataService.name); - this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); this.storageCore = StorageCore.create( assetRepository, cryptoRepository, @@ -137,7 +136,7 @@ export class MetadataService { if (app !== 'microservices') { return; } - const config = await this.configCore.getConfig({ withCache: false }); + const config = await this.getConfig({ withCache: false }); await this.init(config); } @@ -222,7 +221,7 @@ export class MetadataService { } async handleMetadataExtraction({ id }: IEntityJob): Promise { - const { metadata, reverseGeocoding } = await this.configCore.getConfig({ withCache: true }); + const { metadata, reverseGeocoding } = await this.getConfig({ withCache: true }); const [asset] = await this.assetRepository.getByIds([id]); if (!asset) { return JobStatus.FAILED; diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index a3adfa45656f8..cf6b89384d5e8 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -1,6 +1,5 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants'; -import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnEvent } from 'src/decorators'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; 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 { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { BaseService } from 'src/services/base.service'; import { getAssetFiles } from 'src/utils/asset.util'; import { getFilenameExtension } from 'src/utils/file'; import { isEqualObject } from 'src/utils/object'; import { getPreferences } from 'src/utils/preferences'; @Injectable() -export class NotificationService { - private configCore: SystemConfigCore; - +export class NotificationService extends BaseService { constructor( @Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, @Inject(INotificationRepository) private notificationRepository: INotificationRepository, @Inject(IUserRepository) private userRepository: IUserRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, + @Inject(ILoggerRepository) logger: ILoggerRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, ) { + super(systemMetadataRepository, logger); this.logger.setContext(NotificationService.name); - this.configCore = SystemConfigCore.create(systemMetadataRepository, logger); } @OnEvent({ name: 'config.update' }) @@ -149,7 +147,7 @@ export class NotificationService { 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({ template: EmailTemplate.TEST_EMAIL, data: { @@ -177,7 +175,7 @@ export class NotificationService { 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({ template: EmailTemplate.WELCOME, data: { @@ -220,7 +218,7 @@ export class NotificationService { 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({ template: EmailTemplate.ALBUM_INVITE, data: { @@ -262,7 +260,7 @@ export class NotificationService { const recipients = [...album.albumUsers.map((user) => user.user), owner].filter((user) => user.id !== senderId); 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) { const user = await this.userRepository.get(recipient.id, { withDeleted: false }); @@ -303,7 +301,7 @@ export class NotificationService { } async handleSendEmail(data: IEmailJob): Promise { - const { notifications } = await this.configCore.getConfig({ withCache: false }); + const { notifications } = await this.getConfig({ withCache: false }); if (!notifications.smtp.enabled) { return JobStatus.SKIPPED; } diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index b009696b637fe..7cb6f42f15a44 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { FACE_THUMBNAIL_SIZE } from 'src/constants'; 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 { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.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 { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { BaseService } from 'src/services/base.service'; import { checkAccess, requireAccess } from 'src/utils/access'; import { getAssetFiles } from 'src/utils/asset.util'; import { ImmichFileResponse } from 'src/utils/file'; @@ -64,8 +64,7 @@ import { usePagination } from 'src/utils/pagination'; import { IsNull } from 'typeorm'; @Injectable() -export class PersonService { - private configCore: SystemConfigCore; +export class PersonService extends BaseService { private storageCore: StorageCore; constructor( @@ -75,15 +74,15 @@ export class PersonService { @Inject(IMoveRepository) moveRepository: IMoveRepository, @Inject(IMediaRepository) private mediaRepository: IMediaRepository, @Inject(IPersonRepository) private repository: IPersonRepository, - @Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository, + @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(ISearchRepository) private smartInfoRepository: ISearchRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, + @Inject(ILoggerRepository) logger: ILoggerRepository, ) { + super(systemMetadataRepository, logger); this.logger.setContext(PersonService.name); - this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); this.storageCore = StorageCore.create( assetRepository, cryptoRepository, @@ -102,7 +101,7 @@ export class PersonService { 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, { minimumFaceCount: machineLearning.facialRecognition.minFaces, withHidden, @@ -283,7 +282,7 @@ export class PersonService { } async handleQueueDetectFaces({ force }: IBaseJob): Promise { - const { machineLearning } = await this.configCore.getConfig({ withCache: false }); + const { machineLearning } = await this.getConfig({ withCache: false }); if (!isFacialRecognitionEnabled(machineLearning)) { return JobStatus.SKIPPED; } @@ -314,7 +313,7 @@ export class PersonService { } async handleDetectFaces({ id }: IEntityJob): Promise { - const { machineLearning } = await this.configCore.getConfig({ withCache: true }); + const { machineLearning } = await this.getConfig({ withCache: true }); if (!isFacialRecognitionEnabled(machineLearning)) { return JobStatus.SKIPPED; } @@ -375,7 +374,7 @@ export class PersonService { } async handleQueueRecognizeFaces({ force, nightly }: INightlyJob): Promise { - const { machineLearning } = await this.configCore.getConfig({ withCache: false }); + const { machineLearning } = await this.getConfig({ withCache: false }); if (!isFacialRecognitionEnabled(machineLearning)) { return JobStatus.SKIPPED; } @@ -425,7 +424,7 @@ export class PersonService { } async handleRecognizeFaces({ id, deferred }: IDeferrableJob): Promise { - const { machineLearning } = await this.configCore.getConfig({ withCache: true }); + const { machineLearning } = await this.getConfig({ withCache: true }); if (!isFacialRecognitionEnabled(machineLearning)) { return JobStatus.SKIPPED; } @@ -519,7 +518,7 @@ export class PersonService { } async handleGeneratePersonThumbnail(data: IEntityJob): Promise { - const { machineLearning, metadata, image } = await this.configCore.getConfig({ withCache: true }); + const { machineLearning, metadata, image } = await this.getConfig({ withCache: true }); if (!isFacialRecognitionEnabled(machineLearning) && !isFaceImportEnabled(metadata)) { return JobStatus.SKIPPED; } diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index c3cc5399c8d73..4227f35ec3a06 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -1,5 +1,4 @@ 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 { AuthDto } from 'src/dtos/auth.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 { ISearchRepository, SearchExploreItem } from 'src/interfaces/search.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { BaseService } from 'src/services/base.service'; import { getMyPartnerIds } from 'src/utils/asset.util'; import { isSmartSearchEnabled } from 'src/utils/misc'; @Injectable() -export class SearchService { - private configCore: SystemConfigCore; - +export class SearchService extends BaseService { constructor( @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, @@ -38,10 +36,10 @@ export class SearchService { @Inject(ISearchRepository) private searchRepository: ISearchRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, + @Inject(ILoggerRepository) logger: ILoggerRepository, ) { + super(systemMetadataRepository, logger); this.logger.setContext(SearchService.name); - this.configCore = SystemConfigCore.create(systemMetadataRepository, logger); } async searchPerson(auth: AuthDto, dto: SearchPeopleDto): Promise { @@ -101,7 +99,7 @@ export class SearchService { } async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise { - const { machineLearning } = await this.configCore.getConfig({ withCache: false }); + const { machineLearning } = await this.getConfig({ withCache: false }); if (!isSmartSearchEnabled(machineLearning)) { throw new BadRequestException('Smart search is not enabled'); } diff --git a/server/src/services/server.service.spec.ts b/server/src/services/server.service.spec.ts index 4e6a8972b008c..e0cd41a27e2d8 100644 --- a/server/src/services/server.service.spec.ts +++ b/server/src/services/server.service.spec.ts @@ -176,9 +176,9 @@ describe(ServerService.name, () => { }); }); - describe('getConfig', () => { + describe('getSystemConfig', () => { it('should respond the server configuration', async () => { - await expect(sut.getConfig()).resolves.toEqual({ + await expect(sut.getSystemConfig()).resolves.toEqual({ loginPageMessage: '', oauthButtonText: 'Login with OAuth', trashDays: 30, diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index 708fe32db52cb..ed1533f6672b9 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -2,7 +2,6 @@ import { BadRequestException, Inject, Injectable, NotFoundException } from '@nes import { getBuildMetadata, getServerLicensePublicKey } from 'src/config'; import { serverVersion } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; -import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnEvent } from 'src/decorators'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { @@ -22,24 +21,24 @@ import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.interface'; +import { BaseService } from 'src/services/base.service'; import { asHumanReadable } from 'src/utils/bytes'; +import { isUsingConfigFile } from 'src/utils/config'; import { mimeTypes } from 'src/utils/mime-types'; import { isDuplicateDetectionEnabled, isFacialRecognitionEnabled, isSmartSearchEnabled } from 'src/utils/misc'; @Injectable() -export class ServerService { - private configCore: SystemConfigCore; - +export class ServerService extends BaseService { constructor( @Inject(IUserRepository) private userRepository: IUserRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, - @Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository, + @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, @Inject(IServerInfoRepository) private serverInfoRepository: IServerInfoRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, + @Inject(ILoggerRepository) logger: ILoggerRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, ) { + super(systemMetadataRepository, logger); this.logger.setContext(ServerService.name); - this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } @OnEvent({ name: 'app.bootstrap' }) @@ -91,7 +90,7 @@ export class ServerService { async getFeatures(): Promise { const { reverseGeocoding, metadata, map, machineLearning, trash, oauth, passwordLogin, notifications } = - await this.configCore.getConfig({ withCache: false }); + await this.getConfig({ withCache: false }); return { smartSearch: isSmartSearchEnabled(machineLearning), @@ -106,18 +105,18 @@ export class ServerService { oauth: oauth.enabled, oauthAutoLaunch: oauth.autoLaunch, passwordLogin: passwordLogin.enabled, - configFile: this.configCore.isUsingConfigFile(), + configFile: isUsingConfigFile(), email: notifications.smtp.enabled, }; } async getTheme() { - const { theme } = await this.configCore.getConfig({ withCache: false }); + const { theme } = await this.getConfig({ withCache: false }); return theme; } - async getConfig(): Promise { - const config = await this.configCore.getConfig({ withCache: false }); + async getSystemConfig(): Promise { + const config = await this.getConfig({ withCache: false }); const isInitialized = await this.userRepository.hasAdmin(); const onboarding = await this.systemMetadataRepository.get(SystemMetadataKey.ADMIN_ONBOARDING); diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index 54c7fdf25bed7..883270f808b7e 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -1,6 +1,5 @@ import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; 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 { AssetIdsDto } from 'src/dtos/asset.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 { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { BaseService } from 'src/services/base.service'; import { checkAccess, requireAccess } from 'src/utils/access'; import { OpenGraphTags } from 'src/utils/misc'; @Injectable() -export class SharedLinkService { - private configCore: SystemConfigCore; - +export class SharedLinkService extends BaseService { constructor( @Inject(IAccessRepository) private access: IAccessRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, + @Inject(ILoggerRepository) logger: ILoggerRepository, @Inject(ISharedLinkRepository) private repository: ISharedLinkRepository, @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, ) { + super(systemMetadataRepository, logger); this.logger.setContext(SharedLinkService.name); - this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } getAll(auth: AuthDto): Promise { @@ -195,7 +193,7 @@ export class SharedLinkService { 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 assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id; const assetCount = sharedLink.assets.length > 0 ? sharedLink.assets.length : sharedLink.album?.assets.length || 0; diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index ef7865d25c4d9..5db4236d58706 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -1,6 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; import { SystemConfig } from 'src/config'; -import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnEvent } from 'src/decorators'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.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 { ISearchRepository } from 'src/interfaces/search.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { BaseService } from 'src/services/base.service'; import { getAssetFiles } from 'src/utils/asset.util'; import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; @Injectable() -export class SmartInfoService { - private configCore: SystemConfigCore; - +export class SmartInfoService extends BaseService { constructor( @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, @@ -33,10 +31,10 @@ export class SmartInfoService { @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, @Inject(ISearchRepository) private repository: ISearchRepository, @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, + @Inject(ILoggerRepository) logger: ILoggerRepository, ) { + super(systemMetadataRepository, logger); this.logger.setContext(SmartInfoService.name); - this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } @OnEvent({ name: 'app.bootstrap' }) @@ -45,7 +43,7 @@ export class SmartInfoService { return; } - const config = await this.configCore.getConfig({ withCache: false }); + const config = await this.getConfig({ withCache: false }); await this.init(config); } @@ -106,7 +104,7 @@ export class SmartInfoService { } async handleQueueEncodeClip({ force }: IBaseJob): Promise { - const { machineLearning } = await this.configCore.getConfig({ withCache: false }); + const { machineLearning } = await this.getConfig({ withCache: false }); if (!isSmartSearchEnabled(machineLearning)) { return JobStatus.SKIPPED; } @@ -131,7 +129,7 @@ export class SmartInfoService { } async handleEncodeClip({ id }: IEntityJob): Promise { - const { machineLearning } = await this.configCore.getConfig({ withCache: true }); + const { machineLearning } = await this.getConfig({ withCache: true }); if (!isSmartSearchEnabled(machineLearning)) { return JobStatus.SKIPPED; } diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index 33b08efc9bc53..6ce79be33f053 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -13,7 +13,6 @@ import { supportedYearTokens, } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; -import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnEvent } from 'src/decorators'; import { AssetEntity } from 'src/entities/asset.entity'; 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 { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { BaseService } from 'src/services/base.service'; import { getLivePhotoMotionFilename } from 'src/utils/file'; import { usePagination } from 'src/utils/pagination'; @@ -45,8 +45,7 @@ interface RenderMetadata { } @Injectable() -export class StorageTemplateService { - private configCore: SystemConfigCore; +export class StorageTemplateService extends BaseService { private storageCore: StorageCore; private _template: { compiled: HandlebarsTemplateDelegate; @@ -71,10 +70,10 @@ export class StorageTemplateService { @Inject(IUserRepository) private userRepository: IUserRepository, @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository, @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, + @Inject(ILoggerRepository) logger: ILoggerRepository, ) { + super(systemMetadataRepository, logger); this.logger.setContext(StorageTemplateService.name); - this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); this.storageCore = StorageCore.create( assetRepository, cryptoRepository, @@ -117,7 +116,7 @@ export class StorageTemplateService { } async handleMigrationSingle({ id }: IEntityJob): Promise { - const config = await this.configCore.getConfig({ withCache: true }); + const config = await this.getConfig({ withCache: true }); const storageTemplateEnabled = config.storageTemplate.enabled; if (!storageTemplateEnabled) { return JobStatus.SKIPPED; @@ -147,7 +146,7 @@ export class StorageTemplateService { async handleMigration(): Promise { 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; if (!enabled) { this.logger.log('Storage template migration disabled, skipping'); diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index ac517bb3ff587..0e45e0b6947f2 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -216,7 +216,7 @@ describe(SystemConfigService.name, () => { it('should return the default config', async () => { systemMock.get.mockResolvedValue({}); - await expect(sut.getConfig()).resolves.toEqual(defaults); + await expect(sut.getSystemConfig()).resolves.toEqual(defaults); }); it('should merge the overrides', async () => { @@ -227,7 +227,7 @@ describe(SystemConfigService.name, () => { 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 () => { @@ -235,7 +235,7 @@ describe(SystemConfigService.name, () => { 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'); }); @@ -245,7 +245,7 @@ describe(SystemConfigService.name, () => { 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(loggerMock.error).toHaveBeenCalledTimes(2); @@ -269,7 +269,7 @@ describe(SystemConfigService.name, () => { `; 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'); }); @@ -278,7 +278,7 @@ describe(SystemConfigService.name, () => { process.env.IMMICH_CONFIG_FILE = 'immich-config.json'; 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'); }); @@ -288,7 +288,7 @@ describe(SystemConfigService.name, () => { const partialConfig = { machineLearning: { url: 'immich_machine_learning' } }; systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig)); - const config = await sut.getConfig(); + const config = await sut.getSystemConfig(); expect(config.machineLearning.url).toEqual('immich_machine_learning'); }); @@ -304,7 +304,7 @@ describe(SystemConfigService.name, () => { const partialConfig = { server: { externalDomain } }; 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'); }); } @@ -316,7 +316,7 @@ describe(SystemConfigService.name, () => { `; systemMock.readFile.mockResolvedValue(partialConfig); - await sut.getConfig(); + await sut.getSystemConfig(); expect(loggerMock.warn).toHaveBeenCalled(); }); @@ -335,10 +335,10 @@ describe(SystemConfigService.name, () => { systemMock.readFile.mockResolvedValue(JSON.stringify(test.config)); if (test.warn) { - await sut.getConfig(); + await sut.getSystemConfig(); expect(loggerMock.warn).toHaveBeenCalled(); } else { - await expect(sut.getConfig()).rejects.toBeInstanceOf(Error); + await expect(sut.getSystemConfig()).rejects.toBeInstanceOf(Error); } }); } @@ -382,7 +382,7 @@ describe(SystemConfigService.name, () => { describe('updateConfig', () => { it('should update the config and emit an event', async () => { systemMock.get.mockResolvedValue(partialConfig); - await expect(sut.updateConfig(updatedConfig)).resolves.toEqual(updatedConfig); + await expect(sut.updateSystemConfig(updatedConfig)).resolves.toEqual(updatedConfig); expect(eventMock.emit).toHaveBeenCalledWith( 'config.update', 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 () => { process.env.IMMICH_CONFIG_FILE = 'immich-config.json'; 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(); }); }); diff --git a/server/src/services/system-config.service.ts b/server/src/services/system-config.service.ts index 100ab6f47c9e9..acf9f542ca46a 100644 --- a/server/src/services/system-config.service.ts +++ b/server/src/services/system-config.service.ts @@ -12,36 +12,35 @@ import { supportedWeekTokens, supportedYearTokens, } from 'src/constants'; -import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnEvent } from 'src/decorators'; import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto'; import { LogLevel } from 'src/enum'; import { ArgOf, IEventRepository } from 'src/interfaces/event.interface'; import { ILoggerRepository } from 'src/interfaces/logger.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'; @Injectable() -export class SystemConfigService { - private core: SystemConfigCore; - +export class SystemConfigService extends BaseService { constructor( - @Inject(ISystemMetadataRepository) repository: ISystemMetadataRepository, + @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, @Inject(IEventRepository) private eventRepository: IEventRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, + @Inject(ILoggerRepository) logger: ILoggerRepository, ) { + super(systemMetadataRepository, logger); this.logger.setContext(SystemConfigService.name); - this.core = SystemConfigCore.create(repository, this.logger); } @OnEvent({ name: 'app.bootstrap', priority: -100 }) 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 }); } - async getConfig(): Promise { - const config = await this.core.getConfig({ withCache: false }); + async getSystemConfig(): Promise { + const config = await this.getConfig({ withCache: false }); return mapConfig(config); } @@ -57,7 +56,7 @@ export class SystemConfigService { this.logger.setLogLevel(level); 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 - this.core.invalidateCache(); + clearConfigCache(); } @OnEvent({ name: 'config.validate' }) @@ -67,12 +66,12 @@ export class SystemConfigService { } } - async updateConfig(dto: SystemConfigDto): Promise { - if (this.core.isUsingConfigFile()) { + async updateSystemConfig(dto: SystemConfigDto): Promise { + if (isUsingConfigFile()) { 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 { 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); } - const newConfig = await this.core.updateConfig(dto); + const newConfig = await this.updateConfig(dto); await this.eventRepository.emit('config.update', { newConfig, oldConfig }); @@ -104,7 +103,7 @@ export class SystemConfigService { } async getCustomCss(): Promise { - const { theme } = await this.core.getConfig({ withCache: false }); + const { theme } = await this.getConfig({ withCache: false }); return theme.customCss; } diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index dca893aa826b4..f770d1d3b65b9 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -3,7 +3,6 @@ import { DateTime } from 'luxon'; import { getClientLicensePublicKey, getServerLicensePublicKey } from 'src/config'; import { SALT_ROUNDS } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; -import { SystemConfigCore } from 'src/cores/system-config.core'; import { AuthDto } from 'src/dtos/auth.dto'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.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 { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface'; +import { BaseService } from 'src/services/base.service'; import { ImmichFileResponse } from 'src/utils/file'; import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences'; @Injectable() -export class UserService { - private configCore: SystemConfigCore; - +export class UserService extends BaseService { constructor( @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @@ -33,10 +31,10 @@ export class UserService { @Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, @Inject(IUserRepository) private userRepository: IUserRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, + @Inject(ILoggerRepository) logger: ILoggerRepository, ) { + super(systemMetadataRepository, logger); this.logger.setContext(UserService.name); - this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } async search(): Promise { @@ -189,7 +187,7 @@ export class UserService { async handleUserDeleteCheck(): Promise { 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( users.flatMap((user) => this.isReadyForDeletion(user, config.user.deleteDelay) @@ -201,7 +199,7 @@ export class UserService { } async handleUserDelete({ id, force }: IEntityJob): Promise { - const config = await this.configCore.getConfig({ withCache: false }); + const config = await this.getConfig({ withCache: false }); const user = await this.userRepository.get(id, { withDeleted: true }); if (!user) { return JobStatus.FAILED; diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index 1d2785356e3ed..0479faaed01f5 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -2,7 +2,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; import semver, { SemVer } from 'semver'; import { isDev, serverVersion } from 'src/constants'; -import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnEvent } from 'src/decorators'; import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; 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 { IServerInfoRepository } from 'src/interfaces/server-info.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { BaseService } from 'src/services/base.service'; const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => { return { @@ -23,18 +23,16 @@ const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): Re }; @Injectable() -export class VersionService { - private configCore: SystemConfigCore; - +export class VersionService extends BaseService { constructor( @Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IServerInfoRepository) private repository: IServerInfoRepository, - @Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, + @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, + @Inject(ILoggerRepository) logger: ILoggerRepository, ) { + super(systemMetadataRepository, logger); this.logger.setContext(VersionService.name); - this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } @OnEvent({ name: 'app.bootstrap' }) @@ -58,7 +56,7 @@ export class VersionService { return JobStatus.SKIPPED; } - const { newVersionCheck } = await this.configCore.getConfig({ withCache: true }); + const { newVersionCheck } = await this.getConfig({ withCache: true }); if (!newVersionCheck.enabled) { return JobStatus.SKIPPED; } diff --git a/server/src/utils/config.ts b/server/src/utils/config.ts new file mode 100644 index 0000000000000..307db173caf80 --- /dev/null +++ b/server/src/utils/config.ts @@ -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; + +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 => { + 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 => { + const { metadataRepo } = repos; + // get the difference between the new config and the default config + const partialConfig: DeepPartial = {}; + 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; +}; diff --git a/server/test/repositories/system-metadata.repository.mock.ts b/server/test/repositories/system-metadata.repository.mock.ts index e44301fb2137c..793dd4c1c0bab 100644 --- a/server/test/repositories/system-metadata.repository.mock.ts +++ b/server/test/repositories/system-metadata.repository.mock.ts @@ -1,12 +1,9 @@ -import { SystemConfigCore } from 'src/cores/system-config.core'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { clearConfigCache } from 'src/utils/config'; import { Mocked, vitest } from 'vitest'; -export const newSystemMetadataRepositoryMock = (reset = true): Mocked => { - if (reset) { - SystemConfigCore.reset(); - } - +export const newSystemMetadataRepositoryMock = (): Mocked => { + clearConfigCache(); return { get: vitest.fn() as any, set: vitest.fn(), From fe33732958b555242acf5efe889ea856bbe51321 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 1 Oct 2024 08:18:13 +0700 Subject: [PATCH 05/22] chore(mobile): update photo_manager 3.5.0 (#13050) --- .../manifest.json | 1 + mobile/lib/repositories/file_media.repository.dart | 8 ++++++-- mobile/pubspec.lock | 4 ++-- mobile/pubspec.yaml | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 mobile/ios/build/XCBuildData/a34f3d77f077776687d3b444cba8f1c4.xcbuilddata/manifest.json diff --git a/mobile/ios/build/XCBuildData/a34f3d77f077776687d3b444cba8f1c4.xcbuilddata/manifest.json b/mobile/ios/build/XCBuildData/a34f3d77f077776687d3b444cba8f1c4.xcbuilddata/manifest.json new file mode 100644 index 0000000000000..7391713b6fb06 --- /dev/null +++ b/mobile/ios/build/XCBuildData/a34f3d77f077776687d3b444cba8f1c4.xcbuilddata/manifest.json @@ -0,0 +1 @@ +{"client":{"name":"basic","version":0,"file-system":"device-agnostic","perform-ownership-analysis":"no"},"targets":{"":[""]},"commands":{"":{"tool":"phony","inputs":[""],"outputs":[""]},"P0:::Gate WorkspaceHeaderMapVFSFilesWritten":{"tool":"phony","inputs":[],"outputs":[""]}}} \ No newline at end of file diff --git a/mobile/lib/repositories/file_media.repository.dart b/mobile/lib/repositories/file_media.repository.dart index e115868ba0d60..5612b378c3c9e 100644 --- a/mobile/lib/repositories/file_media.repository.dart +++ b/mobile/lib/repositories/file_media.repository.dart @@ -16,8 +16,12 @@ class FileMediaRepository implements IFileMediaRepository { required String title, String? relativePath, }) async { - final entity = await PhotoManager.editor - .saveImage(data, title: title, relativePath: relativePath); + final entity = await PhotoManager.editor.saveImage( + data, + filename: title, + title: title, + relativePath: relativePath, + ); return AssetMediaRepository.toAsset(entity); } diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 9dadbd1028a64..8127a2143f141 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1211,10 +1211,10 @@ packages: dependency: "direct main" description: name: photo_manager - sha256: "1e8bbe46a6858870e34c976aafd85378bed221ce31c1201961eba9ad3d94df9f" + sha256: "32a1ce1095aeaaa792a29f28c1f74613aa75109f21c2d4ab85be3ad9964230a4" url: "https://pub.dev" source: hosted - version: "3.2.3" + version: "3.5.0" photo_manager_image_provider: dependency: "direct main" description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 092b0bb75cf1d..51c0005f5cbdb 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: sdk: flutter path_provider_ios: - photo_manager: ^3.2.3 + photo_manager: ^3.5.0 photo_manager_image_provider: ^2.1.1 flutter_hooks: ^0.20.4 hooks_riverpod: ^2.4.9 From d772cc6c6aacfe1cc71a82ef94d12bf6a71c28d7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 08:23:15 +0700 Subject: [PATCH 06/22] chore(deps): update dependency lints to v5 (#13059) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- mobile/immich_lint/pubspec.lock | 6 +++--- mobile/immich_lint/pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mobile/immich_lint/pubspec.lock b/mobile/immich_lint/pubspec.lock index 6b7a4c99c5ae3..e81bad7da2a58 100644 --- a/mobile/immich_lint/pubspec.lock +++ b/mobile/immich_lint/pubspec.lock @@ -186,10 +186,10 @@ packages: dependency: "direct dev" description: name: lints - sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.0.0" logging: dependency: transitive description: @@ -367,4 +367,4 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" + dart: ">=3.5.0 <4.0.0" diff --git a/mobile/immich_lint/pubspec.yaml b/mobile/immich_lint/pubspec.yaml index 78298f451e7ca..9d1a3c26b3ca8 100644 --- a/mobile/immich_lint/pubspec.yaml +++ b/mobile/immich_lint/pubspec.yaml @@ -11,4 +11,4 @@ dependencies: glob: ^2.1.2 dev_dependencies: - lints: ^4.0.0 + lints: ^5.0.0 From 14e6d23eebe1687eab0cf8d079a66ad7d8040656 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 01:26:39 +0000 Subject: [PATCH 07/22] chore(deps): update dependency @types/node to ^20.16.9 (#13069) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- cli/package-lock.json | 10 +++++----- cli/package.json | 2 +- e2e/package-lock.json | 12 ++++++------ e2e/package.json | 2 +- open-api/typescript-sdk/package-lock.json | 8 ++++---- open-api/typescript-sdk/package.json | 2 +- server/package-lock.json | 14 +++++++------- server/package.json | 2 +- 8 files changed, 26 insertions(+), 26 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index e508fe843f60d..2f7e9dda9ef51 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -24,7 +24,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.16.5", + "@types/node": "^20.16.9", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", @@ -59,7 +59,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.16.5", + "@types/node": "^20.16.9", "typescript": "^5.3.3" } }, @@ -1337,9 +1337,9 @@ } }, "node_modules/@types/node": { - "version": "20.16.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", - "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", + "version": "20.16.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz", + "integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/cli/package.json b/cli/package.json index 522a8e593e9e7..cee258bff5e21 100644 --- a/cli/package.json +++ b/cli/package.json @@ -20,7 +20,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.16.5", + "@types/node": "^20.16.9", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", diff --git a/e2e/package-lock.json b/e2e/package-lock.json index e7b463b0b2696..f5ab18e4c854b 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -15,7 +15,7 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^20.16.5", + "@types/node": "^20.16.9", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", @@ -64,7 +64,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.16.5", + "@types/node": "^20.16.9", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", @@ -99,7 +99,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.16.5", + "@types/node": "^20.16.9", "typescript": "^5.3.3" } }, @@ -1569,9 +1569,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.16.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", - "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", + "version": "20.16.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz", + "integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index 7c0025902dd3a..c107732ab3538 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -25,7 +25,7 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^20.16.5", + "@types/node": "^20.16.9", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 72d7a3ec546d8..e977f56834fb3 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -12,7 +12,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.16.5", + "@types/node": "^20.16.9", "typescript": "^5.3.3" } }, @@ -22,9 +22,9 @@ "integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g==" }, "node_modules/@types/node": { - "version": "20.16.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", - "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", + "version": "20.16.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz", + "integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 41bc3a3b16017..17472327f75dd 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -19,7 +19,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.16.5", + "@types/node": "^20.16.9", "typescript": "^5.3.3" }, "repository": { diff --git a/server/package-lock.json b/server/package-lock.json index 646a26b1ee9d0..450b210388427 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -83,7 +83,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^20.16.5", + "@types/node": "^20.16.9", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", "@types/react": "^18.3.4", @@ -5389,9 +5389,9 @@ } }, "node_modules/@types/node": { - "version": "20.16.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", - "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", + "version": "20.16.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz", + "integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==", "dependencies": { "undici-types": "~6.19.2" } @@ -18701,9 +18701,9 @@ } }, "@types/node": { - "version": "20.16.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", - "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", + "version": "20.16.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz", + "integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==", "requires": { "undici-types": "~6.19.2" } diff --git a/server/package.json b/server/package.json index d4816109069e9..2e6238ad54d6c 100644 --- a/server/package.json +++ b/server/package.json @@ -109,7 +109,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^20.16.5", + "@types/node": "^20.16.9", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", "@types/react": "^18.3.4", From f0ad6627a5bba4a29e97d3247f47eeec95880f38 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 21:54:28 -0400 Subject: [PATCH 08/22] fix(deps): update machine-learning (#13070) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- machine-learning/Dockerfile | 4 ++-- machine-learning/poetry.lock | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index d982962fbcdc6..3bfdf7d2e2cc6 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,6 +1,6 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:e456ff58048f52f121025159e68bf16248c4122c8b96fadffd89331df50c9994 AS builder-cpu +FROM python:3.11-bookworm@sha256:3cdce69fd5663ca47c420ec4d4df8e3545519a4030372f7d2064fb1be2279844 AS builder-cpu FROM builder-cpu AS builder-openvino @@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv COPY poetry.lock pyproject.toml ./ RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev -FROM python:3.11-slim-bookworm@sha256:585cf0799407efc267fe1cce318322ec26e015ac1b3d77f2517d50bc3acfc232 AS prod-cpu +FROM python:3.11-slim-bookworm@sha256:5501a4fe605abe24de87c2f3d6cf9fd760354416a0cad0296cf284fddcdca9e2 AS prod-cpu FROM prod-cpu AS prod-openvino diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 5bb1726378050..1f6a378edaed0 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -2,13 +2,13 @@ [[package]] name = "aiocache" -version = "0.12.2" +version = "0.12.3" description = "multi backend asyncio cache" optional = false python-versions = "*" files = [ - {file = "aiocache-0.12.2-py2.py3-none-any.whl", hash = "sha256:9b6fa30634ab0bfc3ecc44928a91ff07c6ea16d27d55469636b296ebc6eb5918"}, - {file = "aiocache-0.12.2.tar.gz", hash = "sha256:b41c9a145b050a5dcbae1599f847db6dd445193b1f3bd172d8e0fe0cb9e96684"}, + {file = "aiocache-0.12.3-py2.py3-none-any.whl", hash = "sha256:889086fc24710f431937b87ad3720a289f7fc31c4fd8b68e9f918b9bacd8270d"}, + {file = "aiocache-0.12.3.tar.gz", hash = "sha256:f528b27bf4d436b497a1d0d1a8f59a542c153ab1e37c3621713cb376d44c4713"}, ] [package.extras] @@ -1237,13 +1237,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "huggingface-hub" -version = "0.25.0" +version = "0.25.1" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" optional = false python-versions = ">=3.8.0" files = [ - {file = "huggingface_hub-0.25.0-py3-none-any.whl", hash = "sha256:e2f357b35d72d5012cfd127108c4e14abcd61ba4ebc90a5a374dc2456cb34e12"}, - {file = "huggingface_hub-0.25.0.tar.gz", hash = "sha256:fb5fbe6c12fcd99d187ec7db95db9110fb1a20505f23040a5449a717c1a0db4d"}, + {file = "huggingface_hub-0.25.1-py3-none-any.whl", hash = "sha256:a5158ded931b3188f54ea9028097312cb0acd50bffaaa2612014c3c526b44972"}, + {file = "huggingface_hub-0.25.1.tar.gz", hash = "sha256:9ff7cb327343211fbd06e2b149b8f362fd1e389454f3f14c6db75a4999ee20ff"}, ] [package.dependencies] @@ -1531,13 +1531,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] [[package]] name = "locust" -version = "2.31.6" +version = "2.31.8" description = "Developer-friendly load testing framework" optional = false python-versions = ">=3.9" files = [ - {file = "locust-2.31.6-py3-none-any.whl", hash = "sha256:004c963c7a588dc15d57d710cdc6a262d85b57936d7fad3c38ac0657aa98fc3b"}, - {file = "locust-2.31.6.tar.gz", hash = "sha256:03b6da0491d6a0b905692d9ac128d9deec403f40dc605c481a90dbab5126318c"}, + {file = "locust-2.31.8-py3-none-any.whl", hash = "sha256:4194e3d4a0472f1206c51532ed527017f3da1a7d1037ca4b2f0735d5dcd2f78f"}, + {file = "locust-2.31.8.tar.gz", hash = "sha256:b240c0d3e1724317d9211e81e99fbe42a3469071ef4d34d2ae6a727776d56377"}, ] [package.dependencies] From 06048b6db92941ed819528f4b9d9fc47a0edfd9a Mon Sep 17 00:00:00 2001 From: martin <74269598+martabal@users.noreply.github.com> Date: Tue, 1 Oct 2024 04:08:25 +0200 Subject: [PATCH 09/22] feat: preload fonts (#13068) --- server/src/constants.ts | 1 - web/src/app.html | 2 ++ web/src/hooks.server.ts | 12 ++++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 web/src/hooks.server.ts diff --git a/server/src/constants.ts b/server/src/constants.ts index e0a4fe8cef306..8115101ca05a7 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -23,7 +23,6 @@ export const ONE_HOUR = Duration.fromObject({ hours: 1 }); export const envName = (process.env.IMMICH_ENV || 'production').toUpperCase(); export const isDev = () => process.env.IMMICH_ENV === 'development'; export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload'; -export const WEB_ROOT = process.env.IMMICH_WEB_ROOT || '/usr/src/app/www'; const HOST_SERVER_PORT = process.env.IMMICH_PORT || '2283'; export const DEFAULT_EXTERNAL_DOMAIN = 'http://localhost:' + HOST_SERVER_PORT; diff --git a/web/src/app.html b/web/src/app.html index ff6a8bf580a89..d76e52c8593f4 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -13,6 +13,8 @@ + + %sveltekit.head%