refactor(server): feature flags (#9492)

This commit is contained in:
Jason Rasmussen
2024-05-14 15:31:36 -04:00
committed by GitHub
parent 5583635947
commit 0f129cae4a
11 changed files with 59 additions and 132 deletions

View File

@@ -1,7 +1,6 @@
import { BadRequestException } from '@nestjs/common';
import { SystemConfig } from 'src/config';
import { FeatureFlag, SystemConfigCore } from 'src/cores/system-config.core';
import { SystemConfigKey, SystemConfigKeyPaths } from 'src/entities/system-config.entity';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import {
@@ -368,32 +367,5 @@ describe(JobService.name, () => {
expect(jobMock.queueAll).not.toHaveBeenCalled();
});
}
const featureTests: Array<{ queue: QueueName; feature: FeatureFlag; configKey: SystemConfigKeyPaths }> = [
{
queue: QueueName.SMART_SEARCH,
feature: FeatureFlag.SMART_SEARCH,
configKey: SystemConfigKey.MACHINE_LEARNING_CLIP_ENABLED,
},
{
queue: QueueName.FACE_DETECTION,
feature: FeatureFlag.FACIAL_RECOGNITION,
configKey: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_ENABLED,
},
{
queue: QueueName.FACIAL_RECOGNITION,
feature: FeatureFlag.FACIAL_RECOGNITION,
configKey: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_ENABLED,
},
];
for (const { queue, feature, configKey } of featureTests) {
it(`should throw an error if attempting to queue ${queue} when ${feature} is disabled`, async () => {
configMock.load.mockResolvedValue([{ key: configKey, value: false }]);
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await expect(sut.handleCommand(queue, { command: JobCommand.START, force: false })).rejects.toThrow();
});
}
});
});

View File

@@ -1,6 +1,6 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { snakeCase } from 'lodash';
import { FeatureFlag, SystemConfigCore } from 'src/cores/system-config.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { mapAsset } from 'src/dtos/asset-response.dto';
import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from 'src/dtos/job.dto';
import { AssetType } from 'src/entities/asset.entity';
@@ -112,7 +112,6 @@ export class JobService {
}
case QueueName.SMART_SEARCH: {
await this.configCore.requireFeature(FeatureFlag.SMART_SEARCH);
return this.jobRepository.queue({ name: JobName.QUEUE_SMART_SEARCH, data: { force } });
}
@@ -121,7 +120,6 @@ export class JobService {
}
case QueueName.SIDECAR: {
await this.configCore.requireFeature(FeatureFlag.SIDECAR);
return this.jobRepository.queue({ name: JobName.QUEUE_SIDECAR, data: { force } });
}
@@ -130,12 +128,10 @@ export class JobService {
}
case QueueName.FACE_DETECTION: {
await this.configCore.requireFeature(FeatureFlag.FACIAL_RECOGNITION);
return this.jobRepository.queue({ name: JobName.QUEUE_FACE_DETECTION, data: { force } });
}
case QueueName.FACIAL_RECOGNITION: {
await this.configCore.requireFeature(FeatureFlag.FACIAL_RECOGNITION);
return this.jobRepository.queue({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force } });
}

View File

