You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	refactor(server): system config (#1353)
* refactor(server): system config * fix: jest circular import * chore: ignore migrations in coverage report * chore: tests * chore: tests * chore: todo note * chore: remove vite config backup * chore: fix redis hostname
This commit is contained in:
		| @@ -5,7 +5,7 @@ DB_PASSWORD=postgres | ||||
| DB_DATABASE_NAME=e2e_test | ||||
|  | ||||
| # Redis | ||||
| REDIS_HOSTNAME=immich_redis_test | ||||
| REDIS_HOSTNAME=immich-redis-test | ||||
|  | ||||
| # Upload File Config | ||||
| UPLOAD_LOCATION=./upload | ||||
|   | ||||
| @@ -3,7 +3,6 @@ import { AssetService } from './asset.service'; | ||||
| import { AssetController } from './asset.controller'; | ||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | ||||
| import { AssetEntity } from '@app/infra'; | ||||
| import { BullModule } from '@nestjs/bull'; | ||||
| import { BackgroundTaskModule } from '../../modules/background-task/background-task.module'; | ||||
| import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; | ||||
| import { CommunicationModule } from '../communication/communication.module'; | ||||
| @@ -12,7 +11,6 @@ import { DownloadModule } from '../../modules/download/download.module'; | ||||
| import { TagModule } from '../tag/tag.module'; | ||||
| import { AlbumModule } from '../album/album.module'; | ||||
| import { StorageModule } from '@app/storage'; | ||||
| import { immichSharedQueues } from '@app/job/constants/bull-queue-registration.constant'; | ||||
| import { ShareModule } from '../share/share.module'; | ||||
|  | ||||
| const ASSET_REPOSITORY_PROVIDER = { | ||||
| @@ -29,7 +27,6 @@ const ASSET_REPOSITORY_PROVIDER = { | ||||
|     TagModule, | ||||
|     StorageModule, | ||||
|     forwardRef(() => AlbumModule), | ||||
|     BullModule.registerQueue(...immichSharedQueues), | ||||
|     ShareModule, | ||||
|   ], | ||||
|   controllers: [AssetController], | ||||
|   | ||||
| @@ -9,7 +9,7 @@ import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto'; | ||||
| import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto'; | ||||
| import { DownloadService } from '../../modules/download/download.service'; | ||||
| import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; | ||||
| import { IAssetUploadedJob, IVideoTranscodeJob } from '@app/job'; | ||||
| import { IAssetUploadedJob, IVideoTranscodeJob } from '@app/domain'; | ||||
| import { Queue } from 'bull'; | ||||
| import { IAlbumRepository } from '../album/album-repository'; | ||||
| import { StorageService } from '@app/storage'; | ||||
|   | ||||
| @@ -43,7 +43,7 @@ import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-as | ||||
| import { UpdateAssetDto } from './dto/update-asset.dto'; | ||||
| import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto'; | ||||
| import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; | ||||
| import { IAssetUploadedJob, IVideoTranscodeJob, QueueName, JobName } from '@app/job'; | ||||
| import { IAssetUploadedJob, IVideoTranscodeJob, QueueName, JobName } from '@app/domain'; | ||||
| import { InjectQueue } from '@nestjs/bull'; | ||||
| import { Queue } from 'bull'; | ||||
| import { DownloadService } from '../../modules/download/download.service'; | ||||
|   | ||||
| @@ -1,12 +1,11 @@ | ||||
| import { Module } from '@nestjs/common'; | ||||
| import { ImmichConfigModule } from '@app/immich-config'; | ||||
| import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module'; | ||||
| import { OAuthModule } from '../oauth/oauth.module'; | ||||
| import { AuthController } from './auth.controller'; | ||||
| import { AuthService } from './auth.service'; | ||||
|  | ||||
| @Module({ | ||||
|   imports: [ImmichJwtModule, OAuthModule, ImmichConfigModule], | ||||
|   imports: [ImmichJwtModule, OAuthModule], | ||||
|   controllers: [AuthController], | ||||
|   providers: [AuthService], | ||||
| }) | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import { UserEntity } from '@app/infra'; | ||||
| import { BadRequestException, UnauthorizedException } from '@nestjs/common'; | ||||
| import * as bcrypt from 'bcrypt'; | ||||
| import { SystemConfig } from '@app/infra'; | ||||
| import { ImmichConfigService } from '@app/immich-config'; | ||||
| import { SystemConfigService } from '@app/domain'; | ||||
| import { AuthType } from '../../constants/jwt.constant'; | ||||
| import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; | ||||
| import { OAuthService } from '../oauth/oauth.service'; | ||||
| @@ -50,7 +50,7 @@ describe('AuthService', () => { | ||||
|   let sut: AuthService; | ||||
|   let userRepositoryMock: jest.Mocked<IUserRepository>; | ||||
|   let immichJwtServiceMock: jest.Mocked<ImmichJwtService>; | ||||
|   let immichConfigServiceMock: jest.Mocked<ImmichConfigService>; | ||||
|   let immichConfigServiceMock: jest.Mocked<SystemConfigService>; | ||||
|   let oauthServiceMock: jest.Mocked<OAuthService>; | ||||
|   let compare: jest.Mock; | ||||
|  | ||||
| @@ -89,7 +89,7 @@ describe('AuthService', () => { | ||||
|  | ||||
|     immichConfigServiceMock = { | ||||
|       config$: { subscribe: jest.fn() }, | ||||
|     } as unknown as jest.Mocked<ImmichConfigService>; | ||||
|     } as unknown as jest.Mocked<SystemConfigService>; | ||||
|  | ||||
|     sut = new AuthService( | ||||
|       oauthServiceMock, | ||||
|   | ||||
| @@ -20,7 +20,7 @@ import { LoginResponseDto } from './response-dto/login-response.dto'; | ||||
| import { LogoutResponseDto } from './response-dto/logout-response.dto'; | ||||
| import { OAuthService } from '../oauth/oauth.service'; | ||||
| import { UserCore } from '@app/domain'; | ||||
| import { ImmichConfigService, INITIAL_SYSTEM_CONFIG } from '@app/immich-config'; | ||||
| import { SystemConfigService, INITIAL_SYSTEM_CONFIG } from '@app/domain'; | ||||
| import { SystemConfig } from '@app/infra'; | ||||
|  | ||||
| @Injectable() | ||||
| @@ -32,7 +32,7 @@ export class AuthService { | ||||
|     private oauthService: OAuthService, | ||||
|     private immichJwtService: ImmichJwtService, | ||||
|     @Inject(IUserRepository) userRepository: IUserRepository, | ||||
|     private configService: ImmichConfigService, | ||||
|     private configService: SystemConfigService, | ||||
|     @Inject(INITIAL_SYSTEM_CONFIG) private config: SystemConfig, | ||||
|   ) { | ||||
|     this.userCore = new UserCore(userRepository); | ||||
|   | ||||
| @@ -6,20 +6,10 @@ import { TypeOrmModule } from '@nestjs/typeorm'; | ||||
| import { ExifEntity } from '@app/infra'; | ||||
| import { TagModule } from '../tag/tag.module'; | ||||
| import { AssetModule } from '../asset/asset.module'; | ||||
|  | ||||
| import { StorageModule } from '@app/storage'; | ||||
| import { BullModule } from '@nestjs/bull'; | ||||
| import { immichSharedQueues } from '@app/job/constants/bull-queue-registration.constant'; | ||||
|  | ||||
| @Module({ | ||||
|   imports: [ | ||||
|     TypeOrmModule.forFeature([ExifEntity]), | ||||
|     ImmichJwtModule, | ||||
|     TagModule, | ||||
|     AssetModule, | ||||
|     StorageModule, | ||||
|     BullModule.registerQueue(...immichSharedQueues), | ||||
|   ], | ||||
|   imports: [TypeOrmModule.forFeature([ExifEntity]), ImmichJwtModule, TagModule, AssetModule, StorageModule], | ||||
|   controllers: [JobController], | ||||
|   providers: [JobService], | ||||
| }) | ||||
|   | ||||
| @@ -1,4 +1,11 @@ | ||||
| import { IMetadataExtractionJob, IThumbnailGenerationJob, IVideoTranscodeJob, QueueName, JobName } from '@app/job'; | ||||
| import { | ||||
|   IMachineLearningJob, | ||||
|   IMetadataExtractionJob, | ||||
|   IThumbnailGenerationJob, | ||||
|   IVideoTranscodeJob, | ||||
|   QueueName, | ||||
|   JobName, | ||||
| } from '@app/domain'; | ||||
| import { InjectQueue } from '@nestjs/bull'; | ||||
| import { Queue } from 'bull'; | ||||
| import { BadRequestException, Inject, Injectable } from '@nestjs/common'; | ||||
| @@ -7,7 +14,6 @@ import { IAssetRepository } from '../asset/asset-repository'; | ||||
| import { AssetType } from '@app/infra'; | ||||
| import { GetJobDto, JobId } from './dto/get-job.dto'; | ||||
| import { JobStatusResponseDto } from './response-dto/job-status-response.dto'; | ||||
| import { IMachineLearningJob } from '@app/job/interfaces/machine-learning.interface'; | ||||
| import { StorageService } from '@app/storage'; | ||||
| import { MACHINE_LEARNING_ENABLED } from '@app/common'; | ||||
|  | ||||
|   | ||||
| @@ -1,11 +1,10 @@ | ||||
| import { ImmichConfigModule } from '@app/immich-config'; | ||||
| import { Module } from '@nestjs/common'; | ||||
| import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module'; | ||||
| import { OAuthController } from './oauth.controller'; | ||||
| import { OAuthService } from './oauth.service'; | ||||
|  | ||||
| @Module({ | ||||
|   imports: [ImmichJwtModule, ImmichConfigModule], | ||||
|   imports: [ImmichJwtModule], | ||||
|   controllers: [OAuthController], | ||||
|   providers: [OAuthService], | ||||
|   exports: [OAuthService], | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { SystemConfig, UserEntity } from '@app/infra'; | ||||
| import { ImmichConfigService } from '@app/immich-config'; | ||||
| import { SystemConfigService } from '@app/domain'; | ||||
| import { BadRequestException } from '@nestjs/common'; | ||||
| import { generators, Issuer } from 'openid-client'; | ||||
| import { AuthUserDto } from '../../decorators/auth-user.decorator'; | ||||
| @@ -86,7 +86,7 @@ jest.mock('@nestjs/common', () => ({ | ||||
| describe('OAuthService', () => { | ||||
|   let sut: OAuthService; | ||||
|   let userRepositoryMock: jest.Mocked<IUserRepository>; | ||||
|   let immichConfigServiceMock: jest.Mocked<ImmichConfigService>; | ||||
|   let immichConfigServiceMock: jest.Mocked<SystemConfigService>; | ||||
|   let immichJwtServiceMock: jest.Mocked<ImmichJwtService>; | ||||
|   let callbackMock: jest.Mock; | ||||
|  | ||||
| @@ -132,7 +132,7 @@ describe('OAuthService', () => { | ||||
|  | ||||
|     immichConfigServiceMock = { | ||||
|       config$: { subscribe: jest.fn() }, | ||||
|     } as unknown as jest.Mocked<ImmichConfigService>; | ||||
|     } as unknown as jest.Mocked<SystemConfigService>; | ||||
|  | ||||
|     sut = new OAuthService(immichJwtServiceMock, immichConfigServiceMock, userRepositoryMock, config.disabled); | ||||
|   }); | ||||
|   | ||||
| @@ -1,11 +1,10 @@ | ||||
| import { SystemConfig } from '@app/infra'; | ||||
| import { ImmichConfigService, INITIAL_SYSTEM_CONFIG } from '@app/immich-config'; | ||||
| import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common'; | ||||
| import { ClientMetadata, custom, generators, Issuer, UserinfoResponse } from 'openid-client'; | ||||
| import { AuthUserDto } from '../../decorators/auth-user.decorator'; | ||||
| import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; | ||||
| import { LoginResponseDto } from '../auth/response-dto/login-response.dto'; | ||||
| import { IUserRepository, UserResponseDto, UserCore } from '@app/domain'; | ||||
| import { IUserRepository, UserResponseDto, UserCore, SystemConfigService, INITIAL_SYSTEM_CONFIG } from '@app/domain'; | ||||
| import { OAuthCallbackDto } from './dto/oauth-auth-code.dto'; | ||||
| import { OAuthConfigDto } from './dto/oauth-config.dto'; | ||||
| import { OAuthConfigResponseDto } from './response-dto/oauth-config-response.dto'; | ||||
| @@ -23,7 +22,7 @@ export class OAuthService { | ||||
|  | ||||
|   constructor( | ||||
|     private immichJwtService: ImmichJwtService, | ||||
|     immichConfigService: ImmichConfigService, | ||||
|     configService: SystemConfigService, | ||||
|     @Inject(IUserRepository) userRepository: IUserRepository, | ||||
|     @Inject(INITIAL_SYSTEM_CONFIG) private config: SystemConfig, | ||||
|   ) { | ||||
| @@ -33,7 +32,7 @@ export class OAuthService { | ||||
|       timeout: 30000, | ||||
|     }); | ||||
|  | ||||
|     immichConfigService.config$.subscribe((config) => (this.config = config)); | ||||
|     configService.config$.subscribe((config) => (this.config = config)); | ||||
|   } | ||||
|  | ||||
|   public async generateConfig(dto: OAuthConfigDto): Promise<OAuthConfigResponseDto> { | ||||
|   | ||||
| @@ -1,21 +0,0 @@ | ||||
| import { SystemConfigEntity } from '@app/infra'; | ||||
| import { immichSharedQueues } from '@app/job/constants/bull-queue-registration.constant'; | ||||
| import { BullModule } from '@nestjs/bull'; | ||||
| import { Module } from '@nestjs/common'; | ||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | ||||
| import { ImmichConfigModule } from 'libs/immich-config/src'; | ||||
| import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module'; | ||||
| import { SystemConfigController } from './system-config.controller'; | ||||
| import { SystemConfigService } from './system-config.service'; | ||||
|  | ||||
| @Module({ | ||||
|   imports: [ | ||||
|     ImmichJwtModule, | ||||
|     ImmichConfigModule, | ||||
|     TypeOrmModule.forFeature([SystemConfigEntity]), | ||||
|     BullModule.registerQueue(...immichSharedQueues), | ||||
|   ], | ||||
|   controllers: [SystemConfigController], | ||||
|   providers: [SystemConfigService], | ||||
| }) | ||||
| export class SystemConfigModule {} | ||||
| @@ -1,54 +0,0 @@ | ||||
| import { JobName, QueueName } from '@app/job'; | ||||
| import { | ||||
|   supportedDayTokens, | ||||
|   supportedHourTokens, | ||||
|   supportedMinuteTokens, | ||||
|   supportedMonthTokens, | ||||
|   supportedPresetTokens, | ||||
|   supportedSecondTokens, | ||||
|   supportedYearTokens, | ||||
| } from '@app/storage/constants/supported-datetime-template'; | ||||
| import { InjectQueue } from '@nestjs/bull'; | ||||
| import { Injectable } from '@nestjs/common'; | ||||
| import { Queue } from 'bull'; | ||||
| import { ImmichConfigService } from 'libs/immich-config/src'; | ||||
| import { mapConfig, SystemConfigDto } from './dto/system-config.dto'; | ||||
| import { SystemConfigTemplateStorageOptionDto } from './response-dto/system-config-template-storage-option.dto'; | ||||
|  | ||||
| @Injectable() | ||||
| export class SystemConfigService { | ||||
|   constructor( | ||||
|     private immichConfigService: ImmichConfigService, | ||||
|     @InjectQueue(QueueName.CONFIG) private configQueue: Queue, | ||||
|   ) {} | ||||
|  | ||||
|   public async getConfig(): Promise<SystemConfigDto> { | ||||
|     const config = await this.immichConfigService.getConfig(); | ||||
|     return mapConfig(config); | ||||
|   } | ||||
|  | ||||
|   public getDefaults(): SystemConfigDto { | ||||
|     const config = this.immichConfigService.getDefaults(); | ||||
|     return mapConfig(config); | ||||
|   } | ||||
|  | ||||
|   public async updateConfig(dto: SystemConfigDto): Promise<SystemConfigDto> { | ||||
|     const config = await this.immichConfigService.updateConfig(dto); | ||||
|     this.configQueue.add(JobName.CONFIG_CHANGE, {}); | ||||
|     return mapConfig(config); | ||||
|   } | ||||
|  | ||||
|   public getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto { | ||||
|     const options = new SystemConfigTemplateStorageOptionDto(); | ||||
|  | ||||
|     options.dayOptions = supportedDayTokens; | ||||
|     options.monthOptions = supportedMonthTokens; | ||||
|     options.yearOptions = supportedYearTokens; | ||||
|     options.hourOptions = supportedHourTokens; | ||||
|     options.minuteOptions = supportedMinuteTokens; | ||||
|     options.secondOptions = supportedSecondTokens; | ||||
|     options.presetOptions = supportedPresetTokens; | ||||
|  | ||||
|     return options; | ||||
|   } | ||||
| } | ||||
| @@ -1,10 +1,10 @@ | ||||
| import { Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; | ||||
| import { ApiExcludeEndpoint } from '@nestjs/swagger'; | ||||
| import { ImmichConfigService } from '@app/immich-config'; | ||||
| import { SystemConfigService } from '@app/domain'; | ||||
|  | ||||
| @Controller() | ||||
| export class AppController { | ||||
|   constructor(private configService: ImmichConfigService) {} | ||||
|   constructor(private configService: SystemConfigService) {} | ||||
|  | ||||
|   @ApiExcludeEndpoint() | ||||
|   @Post('refresh-config') | ||||
|   | ||||
| @@ -1,11 +1,10 @@ | ||||
| import { immichAppConfig, immichBullAsyncConfig } from '@app/common/config'; | ||||
| import { immichAppConfig } from '@app/common/config'; | ||||
| import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; | ||||
| import { AssetModule } from './api-v1/asset/asset.module'; | ||||
| import { AuthModule } from './api-v1/auth/auth.module'; | ||||
| import { ImmichJwtModule } from './modules/immich-jwt/immich-jwt.module'; | ||||
| import { DeviceInfoModule } from './api-v1/device-info/device-info.module'; | ||||
| import { ConfigModule } from '@nestjs/config'; | ||||
| import { BullModule } from '@nestjs/bull'; | ||||
| import { ServerInfoModule } from './api-v1/server-info/server-info.module'; | ||||
| import { BackgroundTaskModule } from './modules/background-task/background-task.module'; | ||||
| import { CommunicationModule } from './api-v1/communication/communication.module'; | ||||
| @@ -14,14 +13,12 @@ import { AppController } from './app.controller'; | ||||
| import { ScheduleModule } from '@nestjs/schedule'; | ||||
| import { ScheduleTasksModule } from './modules/schedule-tasks/schedule-tasks.module'; | ||||
| import { JobModule } from './api-v1/job/job.module'; | ||||
| import { SystemConfigModule } from './api-v1/system-config/system-config.module'; | ||||
| import { OAuthModule } from './api-v1/oauth/oauth.module'; | ||||
| import { TagModule } from './api-v1/tag/tag.module'; | ||||
| import { ImmichConfigModule } from '@app/immich-config'; | ||||
| import { ShareModule } from './api-v1/share/share.module'; | ||||
| import { DomainModule } from '@app/domain'; | ||||
| import { InfraModule } from '@app/infra'; | ||||
| import { APIKeyController, UserController } from './controllers'; | ||||
| import { APIKeyController, SystemConfigController, UserController } from './controllers'; | ||||
|  | ||||
| @Module({ | ||||
|   imports: [ | ||||
| @@ -37,12 +34,9 @@ import { APIKeyController, UserController } from './controllers'; | ||||
|     OAuthModule, | ||||
|  | ||||
|     ImmichJwtModule, | ||||
|     ImmichConfigModule, | ||||
|  | ||||
|     DeviceInfoModule, | ||||
|  | ||||
|     BullModule.forRootAsync(immichBullAsyncConfig), | ||||
|  | ||||
|     ServerInfoModule, | ||||
|  | ||||
|     BackgroundTaskModule, | ||||
| @@ -57,8 +51,6 @@ import { APIKeyController, UserController } from './controllers'; | ||||
|  | ||||
|     JobModule, | ||||
|  | ||||
|     SystemConfigModule, | ||||
|  | ||||
|     TagModule, | ||||
|  | ||||
|     ShareModule, | ||||
| @@ -67,6 +59,7 @@ import { APIKeyController, UserController } from './controllers'; | ||||
|     // | ||||
|     AppController, | ||||
|     APIKeyController, | ||||
|     SystemConfigController, | ||||
|     UserController, | ||||
|   ], | ||||
|   providers: [], | ||||
|   | ||||
| @@ -1,2 +1,3 @@ | ||||
| export * from './api-key.controller'; | ||||
| export * from './system-config.controller'; | ||||
| export * from './user.controller'; | ||||
|   | ||||
| @@ -1,9 +1,7 @@ | ||||
| import { SystemConfigDto, SystemConfigService, SystemConfigTemplateStorageOptionDto } from '@app/domain'; | ||||
| import { Body, Controller, Get, Put, ValidationPipe } from '@nestjs/common'; | ||||
| import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; | ||||
| import { Authenticated } from '../../decorators/authenticated.decorator'; | ||||
| import { SystemConfigTemplateStorageOptionDto } from './response-dto/system-config-template-storage-option.dto'; | ||||
| import { SystemConfigDto } from './dto/system-config.dto'; | ||||
| import { SystemConfigService } from './system-config.service'; | ||||
| import { Authenticated } from '../decorators/authenticated.decorator'; | ||||
| 
 | ||||
| @ApiTags('System Config') | ||||
| @ApiBearerAuth() | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { BullModule } from '@nestjs/bull'; | ||||
| import { Module } from '@nestjs/common'; | ||||
| import { QueueName } from '@app/job'; | ||||
| import { QueueName } from '@app/domain'; | ||||
| import { BackgroundTaskProcessor } from './background-task.processor'; | ||||
| import { BackgroundTaskService } from './background-task.service'; | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { assetUtils } from '@app/common/utils'; | ||||
| import { Process, Processor } from '@nestjs/bull'; | ||||
| import { Job } from 'bull'; | ||||
| import { JobName, QueueName } from '@app/job'; | ||||
| import { JobName, QueueName } from '@app/domain'; | ||||
| import { AssetResponseDto } from '../../api-v1/asset/response-dto/asset-response.dto'; | ||||
|  | ||||
| @Processor(QueueName.BACKGROUND_TASK) | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { InjectQueue } from '@nestjs/bull/dist/decorators'; | ||||
| import { Injectable } from '@nestjs/common'; | ||||
| import { Queue } from 'bull'; | ||||
| import { JobName, QueueName } from '@app/job'; | ||||
| import { JobName, QueueName } from '@app/domain'; | ||||
| import { AssetResponseDto } from '../../api-v1/asset/response-dto/asset-response.dto'; | ||||
|  | ||||
| @Injectable() | ||||
|   | ||||
| @@ -1,15 +1,10 @@ | ||||
| import { BullModule } from '@nestjs/bull'; | ||||
| import { Module } from '@nestjs/common'; | ||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | ||||
| import { AssetEntity, ExifEntity, UserEntity } from '@app/infra'; | ||||
| import { ScheduleTasksService } from './schedule-tasks.service'; | ||||
| import { immichSharedQueues } from '@app/job/constants/bull-queue-registration.constant'; | ||||
|  | ||||
| @Module({ | ||||
|   imports: [ | ||||
|     TypeOrmModule.forFeature([AssetEntity, ExifEntity, UserEntity]), | ||||
|     BullModule.registerQueue(...immichSharedQueues), | ||||
|   ], | ||||
|   imports: [TypeOrmModule.forFeature([AssetEntity, ExifEntity, UserEntity])], | ||||
|   providers: [ScheduleTasksService], | ||||
| }) | ||||
| export class ScheduleTasksModule {} | ||||
|   | ||||
| @@ -5,9 +5,9 @@ import { IsNull, Not, Repository } from 'typeorm'; | ||||
| import { AssetEntity, AssetType, ExifEntity, UserEntity } from '@app/infra'; | ||||
| import { InjectQueue } from '@nestjs/bull'; | ||||
| import { Queue } from 'bull'; | ||||
| import { IMetadataExtractionJob, IVideoTranscodeJob, QueueName, JobName } from '@app/job'; | ||||
| import { IMetadataExtractionJob, IVideoTranscodeJob, QueueName, JobName } from '@app/domain'; | ||||
| import { ConfigService } from '@nestjs/config'; | ||||
| import { IUserDeletionJob } from '@app/job/interfaces/user-deletion.interface'; | ||||
| import { IUserDeletionJob } from '@app/domain'; | ||||
| import { userUtils } from '@app/common'; | ||||
|  | ||||
| @Injectable() | ||||
|   | ||||
| @@ -8,8 +8,6 @@ | ||||
|   }, | ||||
|   "moduleNameMapper": { | ||||
|     "^@app/common": "<rootDir>../../../libs/common/src", | ||||
|     "^@app/job(|/.*)$": "<rootDir>../../../libs/job/src/$1", | ||||
|     "^@app/immich-config(|/.*)$": "<rootDir>../../../libs/immich-config/src/$1", | ||||
|     "^@app/storage(|/.*)$": "<rootDir>../../../libs/storage/src/$1", | ||||
|     "^@app/infra(|/.*)$": "<rootDir>../../../libs/infra/src/$1", | ||||
|     "^@app/domain(|/.*)$": "<rootDir>../../../libs/domain/src/$1" | ||||
|   | ||||
| @@ -1,11 +1,9 @@ | ||||
| import { immichAppConfig, immichBullAsyncConfig } from '@app/common/config'; | ||||
| import { immichAppConfig } from '@app/common/config'; | ||||
| import { AssetEntity, ExifEntity, SmartInfoEntity, UserEntity, APIKeyEntity, InfraModule } from '@app/infra'; | ||||
| import { StorageModule } from '@app/storage'; | ||||
| import { BullModule } from '@nestjs/bull'; | ||||
| import { Module } from '@nestjs/common'; | ||||
| import { ConfigModule } from '@nestjs/config'; | ||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | ||||
| import { ImmichConfigModule } from 'libs/immich-config/src'; | ||||
| import { CommunicationModule } from '../../immich/src/api-v1/communication/communication.module'; | ||||
| import { MicroservicesService } from './microservices.service'; | ||||
| import { AssetUploadedProcessor } from './processors/asset-uploaded.processor'; | ||||
| @@ -16,7 +14,6 @@ import { StorageMigrationProcessor } from './processors/storage-migration.proces | ||||
| import { ThumbnailGeneratorProcessor } from './processors/thumbnail.processor'; | ||||
| import { UserDeletionProcessor } from './processors/user-deletion.processor'; | ||||
| import { VideoTranscodeProcessor } from './processors/video-transcode.processor'; | ||||
| import { immichSharedQueues } from '@app/job/constants/bull-queue-registration.constant'; | ||||
| import { DomainModule } from '@app/domain'; | ||||
|  | ||||
| @Module({ | ||||
| @@ -25,11 +22,8 @@ import { DomainModule } from '@app/domain'; | ||||
|     DomainModule.register({ | ||||
|       imports: [InfraModule], | ||||
|     }), | ||||
|     ImmichConfigModule, | ||||
|     TypeOrmModule.forFeature([UserEntity, ExifEntity, AssetEntity, SmartInfoEntity, APIKeyEntity]), | ||||
|     StorageModule, | ||||
|     BullModule.forRootAsync(immichBullAsyncConfig), | ||||
|     BullModule.registerQueue(...immichSharedQueues), | ||||
|     CommunicationModule, | ||||
|   ], | ||||
|   controllers: [], | ||||
| @@ -44,6 +38,5 @@ import { DomainModule } from '@app/domain'; | ||||
|     UserDeletionProcessor, | ||||
|     StorageMigrationProcessor, | ||||
|   ], | ||||
|   exports: [BullModule], | ||||
| }) | ||||
| export class MicroservicesModule {} | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { QueueName } from '@app/job'; | ||||
| import { QueueName } from '@app/domain'; | ||||
| import { InjectQueue } from '@nestjs/bull'; | ||||
| import { Injectable, OnModuleInit } from '@nestjs/common'; | ||||
| import { Queue } from 'bull'; | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import { | ||||
|   IVideoTranscodeJob, | ||||
|   QueueName, | ||||
|   JobName, | ||||
| } from '@app/job'; | ||||
| } from '@app/domain'; | ||||
| import { InjectQueue, Process, Processor } from '@nestjs/bull'; | ||||
| import { Job, Queue } from 'bull'; | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { AssetEntity } from '@app/infra'; | ||||
| import { QueueName } from '@app/job'; | ||||
| import { QueueName } from '@app/domain'; | ||||
| import { Process, Processor } from '@nestjs/bull'; | ||||
| import { Logger } from '@nestjs/common'; | ||||
| import { InjectRepository } from '@nestjs/typeorm'; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { AssetEntity } from '@app/infra'; | ||||
| import { SmartInfoEntity } from '@app/infra'; | ||||
| import { QueueName, JobName } from '@app/job'; | ||||
| import { IMachineLearningJob } from '@app/job/interfaces/machine-learning.interface'; | ||||
| import { QueueName, JobName } from '@app/domain'; | ||||
| import { IMachineLearningJob } from '@app/domain'; | ||||
| import { Process, Processor } from '@nestjs/bull'; | ||||
| import { Logger } from '@nestjs/common'; | ||||
| import { InjectRepository } from '@nestjs/typeorm'; | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import { | ||||
|   IVideoLengthExtractionProcessor, | ||||
|   QueueName, | ||||
|   JobName, | ||||
| } from '@app/job'; | ||||
| } from '@app/domain'; | ||||
| import { Process, Processor } from '@nestjs/bull'; | ||||
| import { Logger } from '@nestjs/common'; | ||||
| import { ConfigService } from '@nestjs/config'; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { APP_UPLOAD_LOCATION } from '@app/common'; | ||||
| import { AssetEntity } from '@app/infra'; | ||||
| import { ImmichConfigService } from '@app/immich-config'; | ||||
| import { QueueName, JobName } from '@app/job'; | ||||
| import { SystemConfigService } from '@app/domain'; | ||||
| import { QueueName, JobName } from '@app/domain'; | ||||
| import { StorageService } from '@app/storage'; | ||||
| import { Process, Processor } from '@nestjs/bull'; | ||||
| import { Logger } from '@nestjs/common'; | ||||
| @@ -14,7 +14,7 @@ export class StorageMigrationProcessor { | ||||
|  | ||||
|   constructor( | ||||
|     private storageService: StorageService, | ||||
|     private immichConfigService: ImmichConfigService, | ||||
|     private systemConfigService: SystemConfigService, | ||||
|  | ||||
|     @InjectRepository(AssetEntity) | ||||
|     private assetRepository: Repository<AssetEntity>, | ||||
| @@ -56,6 +56,6 @@ export class StorageMigrationProcessor { | ||||
|    */ | ||||
|   @Process({ name: JobName.CONFIG_CHANGE, concurrency: 1 }) | ||||
|   async updateTemplate() { | ||||
|     await this.immichConfigService.refreshConfig(); | ||||
|     await this.systemConfigService.refreshConfig(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { APP_UPLOAD_LOCATION } from '@app/common'; | ||||
| import { AssetEntity, AssetType } from '@app/infra'; | ||||
| import { WebpGeneratorProcessor, JpegGeneratorProcessor, QueueName, JobName } from '@app/job'; | ||||
| import { WebpGeneratorProcessor, JpegGeneratorProcessor, QueueName, JobName } from '@app/domain'; | ||||
| import { InjectQueue, Process, Processor } from '@nestjs/bull'; | ||||
| import { Logger } from '@nestjs/common'; | ||||
| import { InjectRepository } from '@nestjs/typeorm'; | ||||
| @@ -13,7 +13,7 @@ import sharp from 'sharp'; | ||||
| import { Repository } from 'typeorm/repository/Repository'; | ||||
| import { join } from 'path'; | ||||
| import { CommunicationGateway } from 'apps/immich/src/api-v1/communication/communication.gateway'; | ||||
| import { IMachineLearningJob } from '@app/job/interfaces/machine-learning.interface'; | ||||
| import { IMachineLearningJob } from '@app/domain'; | ||||
|  | ||||
| @Processor(QueueName.THUMBNAIL_GENERATION) | ||||
| export class ThumbnailGeneratorProcessor { | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { APP_UPLOAD_LOCATION, userUtils } from '@app/common'; | ||||
| import { APIKeyEntity, AssetEntity, UserEntity } from '@app/infra'; | ||||
| import { QueueName, JobName } from '@app/job'; | ||||
| import { IUserDeletionJob } from '@app/job/interfaces/user-deletion.interface'; | ||||
| import { QueueName, JobName } from '@app/domain'; | ||||
| import { IUserDeletionJob } from '@app/domain'; | ||||
| import { Process, Processor } from '@nestjs/bull'; | ||||
| import { Logger } from '@nestjs/common'; | ||||
| import { InjectRepository } from '@nestjs/typeorm'; | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| import { APP_UPLOAD_LOCATION } from '@app/common/constants'; | ||||
| import { AssetEntity } from '@app/infra'; | ||||
| import { QueueName, JobName } from '@app/job'; | ||||
| import { IMp4ConversionProcessor } from '@app/job/interfaces/video-transcode.interface'; | ||||
| import { QueueName, JobName } from '@app/domain'; | ||||
| import { IMp4ConversionProcessor } from '@app/domain'; | ||||
| import { Process, Processor } from '@nestjs/bull'; | ||||
| import { Logger } from '@nestjs/common'; | ||||
| import { InjectRepository } from '@nestjs/typeorm'; | ||||
| import { Job } from 'bull'; | ||||
| import ffmpeg from 'fluent-ffmpeg'; | ||||
| import { existsSync, mkdirSync } from 'fs'; | ||||
| import { ImmichConfigService } from 'libs/immich-config/src'; | ||||
| import { SystemConfigService } from '@app/domain'; | ||||
| import { Repository } from 'typeorm'; | ||||
|  | ||||
| @Processor(QueueName.VIDEO_CONVERSION) | ||||
| @@ -16,7 +16,7 @@ export class VideoTranscodeProcessor { | ||||
|   constructor( | ||||
|     @InjectRepository(AssetEntity) | ||||
|     private assetRepository: Repository<AssetEntity>, | ||||
|     private immichConfigService: ImmichConfigService, | ||||
|     private systemConfigService: SystemConfigService, | ||||
|   ) {} | ||||
|  | ||||
|   @Process({ name: JobName.MP4_CONVERSION, concurrency: 2 }) | ||||
| @@ -41,7 +41,7 @@ export class VideoTranscodeProcessor { | ||||
|   } | ||||
|  | ||||
|   async runFFMPEGPipeLine(asset: AssetEntity, savedEncodedPath: string): Promise<void> { | ||||
|     const config = await this.immichConfigService.getConfig(); | ||||
|     const config = await this.systemConfigService.getConfig(); | ||||
|  | ||||
|     return new Promise((resolve, reject) => { | ||||
|       ffmpeg(asset.originalPath) | ||||
|   | ||||
| @@ -148,6 +148,122 @@ | ||||
|         ] | ||||
|       } | ||||
|     }, | ||||
|     "/system-config": { | ||||
|       "get": { | ||||
|         "operationId": "getConfig", | ||||
|         "description": "", | ||||
|         "parameters": [], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "", | ||||
|             "content": { | ||||
|               "application/json": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/SystemConfigDto" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "tags": [ | ||||
|           "System Config" | ||||
|         ], | ||||
|         "security": [ | ||||
|           { | ||||
|             "bearer": [] | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       "put": { | ||||
|         "operationId": "updateConfig", | ||||
|         "description": "", | ||||
|         "parameters": [], | ||||
|         "requestBody": { | ||||
|           "required": true, | ||||
|           "content": { | ||||
|             "application/json": { | ||||
|               "schema": { | ||||
|                 "$ref": "#/components/schemas/SystemConfigDto" | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "", | ||||
|             "content": { | ||||
|               "application/json": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/SystemConfigDto" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "tags": [ | ||||
|           "System Config" | ||||
|         ], | ||||
|         "security": [ | ||||
|           { | ||||
|             "bearer": [] | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|     }, | ||||
|     "/system-config/defaults": { | ||||
|       "get": { | ||||
|         "operationId": "getDefaults", | ||||
|         "description": "", | ||||
|         "parameters": [], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "", | ||||
|             "content": { | ||||
|               "application/json": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/SystemConfigDto" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "tags": [ | ||||
|           "System Config" | ||||
|         ], | ||||
|         "security": [ | ||||
|           { | ||||
|             "bearer": [] | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|     }, | ||||
|     "/system-config/storage-template-options": { | ||||
|       "get": { | ||||
|         "operationId": "getStorageTemplateOptions", | ||||
|         "description": "", | ||||
|         "parameters": [], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "", | ||||
|             "content": { | ||||
|               "application/json": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/SystemConfigTemplateStorageOptionDto" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "tags": [ | ||||
|           "System Config" | ||||
|         ], | ||||
|         "security": [ | ||||
|           { | ||||
|             "bearer": [] | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|     }, | ||||
|     "/user": { | ||||
|       "get": { | ||||
|         "operationId": "getAllUsers", | ||||
| @@ -2683,122 +2799,6 @@ | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|     }, | ||||
|     "/system-config": { | ||||
|       "get": { | ||||
|         "operationId": "getConfig", | ||||
|         "description": "", | ||||
|         "parameters": [], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "", | ||||
|             "content": { | ||||
|               "application/json": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/SystemConfigDto" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "tags": [ | ||||
|           "System Config" | ||||
|         ], | ||||
|         "security": [ | ||||
|           { | ||||
|             "bearer": [] | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       "put": { | ||||
|         "operationId": "updateConfig", | ||||
|         "description": "", | ||||
|         "parameters": [], | ||||
|         "requestBody": { | ||||
|           "required": true, | ||||
|           "content": { | ||||
|             "application/json": { | ||||
|               "schema": { | ||||
|                 "$ref": "#/components/schemas/SystemConfigDto" | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "", | ||||
|             "content": { | ||||
|               "application/json": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/SystemConfigDto" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "tags": [ | ||||
|           "System Config" | ||||
|         ], | ||||
|         "security": [ | ||||
|           { | ||||
|             "bearer": [] | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|     }, | ||||
|     "/system-config/defaults": { | ||||
|       "get": { | ||||
|         "operationId": "getDefaults", | ||||
|         "description": "", | ||||
|         "parameters": [], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "", | ||||
|             "content": { | ||||
|               "application/json": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/SystemConfigDto" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "tags": [ | ||||
|           "System Config" | ||||
|         ], | ||||
|         "security": [ | ||||
|           { | ||||
|             "bearer": [] | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|     }, | ||||
|     "/system-config/storage-template-options": { | ||||
|       "get": { | ||||
|         "operationId": "getStorageTemplateOptions", | ||||
|         "description": "", | ||||
|         "parameters": [], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "", | ||||
|             "content": { | ||||
|               "application/json": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/SystemConfigTemplateStorageOptionDto" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "tags": [ | ||||
|           "System Config" | ||||
|         ], | ||||
|         "security": [ | ||||
|           { | ||||
|             "bearer": [] | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "info": { | ||||
| @@ -2882,6 +2882,181 @@ | ||||
|           "name" | ||||
|         ] | ||||
|       }, | ||||
|       "SystemConfigFFmpegDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "crf": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "preset": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "targetVideoCodec": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "targetAudioCodec": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "targetScaling": { | ||||
|             "type": "string" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "crf", | ||||
|           "preset", | ||||
|           "targetVideoCodec", | ||||
|           "targetAudioCodec", | ||||
|           "targetScaling" | ||||
|         ] | ||||
|       }, | ||||
|       "SystemConfigOAuthDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "enabled": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "issuerUrl": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "clientId": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "clientSecret": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "scope": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "buttonText": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "autoRegister": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "autoLaunch": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "mobileOverrideEnabled": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "mobileRedirectUri": { | ||||
|             "type": "string" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "enabled", | ||||
|           "issuerUrl", | ||||
|           "clientId", | ||||
|           "clientSecret", | ||||
|           "scope", | ||||
|           "buttonText", | ||||
|           "autoRegister", | ||||
|           "autoLaunch", | ||||
|           "mobileOverrideEnabled", | ||||
|           "mobileRedirectUri" | ||||
|         ] | ||||
|       }, | ||||
|       "SystemConfigPasswordLoginDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "enabled": { | ||||
|             "type": "boolean" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "enabled" | ||||
|         ] | ||||
|       }, | ||||
|       "SystemConfigStorageTemplateDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "template": { | ||||
|             "type": "string" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "template" | ||||
|         ] | ||||
|       }, | ||||
|       "SystemConfigDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "ffmpeg": { | ||||
|             "$ref": "#/components/schemas/SystemConfigFFmpegDto" | ||||
|           }, | ||||
|           "oauth": { | ||||
|             "$ref": "#/components/schemas/SystemConfigOAuthDto" | ||||
|           }, | ||||
|           "passwordLogin": { | ||||
|             "$ref": "#/components/schemas/SystemConfigPasswordLoginDto" | ||||
|           }, | ||||
|           "storageTemplate": { | ||||
|             "$ref": "#/components/schemas/SystemConfigStorageTemplateDto" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "ffmpeg", | ||||
|           "oauth", | ||||
|           "passwordLogin", | ||||
|           "storageTemplate" | ||||
|         ] | ||||
|       }, | ||||
|       "SystemConfigTemplateStorageOptionDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "yearOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           "monthOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           "dayOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           "hourOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           "minuteOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           "secondOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           "presetOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "yearOptions", | ||||
|           "monthOptions", | ||||
|           "dayOptions", | ||||
|           "hourOptions", | ||||
|           "minuteOptions", | ||||
|           "secondOptions", | ||||
|           "presetOptions" | ||||
|         ] | ||||
|       }, | ||||
|       "UserResponseDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
| @@ -4476,181 +4651,6 @@ | ||||
|         "required": [ | ||||
|           "command" | ||||
|         ] | ||||
|       }, | ||||
|       "SystemConfigFFmpegDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "crf": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "preset": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "targetVideoCodec": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "targetAudioCodec": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "targetScaling": { | ||||
|             "type": "string" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "crf", | ||||
|           "preset", | ||||
|           "targetVideoCodec", | ||||
|           "targetAudioCodec", | ||||
|           "targetScaling" | ||||
|         ] | ||||
|       }, | ||||
|       "SystemConfigOAuthDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "enabled": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "issuerUrl": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "clientId": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "clientSecret": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "scope": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "buttonText": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "autoRegister": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "autoLaunch": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "mobileOverrideEnabled": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "mobileRedirectUri": { | ||||
|             "type": "string" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "enabled", | ||||
|           "issuerUrl", | ||||
|           "clientId", | ||||
|           "clientSecret", | ||||
|           "scope", | ||||
|           "buttonText", | ||||
|           "autoRegister", | ||||
|           "autoLaunch", | ||||
|           "mobileOverrideEnabled", | ||||
|           "mobileRedirectUri" | ||||
|         ] | ||||
|       }, | ||||
|       "SystemConfigPasswordLoginDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "enabled": { | ||||
|             "type": "boolean" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "enabled" | ||||
|         ] | ||||
|       }, | ||||
|       "SystemConfigStorageTemplateDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "template": { | ||||
|             "type": "string" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "template" | ||||
|         ] | ||||
|       }, | ||||
|       "SystemConfigDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "ffmpeg": { | ||||
|             "$ref": "#/components/schemas/SystemConfigFFmpegDto" | ||||
|           }, | ||||
|           "oauth": { | ||||
|             "$ref": "#/components/schemas/SystemConfigOAuthDto" | ||||
|           }, | ||||
|           "passwordLogin": { | ||||
|             "$ref": "#/components/schemas/SystemConfigPasswordLoginDto" | ||||
|           }, | ||||
|           "storageTemplate": { | ||||
|             "$ref": "#/components/schemas/SystemConfigStorageTemplateDto" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "ffmpeg", | ||||
|           "oauth", | ||||
|           "passwordLogin", | ||||
|           "storageTemplate" | ||||
|         ] | ||||
|       }, | ||||
|       "SystemConfigTemplateStorageOptionDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "yearOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           "monthOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           "dayOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           "hourOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           "minuteOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           "secondOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           "presetOptions": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "yearOptions", | ||||
|           "monthOptions", | ||||
|           "dayOptions", | ||||
|           "hourOptions", | ||||
|           "minuteOptions", | ||||
|           "secondOptions", | ||||
|           "presetOptions" | ||||
|         ] | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -1,19 +0,0 @@ | ||||
| import { SharedBullAsyncConfiguration } from '@nestjs/bull'; | ||||
|  | ||||
| export const immichBullAsyncConfig: SharedBullAsyncConfiguration = { | ||||
|   useFactory: async () => ({ | ||||
|     prefix: 'immich_bull', | ||||
|     redis: { | ||||
|       host: process.env.REDIS_HOSTNAME || 'immich_redis', | ||||
|       port: parseInt(process.env.REDIS_PORT || '6379'), | ||||
|       db: parseInt(process.env.REDIS_DBINDEX || '0'), | ||||
|       password: process.env.REDIS_PASSWORD || undefined, | ||||
|       path: process.env.REDIS_SOCKET || undefined, | ||||
|     }, | ||||
|     defaultJobOptions: { | ||||
|       attempts: 3, | ||||
|       removeOnComplete: true, | ||||
|       removeOnFail: false, | ||||
|     }, | ||||
|   }), | ||||
| }; | ||||
| @@ -1,2 +1 @@ | ||||
| export * from './app.config'; | ||||
| export * from './bull-queue.config'; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { APIKeyEntity } from '@app/infra'; | ||||
| import { APIKeyEntity } from '@app/infra/db/entities'; | ||||
|  | ||||
| export const IKeyRepository = 'IKeyRepository'; | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { APIKeyEntity } from '@app/infra'; | ||||
| import { APIKeyEntity } from '@app/infra/db/entities'; | ||||
| import { BadRequestException, UnauthorizedException } from '@nestjs/common'; | ||||
| import { authStub, entityStub, newCryptoRepositoryMock, newKeyRepositoryMock } from '../../test'; | ||||
| import { ICryptoRepository } from '../auth'; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { UserEntity } from '@app/infra'; | ||||
| import { UserEntity } from '@app/infra/db/entities'; | ||||
| import { BadRequestException, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; | ||||
| import { AuthUserDto, ICryptoRepository } from '../auth'; | ||||
| import { IKeyRepository } from './api-key.repository'; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { APIKeyEntity } from '@app/infra'; | ||||
| import { APIKeyEntity } from '@app/infra/db/entities'; | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
|  | ||||
| export class APIKeyResponseDto { | ||||
|   | ||||
| @@ -1,11 +1,23 @@ | ||||
| import { DynamicModule, Global, Module, ModuleMetadata, Provider } from '@nestjs/common'; | ||||
| import { APIKeyService } from './api-key'; | ||||
| import { SystemConfigService } from './system-config'; | ||||
| import { UserService } from './user'; | ||||
|  | ||||
| export const INITIAL_SYSTEM_CONFIG = 'INITIAL_SYSTEM_CONFIG'; | ||||
|  | ||||
| const providers: Provider[] = [ | ||||
|   // | ||||
|   APIKeyService, | ||||
|   SystemConfigService, | ||||
|   UserService, | ||||
|  | ||||
|   { | ||||
|     provide: INITIAL_SYSTEM_CONFIG, | ||||
|     inject: [SystemConfigService], | ||||
|     useFactory: async (configService: SystemConfigService) => { | ||||
|       return configService.getConfig(); | ||||
|     }, | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| @Global() | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| export * from './api-key'; | ||||
| export * from './auth'; | ||||
| export * from './domain.module'; | ||||
| export * from './job'; | ||||
| export * from './system-config'; | ||||
| export * from './user'; | ||||
|   | ||||
							
								
								
									
										3
									
								
								server/libs/domain/src/job/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								server/libs/domain/src/job/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| export * from './interfaces'; | ||||
| export * from './job.constants'; | ||||
| export * from './job.repository'; | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { AssetEntity } from '@app/infra'; | ||||
| import { AssetEntity } from '@app/infra/db/entities'; | ||||
| 
 | ||||
| export interface IAssetUploadedJob { | ||||
|   /** | ||||
| @@ -0,0 +1,5 @@ | ||||
| import { AssetEntity } from '@app/infra/db/entities'; | ||||
|  | ||||
| export interface IDeleteFileOnDiskJob { | ||||
|   assets: AssetEntity[]; | ||||
| } | ||||
							
								
								
									
										7
									
								
								server/libs/domain/src/job/interfaces/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								server/libs/domain/src/job/interfaces/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| export * from './asset-uploaded.interface'; | ||||
| export * from './background-task.interface'; | ||||
| export * from './machine-learning.interface'; | ||||
| export * from './metadata-extraction.interface'; | ||||
| export * from './thumbnail-generation.interface'; | ||||
| export * from './user-deletion.interface'; | ||||
| export * from './video-transcode.interface'; | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { AssetEntity } from '@app/infra'; | ||||
| import { AssetEntity } from '@app/infra/db/entities'; | ||||
| 
 | ||||
| export interface IMachineLearningJob { | ||||
|   /** | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { AssetEntity } from '@app/infra'; | ||||
| import { AssetEntity } from '@app/infra/db/entities'; | ||||
| 
 | ||||
| export interface IExifExtractionProcessor { | ||||
|   /** | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { AssetEntity } from '@app/infra'; | ||||
| import { AssetEntity } from '@app/infra/db/entities'; | ||||
| 
 | ||||
| export interface JpegGeneratorProcessor { | ||||
|   /** | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { UserEntity } from '@app/infra'; | ||||
| import { UserEntity } from '@app/infra/db/entities'; | ||||
| 
 | ||||
| export interface IUserDeletionJob { | ||||
|   /** | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { AssetEntity } from '@app/infra'; | ||||
| import { AssetEntity } from '@app/infra/db/entities'; | ||||
| 
 | ||||
| export interface IMp4ConversionProcessor { | ||||
|   /** | ||||
| @@ -1,3 +1,15 @@ | ||||
| export enum QueueName { | ||||
|   THUMBNAIL_GENERATION = 'thumbnail-generation-queue', | ||||
|   METADATA_EXTRACTION = 'metadata-extraction-queue', | ||||
|   VIDEO_CONVERSION = 'video-conversion-queue', | ||||
|   CHECKSUM_GENERATION = 'generate-checksum-queue', | ||||
|   ASSET_UPLOADED = 'asset-uploaded-queue', | ||||
|   MACHINE_LEARNING = 'machine-learning-queue', | ||||
|   USER_DELETION = 'user-deletion-queue', | ||||
|   CONFIG = 'config-queue', | ||||
|   BACKGROUND_TASK = 'background-task', | ||||
| } | ||||
| 
 | ||||
| export enum JobName { | ||||
|   ASSET_UPLOADED = 'asset-uploaded', | ||||
|   MP4_CONVERSION = 'mp4-conversion', | ||||
							
								
								
									
										32
									
								
								server/libs/domain/src/job/job.repository.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								server/libs/domain/src/job/job.repository.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| import { | ||||
|   IAssetUploadedJob, | ||||
|   IDeleteFileOnDiskJob, | ||||
|   IExifExtractionProcessor, | ||||
|   IMachineLearningJob, | ||||
|   IMp4ConversionProcessor, | ||||
|   IReverseGeocodingProcessor, | ||||
|   IUserDeletionJob, | ||||
|   JpegGeneratorProcessor, | ||||
|   WebpGeneratorProcessor, | ||||
| } from './interfaces'; | ||||
| import { JobName } from './job.constants'; | ||||
|  | ||||
| export type JobItem = | ||||
|   | { name: JobName.ASSET_UPLOADED; data: IAssetUploadedJob } | ||||
|   | { name: JobName.MP4_CONVERSION; data: IMp4ConversionProcessor } | ||||
|   | { name: JobName.GENERATE_JPEG_THUMBNAIL; data: JpegGeneratorProcessor } | ||||
|   | { name: JobName.GENERATE_WEBP_THUMBNAIL; data: WebpGeneratorProcessor } | ||||
|   | { name: JobName.EXIF_EXTRACTION; data: IExifExtractionProcessor } | ||||
|   | { name: JobName.REVERSE_GEOCODING; data: IReverseGeocodingProcessor } | ||||
|   | { name: JobName.USER_DELETION; data: IUserDeletionJob } | ||||
|   | { name: JobName.TEMPLATE_MIGRATION } | ||||
|   | { name: JobName.CONFIG_CHANGE } | ||||
|   | { name: JobName.OBJECT_DETECTION; data: IMachineLearningJob } | ||||
|   | { name: JobName.IMAGE_TAGGING; data: IMachineLearningJob } | ||||
|   | { name: JobName.DELETE_FILE_ON_DISK; data: IDeleteFileOnDiskJob }; | ||||
|  | ||||
| export const IJobRepository = 'IJobRepository'; | ||||
|  | ||||
| export interface IJobRepository { | ||||
|   add(item: JobItem): Promise<void>; | ||||
| } | ||||
							
								
								
									
										5
									
								
								server/libs/domain/src/system-config/dto/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								server/libs/domain/src/system-config/dto/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| export * from './system-config-ffmpeg.dto'; | ||||
| export * from './system-config-oauth.dto'; | ||||
| export * from './system-config-password-login.dto'; | ||||
| export * from './system-config-storage-template.dto'; | ||||
| export * from './system-config.dto'; | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { SystemConfig } from '@app/infra'; | ||||
| import { SystemConfig } from '@app/infra/db/entities'; | ||||
| import { ValidateNested } from 'class-validator'; | ||||
| import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto'; | ||||
| import { SystemConfigOAuthDto } from './system-config-oauth.dto'; | ||||
							
								
								
									
										5
									
								
								server/libs/domain/src/system-config/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								server/libs/domain/src/system-config/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| export * from './dto'; | ||||
| export * from './response-dto'; | ||||
| export * from './system-config.repository'; | ||||
| export * from './system-config.service'; | ||||
| export * from './system-config.datetime-variables'; | ||||
| @@ -0,0 +1 @@ | ||||
| export * from './system-config-template-storage-option.dto'; | ||||
| @@ -1,9 +1,9 @@ | ||||
| import { SystemConfig, SystemConfigEntity, SystemConfigKey } from '@app/infra'; | ||||
| import { SystemConfig, SystemConfigEntity, SystemConfigKey } from '@app/infra/db/entities'; | ||||
| import { BadRequestException, Injectable, Logger } from '@nestjs/common'; | ||||
| import { InjectRepository } from '@nestjs/typeorm'; | ||||
| import * as _ from 'lodash'; | ||||
| import { Subject } from 'rxjs'; | ||||
| import { DeepPartial, In, Repository } from 'typeorm'; | ||||
| import { DeepPartial } from 'typeorm'; | ||||
| import { ISystemConfigRepository } from './system-config.repository'; | ||||
| 
 | ||||
| export type SystemConfigValidator = (config: SystemConfig) => void | Promise<void>; | ||||
| 
 | ||||
| @@ -37,16 +37,13 @@ const defaults: SystemConfig = Object.freeze({ | ||||
| }); | ||||
| 
 | ||||
| @Injectable() | ||||
| export class ImmichConfigService { | ||||
|   private logger = new Logger(ImmichConfigService.name); | ||||
| export class SystemConfigCore { | ||||
|   private logger = new Logger(SystemConfigCore.name); | ||||
|   private validators: SystemConfigValidator[] = []; | ||||
| 
 | ||||
|   public config$ = new Subject<SystemConfig>(); | ||||
| 
 | ||||
|   constructor( | ||||
|     @InjectRepository(SystemConfigEntity) | ||||
|     private systemConfigRepository: Repository<SystemConfigEntity>, | ||||
|   ) {} | ||||
|   constructor(private repository: ISystemConfigRepository) {} | ||||
| 
 | ||||
|   public getDefaults(): SystemConfig { | ||||
|     return defaults; | ||||
| @@ -57,7 +54,7 @@ export class ImmichConfigService { | ||||
|   } | ||||
| 
 | ||||
|   public async getConfig() { | ||||
|     const overrides = await this.systemConfigRepository.find(); | ||||
|     const overrides = await this.repository.load(); | ||||
|     const config: DeepPartial<SystemConfig> = {}; | ||||
|     for (const { key, value } of overrides) { | ||||
|       // set via dot notation
 | ||||
| @@ -95,11 +92,11 @@ export class ImmichConfigService { | ||||
|     } | ||||
| 
 | ||||
|     if (updates.length > 0) { | ||||
|       await this.systemConfigRepository.save(updates); | ||||
|       await this.repository.saveAll(updates); | ||||
|     } | ||||
| 
 | ||||
|     if (deletes.length > 0) { | ||||
|       await this.systemConfigRepository.delete({ key: In(deletes.map((item) => item.key)) }); | ||||
|       await this.repository.deleteKeys(deletes.map((item) => item.key)); | ||||
|     } | ||||
| 
 | ||||
|     const newConfig = await this.getConfig(); | ||||
| @@ -0,0 +1,9 @@ | ||||
| import { SystemConfigEntity } from '@app/infra/db/entities'; | ||||
|  | ||||
| export const ISystemConfigRepository = 'ISystemConfigRepository'; | ||||
|  | ||||
| export interface ISystemConfigRepository { | ||||
|   load(): Promise<SystemConfigEntity[]>; | ||||
|   saveAll(items: SystemConfigEntity[]): Promise<SystemConfigEntity[]>; | ||||
|   deleteKeys(keys: string[]): Promise<void>; | ||||
| } | ||||
| @@ -0,0 +1,156 @@ | ||||
| import { SystemConfigEntity, SystemConfigKey } from '@app/infra'; | ||||
| import { BadRequestException } from '@nestjs/common'; | ||||
| import { newJobRepositoryMock, newSystemConfigRepositoryMock, systemConfigStub } from '../../test'; | ||||
| import { IJobRepository, JobName } from '../job'; | ||||
| import { SystemConfigValidator } from './system-config.core'; | ||||
| import { ISystemConfigRepository } from './system-config.repository'; | ||||
| import { SystemConfigService } from './system-config.service'; | ||||
|  | ||||
| const updates: SystemConfigEntity[] = [ | ||||
|   { key: SystemConfigKey.FFMPEG_CRF, value: 'a new value' }, | ||||
|   { key: SystemConfigKey.OAUTH_AUTO_LAUNCH, value: true }, | ||||
| ]; | ||||
|  | ||||
| const updatedConfig = Object.freeze({ | ||||
|   ffmpeg: { | ||||
|     crf: 'a new value', | ||||
|     preset: 'ultrafast', | ||||
|     targetAudioCodec: 'mp3', | ||||
|     targetScaling: '1280:-2', | ||||
|     targetVideoCodec: 'libx264', | ||||
|   }, | ||||
|   oauth: { | ||||
|     autoLaunch: true, | ||||
|     autoRegister: true, | ||||
|     buttonText: 'Login with OAuth', | ||||
|     clientId: '', | ||||
|     clientSecret: '', | ||||
|     enabled: false, | ||||
|     issuerUrl: '', | ||||
|     mobileOverrideEnabled: false, | ||||
|     mobileRedirectUri: '', | ||||
|     scope: 'openid email profile', | ||||
|   }, | ||||
|   passwordLogin: { | ||||
|     enabled: true, | ||||
|   }, | ||||
|   storageTemplate: { | ||||
|     template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| describe(SystemConfigService.name, () => { | ||||
|   let sut: SystemConfigService; | ||||
|   let configMock: jest.Mocked<ISystemConfigRepository>; | ||||
|   let jobMock: jest.Mocked<IJobRepository>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     configMock = newSystemConfigRepositoryMock(); | ||||
|     jobMock = newJobRepositoryMock(); | ||||
|     sut = new SystemConfigService(configMock, jobMock); | ||||
|   }); | ||||
|  | ||||
|   it('should work', () => { | ||||
|     expect(sut).toBeDefined(); | ||||
|   }); | ||||
|  | ||||
|   describe('getDefaults', () => { | ||||
|     it('should return the default config', () => { | ||||
|       configMock.load.mockResolvedValue(updates); | ||||
|  | ||||
|       expect(sut.getDefaults()).toEqual(systemConfigStub.defaults); | ||||
|       expect(configMock.load).not.toHaveBeenCalled(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('addValidator', () => { | ||||
|     it('should call the validator on config changes', async () => { | ||||
|       const validator: SystemConfigValidator = jest.fn(); | ||||
|  | ||||
|       sut.addValidator(validator); | ||||
|  | ||||
|       await sut.updateConfig(systemConfigStub.defaults); | ||||
|  | ||||
|       expect(validator).toHaveBeenCalledWith(systemConfigStub.defaults); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('getConfig', () => { | ||||
|     it('should return the default config', async () => { | ||||
|       configMock.load.mockResolvedValue([]); | ||||
|  | ||||
|       await expect(sut.getConfig()).resolves.toEqual(systemConfigStub.defaults); | ||||
|     }); | ||||
|  | ||||
|     it('should merge the overrides', async () => { | ||||
|       configMock.load.mockResolvedValue([ | ||||
|         { key: SystemConfigKey.FFMPEG_CRF, value: 'a new value' }, | ||||
|         { key: SystemConfigKey.OAUTH_AUTO_LAUNCH, value: true }, | ||||
|       ]); | ||||
|  | ||||
|       await expect(sut.getConfig()).resolves.toEqual(updatedConfig); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('getStorageTemplateOptions', () => { | ||||
|     it('should send back the datetime variables', () => { | ||||
|       expect(sut.getStorageTemplateOptions()).toEqual({ | ||||
|         dayOptions: ['d', 'dd'], | ||||
|         hourOptions: ['h', 'hh', 'H', 'HH'], | ||||
|         minuteOptions: ['m', 'mm'], | ||||
|         monthOptions: ['M', 'MM', 'MMM', 'MMMM'], | ||||
|         presetOptions: [ | ||||
|           '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', | ||||
|           '{{y}}/{{MM}}-{{dd}}/{{filename}}', | ||||
|           '{{y}}/{{MMMM}}-{{dd}}/{{filename}}', | ||||
|           '{{y}}/{{MM}}/{{filename}}', | ||||
|           '{{y}}/{{MMM}}/{{filename}}', | ||||
|           '{{y}}/{{MMMM}}/{{filename}}', | ||||
|           '{{y}}/{{MM}}/{{dd}}/{{filename}}', | ||||
|           '{{y}}/{{MMMM}}/{{dd}}/{{filename}}', | ||||
|           '{{y}}/{{y}}-{{MM}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', | ||||
|           '{{y}}-{{MM}}-{{dd}}/{{filename}}', | ||||
|           '{{y}}-{{MMM}}-{{dd}}/{{filename}}', | ||||
|           '{{y}}-{{MMMM}}-{{dd}}/{{filename}}', | ||||
|         ], | ||||
|         secondOptions: ['s', 'ss'], | ||||
|         yearOptions: ['y', 'yy'], | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('updateConfig', () => { | ||||
|     it('should notify the microservices process', async () => { | ||||
|       configMock.load.mockResolvedValue(updates); | ||||
|  | ||||
|       await expect(sut.updateConfig(updatedConfig)).resolves.toEqual(updatedConfig); | ||||
|  | ||||
|       expect(configMock.saveAll).toHaveBeenCalledWith(updates); | ||||
|       expect(jobMock.add).toHaveBeenCalledWith({ name: JobName.CONFIG_CHANGE }); | ||||
|     }); | ||||
|  | ||||
|     it('should throw an error if the config is not valid', async () => { | ||||
|       const validator = jest.fn().mockRejectedValue('invalid config'); | ||||
|  | ||||
|       sut.addValidator(validator); | ||||
|  | ||||
|       await expect(sut.updateConfig(updatedConfig)).rejects.toBeInstanceOf(BadRequestException); | ||||
|  | ||||
|       expect(validator).toHaveBeenCalledWith(updatedConfig); | ||||
|       expect(configMock.saveAll).not.toHaveBeenCalled(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('refreshConfig', () => { | ||||
|     it('should notify the subscribers', async () => { | ||||
|       const changeMock = jest.fn(); | ||||
|       const subscription = sut.config$.subscribe(changeMock); | ||||
|  | ||||
|       await sut.refreshConfig(); | ||||
|  | ||||
|       expect(changeMock).toHaveBeenCalledWith(systemConfigStub.defaults); | ||||
|  | ||||
|       subscription.unsubscribe(); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,68 @@ | ||||
| import { ISystemConfigRepository } from '../system-config'; | ||||
| import { | ||||
|   supportedDayTokens, | ||||
|   supportedHourTokens, | ||||
|   supportedMinuteTokens, | ||||
|   supportedMonthTokens, | ||||
|   supportedPresetTokens, | ||||
|   supportedSecondTokens, | ||||
|   supportedYearTokens, | ||||
| } from './system-config.datetime-variables'; | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import { IJobRepository, JobName } from '../job'; | ||||
| import { mapConfig, SystemConfigDto } from './dto/system-config.dto'; | ||||
| import { SystemConfigTemplateStorageOptionDto } from './response-dto/system-config-template-storage-option.dto'; | ||||
| import { SystemConfigCore, SystemConfigValidator } from './system-config.core'; | ||||
|  | ||||
| @Injectable() | ||||
| export class SystemConfigService { | ||||
|   private core: SystemConfigCore; | ||||
|   constructor( | ||||
|     @Inject(ISystemConfigRepository) repository: ISystemConfigRepository, | ||||
|     @Inject(IJobRepository) private queue: IJobRepository, | ||||
|   ) { | ||||
|     this.core = new SystemConfigCore(repository); | ||||
|   } | ||||
|  | ||||
|   get config$() { | ||||
|     return this.core.config$; | ||||
|   } | ||||
|  | ||||
|   async getConfig(): Promise<SystemConfigDto> { | ||||
|     const config = await this.core.getConfig(); | ||||
|     return mapConfig(config); | ||||
|   } | ||||
|  | ||||
|   getDefaults(): SystemConfigDto { | ||||
|     const config = this.core.getDefaults(); | ||||
|     return mapConfig(config); | ||||
|   } | ||||
|  | ||||
|   async updateConfig(dto: SystemConfigDto): Promise<SystemConfigDto> { | ||||
|     const config = await this.core.updateConfig(dto); | ||||
|     await this.queue.add({ name: JobName.CONFIG_CHANGE }); | ||||
|     return mapConfig(config); | ||||
|   } | ||||
|  | ||||
|   async refreshConfig() { | ||||
|     await this.core.refreshConfig(); | ||||
|   } | ||||
|  | ||||
|   addValidator(validator: SystemConfigValidator) { | ||||
|     this.core.addValidator(validator); | ||||
|   } | ||||
|  | ||||
|   getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto { | ||||
|     const options = new SystemConfigTemplateStorageOptionDto(); | ||||
|  | ||||
|     options.dayOptions = supportedDayTokens; | ||||
|     options.monthOptions = supportedMonthTokens; | ||||
|     options.yearOptions = supportedYearTokens; | ||||
|     options.hourOptions = supportedHourTokens; | ||||
|     options.secondOptions = supportedSecondTokens; | ||||
|     options.minuteOptions = supportedMinuteTokens; | ||||
|     options.presetOptions = supportedPresetTokens; | ||||
|  | ||||
|     return options; | ||||
|   } | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { UserEntity } from '@app/infra'; | ||||
| import { UserEntity } from '@app/infra/db/entities'; | ||||
|  | ||||
| export class UserResponseDto { | ||||
|   id!: string; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { UserEntity } from '@app/infra'; | ||||
| import { UserEntity } from '@app/infra/db/entities'; | ||||
| import { | ||||
|   BadRequestException, | ||||
|   ForbiddenException, | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { UserEntity } from '@app/infra'; | ||||
| import { UserEntity } from '@app/infra/db/entities'; | ||||
|  | ||||
| export interface UserListFilter { | ||||
|   excludeId?: string; | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { IUserRepository } from '@app/domain'; | ||||
| import { UserEntity } from '@app/infra'; | ||||
| import { IUserRepository } from './user.repository'; | ||||
| import { UserEntity } from '@app/infra/db/entities'; | ||||
| import { BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common'; | ||||
| import { when } from 'jest-when'; | ||||
| import { newUserRepositoryMock } from '../../test'; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { UserEntity } from '@app/infra'; | ||||
| import { SystemConfig, UserEntity } from '@app/infra/db/entities'; | ||||
| import { AuthUserDto } from '../src'; | ||||
|  | ||||
| export const authStub = { | ||||
| @@ -42,3 +42,33 @@ export const entityStub = { | ||||
|     tags: [], | ||||
|   }), | ||||
| }; | ||||
|  | ||||
| export const systemConfigStub = { | ||||
|   defaults: Object.freeze({ | ||||
|     ffmpeg: { | ||||
|       crf: '23', | ||||
|       preset: 'ultrafast', | ||||
|       targetAudioCodec: 'mp3', | ||||
|       targetScaling: '1280:-2', | ||||
|       targetVideoCodec: 'libx264', | ||||
|     }, | ||||
|     oauth: { | ||||
|       autoLaunch: false, | ||||
|       autoRegister: true, | ||||
|       buttonText: 'Login with OAuth', | ||||
|       clientId: '', | ||||
|       clientSecret: '', | ||||
|       enabled: false, | ||||
|       issuerUrl: '', | ||||
|       mobileOverrideEnabled: false, | ||||
|       mobileRedirectUri: '', | ||||
|       scope: 'openid email profile', | ||||
|     }, | ||||
|     passwordLogin: { | ||||
|       enabled: true, | ||||
|     }, | ||||
|     storageTemplate: { | ||||
|       template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', | ||||
|     }, | ||||
|   } as SystemConfig), | ||||
| }; | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| export * from './api-key.repository.mock'; | ||||
| export * from './crypto.repository.mock'; | ||||
| export * from './fixtures'; | ||||
| export * from './job.repository.mock'; | ||||
| export * from './system-config.repository.mock'; | ||||
| export * from './user.repository.mock'; | ||||
|   | ||||
							
								
								
									
										7
									
								
								server/libs/domain/test/job.repository.mock.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								server/libs/domain/test/job.repository.mock.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| import { IJobRepository } from '../src'; | ||||
|  | ||||
| export const newJobRepositoryMock = (): jest.Mocked<IJobRepository> => { | ||||
|   return { | ||||
|     add: jest.fn().mockImplementation(() => Promise.resolve()), | ||||
|   }; | ||||
| }; | ||||
							
								
								
									
										9
									
								
								server/libs/domain/test/system-config.repository.mock.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								server/libs/domain/test/system-config.repository.mock.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| import { ISystemConfigRepository } from '../src'; | ||||
|  | ||||
| export const newSystemConfigRepositoryMock = (): jest.Mocked<ISystemConfigRepository> => { | ||||
|   return { | ||||
|     load: jest.fn().mockResolvedValue([]), | ||||
|     saveAll: jest.fn().mockResolvedValue([]), | ||||
|     deleteKeys: jest.fn(), | ||||
|   }; | ||||
| }; | ||||
| @@ -1,24 +0,0 @@ | ||||
| import { SystemConfigEntity } from '@app/infra'; | ||||
| import { Module, Provider } from '@nestjs/common'; | ||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | ||||
| import { ImmichConfigService } from './immich-config.service'; | ||||
|  | ||||
| export const INITIAL_SYSTEM_CONFIG = 'INITIAL_SYSTEM_CONFIG'; | ||||
|  | ||||
| const providers: Provider[] = [ | ||||
|   ImmichConfigService, | ||||
|   { | ||||
|     provide: INITIAL_SYSTEM_CONFIG, | ||||
|     inject: [ImmichConfigService], | ||||
|     useFactory: async (configService: ImmichConfigService) => { | ||||
|       return configService.getConfig(); | ||||
|     }, | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| @Module({ | ||||
|   imports: [TypeOrmModule.forFeature([SystemConfigEntity])], | ||||
|   providers: [...providers], | ||||
|   exports: [...providers], | ||||
| }) | ||||
| export class ImmichConfigModule {} | ||||
| @@ -1,2 +0,0 @@ | ||||
| export * from './immich-config.module'; | ||||
| export * from './immich-config.service'; | ||||
| @@ -1,9 +0,0 @@ | ||||
| { | ||||
|   "extends": "../../tsconfig.json", | ||||
|   "compilerOptions": { | ||||
|     "declaration": true, | ||||
|     "outDir": "../../dist/libs/immich-config" | ||||
|   }, | ||||
|   "include": ["src/**/*"], | ||||
|   "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] | ||||
| } | ||||
| @@ -0,0 +1,23 @@ | ||||
| import { ISystemConfigRepository } from '@app/domain'; | ||||
| import { InjectRepository } from '@nestjs/typeorm'; | ||||
| import { In, Repository } from 'typeorm'; | ||||
| import { SystemConfigEntity } from '../entities'; | ||||
|  | ||||
| export class SystemConfigRepository implements ISystemConfigRepository { | ||||
|   constructor( | ||||
|     @InjectRepository(SystemConfigEntity) | ||||
|     private repository: Repository<SystemConfigEntity>, | ||||
|   ) {} | ||||
|  | ||||
|   load(): Promise<SystemConfigEntity<string | boolean>[]> { | ||||
|     return this.repository.find(); | ||||
|   } | ||||
|  | ||||
|   saveAll(items: SystemConfigEntity[]): Promise<SystemConfigEntity[]> { | ||||
|     return this.repository.save(items); | ||||
|   } | ||||
|  | ||||
|   async deleteKeys(keys: string[]): Promise<void> { | ||||
|     await this.repository.delete({ key: In(keys) }); | ||||
|   } | ||||
| } | ||||
| @@ -1,26 +1,65 @@ | ||||
| import { ICryptoRepository, IKeyRepository, IUserRepository } from '@app/domain'; | ||||
| import { | ||||
|   ICryptoRepository, | ||||
|   IJobRepository, | ||||
|   IKeyRepository, | ||||
|   ISystemConfigRepository, | ||||
|   IUserRepository, | ||||
|   QueueName, | ||||
| } from '@app/domain'; | ||||
| import { databaseConfig, UserEntity } from '@app/infra'; | ||||
| import { BullModule } from '@nestjs/bull'; | ||||
| import { Global, Module, Provider } from '@nestjs/common'; | ||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | ||||
| import { cryptoRepository } from './auth/crypto.repository'; | ||||
| import { APIKeyEntity, UserRepository } from './db'; | ||||
| import { APIKeyEntity, SystemConfigEntity, UserRepository } from './db'; | ||||
| import { APIKeyRepository } from './db/repository'; | ||||
| import { SystemConfigRepository } from './db/repository/system-config.repository'; | ||||
| import { JobRepository } from './job'; | ||||
|  | ||||
| const providers: Provider[] = [ | ||||
|   // | ||||
|   { provide: ICryptoRepository, useValue: cryptoRepository }, | ||||
|   { provide: IKeyRepository, useClass: APIKeyRepository }, | ||||
|   { provide: IJobRepository, useClass: JobRepository }, | ||||
|   { provide: ISystemConfigRepository, useClass: SystemConfigRepository }, | ||||
|   { provide: IUserRepository, useClass: UserRepository }, | ||||
| ]; | ||||
|  | ||||
| @Global() | ||||
| @Module({ | ||||
|   imports: [ | ||||
|     // | ||||
|     TypeOrmModule.forRoot(databaseConfig), | ||||
|     TypeOrmModule.forFeature([APIKeyEntity, UserEntity]), | ||||
|     TypeOrmModule.forFeature([APIKeyEntity, UserEntity, SystemConfigEntity]), | ||||
|     BullModule.forRootAsync({ | ||||
|       useFactory: async () => ({ | ||||
|         prefix: 'immich_bull', | ||||
|         redis: { | ||||
|           host: process.env.REDIS_HOSTNAME || 'immich_redis', | ||||
|           port: parseInt(process.env.REDIS_PORT || '6379'), | ||||
|           db: parseInt(process.env.REDIS_DBINDEX || '0'), | ||||
|           password: process.env.REDIS_PASSWORD || undefined, | ||||
|           path: process.env.REDIS_SOCKET || undefined, | ||||
|         }, | ||||
|         defaultJobOptions: { | ||||
|           attempts: 3, | ||||
|           removeOnComplete: true, | ||||
|           removeOnFail: false, | ||||
|         }, | ||||
|       }), | ||||
|     }), | ||||
|     BullModule.registerQueue( | ||||
|       { name: QueueName.USER_DELETION }, | ||||
|       { name: QueueName.THUMBNAIL_GENERATION }, | ||||
|       { name: QueueName.ASSET_UPLOADED }, | ||||
|       { name: QueueName.METADATA_EXTRACTION }, | ||||
|       { name: QueueName.VIDEO_CONVERSION }, | ||||
|       { name: QueueName.CHECKSUM_GENERATION }, | ||||
|       { name: QueueName.MACHINE_LEARNING }, | ||||
|       { name: QueueName.CONFIG }, | ||||
|       { name: QueueName.BACKGROUND_TASK }, | ||||
|     ), | ||||
|   ], | ||||
|   providers: [...providers], | ||||
|   exports: [...providers], | ||||
|   exports: [...providers, BullModule], | ||||
| }) | ||||
| export class InfraModule {} | ||||
|   | ||||
							
								
								
									
										1
									
								
								server/libs/infra/src/job/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								server/libs/infra/src/job/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| export * from './job.repository'; | ||||
							
								
								
									
										21
									
								
								server/libs/infra/src/job/job.repository.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								server/libs/infra/src/job/job.repository.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import { IJobRepository, JobItem, JobName, QueueName } from '@app/domain'; | ||||
| import { InjectQueue } from '@nestjs/bull'; | ||||
| import { Logger } from '@nestjs/common'; | ||||
| import { Queue } from 'bull'; | ||||
|  | ||||
| export class JobRepository implements IJobRepository { | ||||
|   private logger = new Logger(JobRepository.name); | ||||
|  | ||||
|   constructor(@InjectQueue(QueueName.CONFIG) private configQueue: Queue) {} | ||||
|  | ||||
|   async add(item: JobItem): Promise<void> { | ||||
|     switch (item.name) { | ||||
|       case JobName.CONFIG_CHANGE: | ||||
|         await this.configQueue.add(JobName.CONFIG_CHANGE, {}); | ||||
|         break; | ||||
|       default: | ||||
|         // TODO inject remaining queues and map job to queue | ||||
|         this.logger.error('Invalid job', item); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,16 +0,0 @@ | ||||
| import { BullModuleOptions } from '@nestjs/bull'; | ||||
| import { QueueName } from './queue-name.constant'; | ||||
|  | ||||
| /** | ||||
|  * Shared queues between apps and microservices | ||||
|  */ | ||||
| export const immichSharedQueues: BullModuleOptions[] = [ | ||||
|   { name: QueueName.USER_DELETION }, | ||||
|   { name: QueueName.THUMBNAIL_GENERATION }, | ||||
|   { name: QueueName.ASSET_UPLOADED }, | ||||
|   { name: QueueName.METADATA_EXTRACTION }, | ||||
|   { name: QueueName.VIDEO_CONVERSION }, | ||||
|   { name: QueueName.CHECKSUM_GENERATION }, | ||||
|   { name: QueueName.MACHINE_LEARNING }, | ||||
|   { name: QueueName.CONFIG }, | ||||
| ]; | ||||
| @@ -1,11 +0,0 @@ | ||||
| export enum QueueName { | ||||
|   THUMBNAIL_GENERATION = 'thumbnail-generation-queue', | ||||
|   METADATA_EXTRACTION = 'metadata-extraction-queue', | ||||
|   VIDEO_CONVERSION = 'video-conversion-queue', | ||||
|   CHECKSUM_GENERATION = 'generate-checksum-queue', | ||||
|   ASSET_UPLOADED = 'asset-uploaded-queue', | ||||
|   MACHINE_LEARNING = 'machine-learning-queue', | ||||
|   USER_DELETION = 'user-deletion-queue', | ||||
|   CONFIG = 'config-queue', | ||||
|   BACKGROUND_TASK = 'background-task', | ||||
| } | ||||
| @@ -1,7 +0,0 @@ | ||||
| export * from './interfaces/asset-uploaded.interface'; | ||||
| export * from './interfaces/metadata-extraction.interface'; | ||||
| export * from './interfaces/video-transcode.interface'; | ||||
| export * from './interfaces/thumbnail-generation.interface'; | ||||
|  | ||||
| export * from './constants/job-name.constant'; | ||||
| export * from './constants/queue-name.constant'; | ||||
| @@ -1,9 +0,0 @@ | ||||
| { | ||||
|   "extends": "../../tsconfig.json", | ||||
|   "compilerOptions": { | ||||
|     "declaration": true, | ||||
|     "outDir": "../../dist/libs/job" | ||||
|   }, | ||||
|   "include": ["src/**/*"], | ||||
|   "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] | ||||
| } | ||||
| @@ -1,6 +0,0 @@ | ||||
| export interface IImmichStorage { | ||||
|   write(): Promise<void>; | ||||
|   read(): Promise<void>; | ||||
| } | ||||
|  | ||||
| export enum IStorageType {} | ||||
| @@ -1,11 +1,10 @@ | ||||
| import { AssetEntity, SystemConfigEntity } from '@app/infra'; | ||||
| import { ImmichConfigModule } from '@app/immich-config'; | ||||
| import { AssetEntity } from '@app/infra'; | ||||
| import { Module } from '@nestjs/common'; | ||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | ||||
| import { StorageService } from './storage.service'; | ||||
|  | ||||
| @Module({ | ||||
|   imports: [TypeOrmModule.forFeature([AssetEntity, SystemConfigEntity]), ImmichConfigModule], | ||||
|   imports: [TypeOrmModule.forFeature([AssetEntity])], | ||||
|   providers: [StorageService], | ||||
|   exports: [StorageService], | ||||
| }) | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { APP_UPLOAD_LOCATION } from '@app/common'; | ||||
| import { AssetEntity, AssetType, SystemConfig } from '@app/infra'; | ||||
| import { ImmichConfigService, INITIAL_SYSTEM_CONFIG } from '@app/immich-config'; | ||||
| import { SystemConfigService, INITIAL_SYSTEM_CONFIG } from '@app/domain'; | ||||
| import { Inject, Injectable, Logger } from '@nestjs/common'; | ||||
| import { InjectRepository } from '@nestjs/typeorm'; | ||||
| import fsPromise from 'fs/promises'; | ||||
| @@ -19,7 +19,7 @@ import { | ||||
|   supportedMonthTokens, | ||||
|   supportedSecondTokens, | ||||
|   supportedYearTokens, | ||||
| } from './constants/supported-datetime-template'; | ||||
| } from '@app/domain'; | ||||
|  | ||||
| const moveFile = promisify<string, string, mv.Options>(mv); | ||||
|  | ||||
| @@ -32,14 +32,14 @@ export class StorageService { | ||||
|   constructor( | ||||
|     @InjectRepository(AssetEntity) | ||||
|     private assetRepository: Repository<AssetEntity>, | ||||
|     private immichConfigService: ImmichConfigService, | ||||
|     private systemConfigService: SystemConfigService, | ||||
|     @Inject(INITIAL_SYSTEM_CONFIG) config: SystemConfig, | ||||
|   ) { | ||||
|     this.storageTemplate = this.compile(config.storageTemplate.template); | ||||
|  | ||||
|     this.immichConfigService.addValidator((config) => this.validateConfig(config)); | ||||
|     this.systemConfigService.addValidator((config) => this.validateConfig(config)); | ||||
|  | ||||
|     this.immichConfigService.config$.subscribe((config) => { | ||||
|     this.systemConfigService.config$.subscribe((config) => { | ||||
|       this.logger.debug(`Received new config, recompiling storage template: ${config.storageTemplate.template}`); | ||||
|       this.storageTemplate = this.compile(config.storageTemplate.template); | ||||
|     }); | ||||
|   | ||||
| @@ -136,7 +136,8 @@ | ||||
|       "^.+\\.(t|j)s$": "ts-jest" | ||||
|     }, | ||||
|     "collectCoverageFrom": [ | ||||
|       "**/*.(t|j)s" | ||||
|       "**/*.(t|j)s", | ||||
|       "!**/migrations/*" | ||||
|     ], | ||||
|     "coverageDirectory": "./coverage", | ||||
|     "coverageThreshold": { | ||||
| @@ -145,10 +146,10 @@ | ||||
|         "statements": 20 | ||||
|       }, | ||||
|       "./libs/domain/": { | ||||
|         "branches": 70, | ||||
|         "branches": 75, | ||||
|         "functions": 85, | ||||
|         "lines": 85, | ||||
|         "statements": 85 | ||||
|         "lines": 90, | ||||
|         "statements": 90 | ||||
|       } | ||||
|     }, | ||||
|     "testEnvironment": "node", | ||||
| @@ -158,9 +159,6 @@ | ||||
|     ], | ||||
|     "moduleNameMapper": { | ||||
|       "@app/common": "<rootDir>/libs/common/src", | ||||
|       "^@app/job(|/.*)$": "<rootDir>/libs/job/src/$1", | ||||
|       "@app/job": "<rootDir>/libs/job/src", | ||||
|       "^@app/immich-config(|/.*)$": "<rootDir>/libs/immich-config/src/$1", | ||||
|       "^@app/storage(|/.*)$": "<rootDir>/libs/storage/src/$1", | ||||
|       "^@app/infra(|/.*)$": "<rootDir>/libs/infra/src/$1", | ||||
|       "^@app/domain(|/.*)$": "<rootDir>/libs/domain/src/$1" | ||||
|   | ||||
| @@ -18,10 +18,6 @@ | ||||
|     "paths": { | ||||
|       "@app/common": ["libs/common/src"], | ||||
|       "@app/common/*": ["libs/common/src/*"], | ||||
|       "@app/job": ["libs/job/src"], | ||||
|       "@app/job/*": ["libs/job/src/*"], | ||||
|       "@app/immich-config": ["libs/immich-config/src"], | ||||
|       "@app/immich-config/*": ["libs/immich-config/src/*"], | ||||
|       "@app/storage": ["libs/storage/src"], | ||||
|       "@app/storage/*": ["libs/storage/src/*"], | ||||
|       "@app/infra": ["libs/infra/src"], | ||||
|   | ||||
		Reference in New Issue
	
	Block a user