Merge branch 'main' into main

This commit is contained in:
San
2024-11-05 20:57:34 +08:00
committed by GitHub
82 changed files with 446 additions and 355 deletions

View File

@@ -6,6 +6,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { ClsModule } from 'nestjs-cls';
import { OpenTelemetryModule } from 'nestjs-otel';
import { commands } from 'src/commands';
import { IWorker } from 'src/constants';
import { controllers } from 'src/controllers';
import { entities } from 'src/entities';
import { ImmichWorker } from 'src/enum';
@@ -57,12 +58,9 @@ const imports = [
TypeOrmModule.forFeature(entities),
];
abstract class BaseModule implements OnModuleInit, OnModuleDestroy {
private get worker() {
return this.getWorker();
}
class BaseModule implements OnModuleInit, OnModuleDestroy {
constructor(
@Inject(IWorker) private worker: ImmichWorker,
@Inject(ILoggerRepository) logger: ILoggerRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@@ -71,8 +69,6 @@ abstract class BaseModule implements OnModuleInit, OnModuleDestroy {
logger.setAppName(this.worker);
}
abstract getWorker(): ImmichWorker;
async onModuleInit() {
this.telemetryRepository.setup({ repositories: repositories.map(({ useClass }) => useClass) });
@@ -94,23 +90,15 @@ abstract class BaseModule implements OnModuleInit, OnModuleDestroy {
@Module({
imports: [...imports, ScheduleModule.forRoot()],
controllers: [...controllers],
providers: [...common, ...middleware],
providers: [...common, ...middleware, { provide: IWorker, useValue: ImmichWorker.API }],
})
export class ApiModule extends BaseModule {
getWorker() {
return ImmichWorker.API;
}
}
export class ApiModule extends BaseModule {}
@Module({
imports: [...imports],
providers: [...common, SchedulerRegistry],
providers: [...common, { provide: IWorker, useValue: ImmichWorker.MICROSERVICES }, SchedulerRegistry],
})
export class MicroservicesModule extends BaseModule {
getWorker() {
return ImmichWorker.MICROSERVICES;
}
}
export class MicroservicesModule extends BaseModule {}
@Module({
imports: [...imports],

View File

@@ -3,7 +3,7 @@ import { ImmichWorker } from 'src/enum';
import { ConfigRepository } from 'src/repositories/config.repository';
const main = async () => {
const { workers, port } = new ConfigRepository().getEnv();
const { host, workers, port } = new ConfigRepository().getEnv();
if (!workers.includes(ImmichWorker.API)) {
process.exit();
}
@@ -11,7 +11,7 @@ const main = async () => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 2000);
try {
const response = await fetch(`http://localhost:${port}/api/server/ping`, {
const response = await fetch(`http://${host || 'localhost'}:${port}/api/server/ping`, {
signal: controller.signal,
});

View File

@@ -13,6 +13,8 @@ export const ADDED_IN_PREFIX = 'This property was added in ';
export const SALT_ROUNDS = 10;
export const IWorker = 'IWorker';
const { version } = JSON.parse(readFileSync('./package.json', 'utf8'));
export const serverVersion = new SemVer(version);

View File

@@ -1,9 +1,10 @@
import { BadRequestException, Inject } from '@nestjs/common';
import { BadRequestException, Inject, Optional } from '@nestjs/common';
import sanitize from 'sanitize-filename';
import { SystemConfig } from 'src/config';
import { SALT_ROUNDS } from 'src/constants';
import { IWorker, SALT_ROUNDS } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
import { UserEntity } from 'src/entities/user.entity';
import { ImmichWorker } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IActivityRepository } from 'src/interfaces/activity.interface';
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
@@ -49,6 +50,7 @@ export class BaseService {
protected storageCore: StorageCore;
constructor(
@Inject(IWorker) @Optional() protected worker: ImmichWorker | undefined,
@Inject(ILoggerRepository) protected logger: ILoggerRepository,
@Inject(IAccessRepository) protected accessRepository: IAccessRepository,
@Inject(IActivityRepository) protected activityRepository: IActivityRepository,

View File

@@ -31,11 +31,23 @@ describe(SearchService.name, () => {
describe('getDuplicates', () => {
it('should get duplicates', async () => {
assetMock.getDuplicates.mockResolvedValue([assetStub.hasDupe]);
assetMock.getDuplicates.mockResolvedValue([assetStub.hasDupe, assetStub.hasDupe]);
await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([
{ duplicateId: assetStub.hasDupe.duplicateId, assets: [expect.objectContaining({ id: assetStub.hasDupe.id })] },
{
duplicateId: assetStub.hasDupe.duplicateId,
assets: [
expect.objectContaining({ id: assetStub.hasDupe.id }),
expect.objectContaining({ id: assetStub.hasDupe.id }),
],
},
]);
});
it('should update assets with duplicateId', async () => {
assetMock.getDuplicates.mockResolvedValue([assetStub.hasDupe]);
await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([]);
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.hasDupe.id], { duplicateId: null });
});
});
describe('handleQueueSearchDuplicates', () => {

View File

@@ -16,8 +16,24 @@ import { usePagination } from 'src/utils/pagination';
export class DuplicateService extends BaseService {
async getDuplicates(auth: AuthDto): Promise<DuplicateResponseDto[]> {
const res = await this.assetRepository.getDuplicates({ userIds: [auth.user.id] });
return mapDuplicateResponse(res.map((a) => mapAsset(a, { auth, withStack: true })));
const uniqueAssetIds: string[] = [];
const duplicates = mapDuplicateResponse(res.map((a) => mapAsset(a, { auth, withStack: true }))).filter(
(duplicate) => {
if (duplicate.assets.length === 1) {
uniqueAssetIds.push(duplicate.assets[0].id);
return false;
}
return true;
},
);
if (uniqueAssetIds.length > 0) {
try {
await this.assetRepository.updateAll(uniqueAssetIds, { duplicateId: null });
} catch (error: any) {
this.logger.error(`Failed to remove duplicateId from assets: ${error.message}`);
}
}
return duplicates;
}
@OnJob({ name: JobName.QUEUE_DUPLICATE_DETECTION, queue: QueueName.DUPLICATE_DETECTION })

View File

@@ -18,7 +18,9 @@ describe(JobService.name, () => {
let telemetryMock: Mocked<ITelemetryRepository>;
beforeEach(() => {
({ sut, assetMock, jobMock, loggerMock, telemetryMock } = newTestService(JobService));
({ sut, assetMock, jobMock, loggerMock, telemetryMock } = newTestService(JobService, {
worker: ImmichWorker.MICROSERVICES,
}));
});
it('should work', () => {
@@ -27,7 +29,6 @@ describe(JobService.name, () => {
describe('onConfigUpdate', () => {
it('should update concurrency', () => {
sut.onBootstrap(ImmichWorker.MICROSERVICES);
sut.onConfigUpdate({ oldConfig: defaults, newConfig: defaults });
expect(jobMock.setConcurrency).toHaveBeenCalledTimes(15);

View File

@@ -38,16 +38,9 @@ const asJobItem = (dto: JobCreateDto): JobItem => {
@Injectable()
export class JobService extends BaseService {
private isMicroservices = false;
@OnEvent({ name: 'app.bootstrap' })
onBootstrap(app: ArgOf<'app.bootstrap'>) {
this.isMicroservices = app === ImmichWorker.MICROSERVICES;
}
@OnEvent({ name: 'config.update', server: true })
onConfigUpdate({ newConfig: config }: ArgOf<'config.update'>) {
if (!this.isMicroservices) {
if (this.worker !== ImmichWorker.MICROSERVICES) {
return;
}