@@ -7,7 +7,7 @@ import { constants } from 'node:fs/promises';
import path from 'node:path';
import { Subscription } from 'rxjs';
import { StorageCore } from 'src/cores/storage.core';
import { FeatureFlag, SystemConfigCore } from 'src/cores/system-config.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity';
import { IAlbumRepository } from 'src/interfaces/album.interface';
@@ -331,7 +331,8 @@ export class MetadataService {
private async applyReverseGeocoding(asset: AssetEntity, exifData: ExifEntityWithoutGeocodeAndTypeOrm) {
const { latitude, longitude } = exifData;
if (!(await this.configCore.hasFeature(FeatureFlag.REVERSE_GEOCODING)) || !longitude || !latitude) {
const { reverseGeocoding } = await this.configCore.getConfig();
if (!reverseGeocoding.enabled || !longitude || !latitude) {
return;
}

View File

@@ -49,6 +49,7 @@ import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'
import { Orientation } from 'src/services/metadata.service';
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
import { mimeTypes } from 'src/utils/mime-types';
import { isFacialRecognitionEnabled } from 'src/utils/misc';
import { usePagination } from 'src/utils/pagination';
import { IsNull } from 'typeorm';
@@ -282,7 +283,7 @@ export class PersonService {
async handleQueueDetectFaces({ force }: IBaseJob): Promise<JobStatus> {
const { machineLearning } = await this.configCore.getConfig();
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
if (!isFacialRecognitionEnabled(machineLearning)) {
return JobStatus.SKIPPED;
}
@@ -313,7 +314,7 @@ export class PersonService {
async handleDetectFaces({ id }: IEntityJob): Promise<JobStatus> {
const { machineLearning } = await this.configCore.getConfig();
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
if (!isFacialRecognitionEnabled(machineLearning)) {
return JobStatus.SKIPPED;
}
@@ -369,7 +370,7 @@ export class PersonService {
async handleQueueRecognizeFaces({ force }: IBaseJob): Promise<JobStatus> {
const { machineLearning } = await this.configCore.getConfig();
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
if (!isFacialRecognitionEnabled(machineLearning)) {
return JobStatus.SKIPPED;
}
@@ -400,7 +401,7 @@ export class PersonService {
async handleRecognizeFaces({ id, deferred }: IDeferrableJob): Promise<JobStatus> {
const { machineLearning } = await this.configCore.getConfig();
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
if (!isFacialRecognitionEnabled(machineLearning)) {
return JobStatus.SKIPPED;
}
@@ -484,7 +485,7 @@ export class PersonService {
async handleGeneratePersonThumbnail(data: IEntityJob): Promise<JobStatus> {
const { machineLearning, image } = await this.configCore.getConfig();
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
if (!isFacialRecognitionEnabled(machineLearning)) {
return JobStatus.SKIPPED;
}

View File

@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import { FeatureFlag, SystemConfigCore } from 'src/cores/system-config.core';
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { PersonResponseDto } from 'src/dtos/person.dto';
@@ -24,6 +24,7 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { ISearchRepository, SearchExploreItem } from 'src/interfaces/search.interface';
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
import { isSmartSearchEnabled } from 'src/utils/misc';
@Injectable()
export class SearchService {
@@ -53,7 +54,6 @@ export class SearchService {
}
async getExploreData(auth: AuthDto): Promise<SearchExploreItem<AssetResponseDto>[]> {
await this.configCore.requireFeature(FeatureFlag.SEARCH);
const options = { maxFields: 12, minAssetsPerField: 5 };
const results = await Promise.all([
this.assetRepository.getAssetIdByCity(auth.user.id, options),
@@ -98,8 +98,11 @@ export class SearchService {
}
async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise<SearchResponseDto> {
await this.configCore.requireFeature(FeatureFlag.SMART_SEARCH);
const { machineLearning } = await this.configCore.getConfig();
if (!isSmartSearchEnabled(machineLearning)) {
throw new BadRequestException('Smart search is not enabled');
}
const userIds = await this.getUserIdsToSearch(auth);
const embedding = await this.machineLearning.encodeText(

View File

@@ -23,6 +23,7 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf
import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.interface';
import { asHumanReadable } from 'src/utils/bytes';
import { mimeTypes } from 'src/utils/mime-types';
import { isFacialRecognitionEnabled, isSmartSearchEnabled } from 'src/utils/misc';
import { Version } from 'src/utils/version';
@Injectable()
@@ -83,7 +84,23 @@ export class ServerInfoService {
}
async getFeatures(): Promise<ServerFeaturesDto> {
return this.configCore.getFeatures();
const { reverseGeocoding, map, machineLearning, trash, oauth, passwordLogin, notifications } =
await this.configCore.getConfig();
return {
smartSearch: isSmartSearchEnabled(machineLearning),
facialRecognition: isFacialRecognitionEnabled(machineLearning),
map: map.enabled,
reverseGeocoding: reverseGeocoding.enabled,
sidecar: true,
search: true,
trash: trash.enabled,
oauth: oauth.enabled,
oauthAutoLaunch: oauth.autoLaunch,
passwordLogin: passwordLogin.enabled,
configFile: this.configCore.isUsingConfigFile(),
email: notifications.smtp.enabled,
};
}
async getTheme() {

View File

@@ -15,6 +15,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
import { ISearchRepository } from 'src/interfaces/search.interface';
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
import { isSmartSearchEnabled } from 'src/utils/misc';
import { usePagination } from 'src/utils/pagination';
@Injectable()
@@ -50,7 +51,7 @@ export class SmartInfoService {
async handleQueueEncodeClip({ force }: IBaseJob): Promise<JobStatus> {
const { machineLearning } = await this.configCore.getConfig();
if (!machineLearning.enabled || !machineLearning.clip.enabled) {
if (!isSmartSearchEnabled(machineLearning)) {
return JobStatus.SKIPPED;
}
@@ -75,7 +76,7 @@ export class SmartInfoService {
async handleEncodeClip({ id }: IEntityJob): Promise<JobStatus> {
const { machineLearning } = await this.configCore.getConfig();
if (!machineLearning.enabled || !machineLearning.clip.enabled) {
if (!isSmartSearchEnabled(machineLearning)) {
return JobStatus.SKIPPED;
}

View File

@@ -67,6 +67,10 @@ export class SystemConfigService {
}
async updateConfig(dto: SystemConfigDto): Promise<SystemConfigDto> {
if (this.core.isUsingConfigFile()) {
throw new BadRequestException('Cannot update configuration while IMMICH_CONFIG_FILE is in use');
}
const oldConfig = await this.core.getConfig();
try {