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
+1 -1
View File
@@ -25,7 +25,7 @@ COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img
COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl
# web build
FROM node:22.10.0-alpine3.20@sha256:fc95a044b87e95507c60c1f8c829e5d98ddf46401034932499db370c494ef0ff AS web
FROM node:22.11.0-alpine3.20@sha256:f265794478aa0b1a23d85a492c8311ed795bc527c3fe7e43453b3c872dcd71a3 AS web
WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
+29 -29
View File
@@ -83,7 +83,7 @@
"@types/lodash": "^4.14.197",
"@types/mock-fs": "^4.13.1",
"@types/multer": "^1.4.7",
"@types/node": "^22.8.1",
"@types/node": "^22.8.5",
"@types/nodemailer": "^6.4.14",
"@types/picomatch": "^3.0.0",
"@types/pngjs": "^6.0.5",
@@ -5110,9 +5110,9 @@
}
},
"node_modules/@types/node": {
"version": "22.8.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz",
"integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==",
"version": "22.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
"dependencies": {
"undici-types": "~6.19.8"
}
@@ -8236,9 +8236,9 @@
}
},
"node_modules/exiftool-vendored": {
"version": "28.6.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.6.0.tgz",
"integrity": "sha512-Cx8/8ov1tKEacHhsi7FNYdisIhKq/SeQfprYSpYzwBuJwkPmCV8w7tTIvUJRQX9rvopXhBA4eBf1FPXqTZW5vA==",
"version": "28.7.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.7.0.tgz",
"integrity": "sha512-0zoq6kBS1yPjzJs+p0qZDinWEA72PTKoRk5ETYKfmeRcZAkhv83Y3HCpbb/LdgJJywfm8BcIJGezrBHvL7dVnQ==",
"dependencies": {
"@photostructure/tz-lookup": "^11.0.0",
"@types/luxon": "^3.4.2",
@@ -8247,23 +8247,23 @@
"luxon": "^3.5.0"
},
"optionalDependencies": {
"exiftool-vendored.exe": "12.97.0",
"exiftool-vendored.pl": "12.97.0"
"exiftool-vendored.exe": "12.99.0",
"exiftool-vendored.pl": "12.99.0"
}
},
"node_modules/exiftool-vendored.exe": {
"version": "12.97.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.97.0.tgz",
"integrity": "sha512-+HxyFigEJOtwRjP7PhEslhZKuVW2V0hvmHPHtbVtNKGfAUGcfc95xNTjASQfKJvc+9ZuvzdEBPkEQmyA/ZYdIw==",
"version": "12.99.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.99.0.tgz",
"integrity": "sha512-ffpJHCzC9OYJqw4JlPNtCwRy02jwhmnSJEF/QqEjpuIWDEnlRBQP/yWRh1Nw21K1R4FB4yG5PlCgEDu09VQz/w==",
"optional": true,
"os": [
"win32"
]
},
"node_modules/exiftool-vendored.pl": {
"version": "12.97.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.97.0.tgz",
"integrity": "sha512-mXe9JEH3csfyPWcC7+H6IpfaokDMMr4S45n7MtiobGPdeeh+kFnf1SQ9cxg4sF403P6IKVeYYPbzgKMlpro9eQ==",
"version": "12.99.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.99.0.tgz",
"integrity": "sha512-qRVEPQxtoerXF+izJ0O7jGAr5o0Uyvnyu7ao5DTKzF+V7Fv3SurE0l43oCeZPFKo/Ld4V7vEylhFCm4IHVZKWA==",
"optional": true,
"os": [
"!win32"
@@ -18258,9 +18258,9 @@
}
},
"@types/node": {
"version": "22.8.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz",
"integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==",
"version": "22.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
"requires": {
"undici-types": "~6.19.8"
}
@@ -20579,29 +20579,29 @@
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
},
"exiftool-vendored": {
"version": "28.6.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.6.0.tgz",
"integrity": "sha512-Cx8/8ov1tKEacHhsi7FNYdisIhKq/SeQfprYSpYzwBuJwkPmCV8w7tTIvUJRQX9rvopXhBA4eBf1FPXqTZW5vA==",
"version": "28.7.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.7.0.tgz",
"integrity": "sha512-0zoq6kBS1yPjzJs+p0qZDinWEA72PTKoRk5ETYKfmeRcZAkhv83Y3HCpbb/LdgJJywfm8BcIJGezrBHvL7dVnQ==",
"requires": {
"@photostructure/tz-lookup": "^11.0.0",
"@types/luxon": "^3.4.2",
"batch-cluster": "^13.0.0",
"exiftool-vendored.exe": "12.97.0",
"exiftool-vendored.pl": "12.97.0",
"exiftool-vendored.exe": "12.99.0",
"exiftool-vendored.pl": "12.99.0",
"he": "^1.2.0",
"luxon": "^3.5.0"
}
},
"exiftool-vendored.exe": {
"version": "12.97.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.97.0.tgz",
"integrity": "sha512-+HxyFigEJOtwRjP7PhEslhZKuVW2V0hvmHPHtbVtNKGfAUGcfc95xNTjASQfKJvc+9ZuvzdEBPkEQmyA/ZYdIw==",
"version": "12.99.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.99.0.tgz",
"integrity": "sha512-ffpJHCzC9OYJqw4JlPNtCwRy02jwhmnSJEF/QqEjpuIWDEnlRBQP/yWRh1Nw21K1R4FB4yG5PlCgEDu09VQz/w==",
"optional": true
},
"exiftool-vendored.pl": {
"version": "12.97.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.97.0.tgz",
"integrity": "sha512-mXe9JEH3csfyPWcC7+H6IpfaokDMMr4S45n7MtiobGPdeeh+kFnf1SQ9cxg4sF403P6IKVeYYPbzgKMlpro9eQ==",
"version": "12.99.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.99.0.tgz",
"integrity": "sha512-qRVEPQxtoerXF+izJ0O7jGAr5o0Uyvnyu7ao5DTKzF+V7Fv3SurE0l43oCeZPFKo/Ld4V7vEylhFCm4IHVZKWA==",
"optional": true
},
"express": {
+1 -1
View File
@@ -108,7 +108,7 @@
"@types/lodash": "^4.14.197",
"@types/mock-fs": "^4.13.1",
"@types/multer": "^1.4.7",
"@types/node": "^22.8.1",
"@types/node": "^22.8.5",
"@types/nodemailer": "^6.4.14",
"@types/picomatch": "^3.0.0",
"@types/pngjs": "^6.0.5",
+7 -19
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],
+2 -2
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,
});
+2
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);
+4 -2
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,
+14 -2
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', () => {
+18 -2
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 })
+3 -2
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);
+1 -8
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;
}
+9 -4
View File
@@ -1,6 +1,7 @@
import { ChildProcessWithoutNullStreams } from 'node:child_process';
import { Writable } from 'node:stream';
import { PNG } from 'pngjs';
import { ImmichWorker } from 'src/enum';
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
import { BaseService } from 'src/services/base.service';
import { newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
@@ -44,8 +45,9 @@ import { newViewRepositoryMock } from 'test/repositories/view.repository.mock';
import { Readable } from 'typeorm/platform/PlatformTools';
import { Mocked, vitest } from 'vitest';
type RepositoryOverrides = {
metadataRepository: IMetadataRepository;
type Overrides = {
worker?: ImmichWorker;
metadataRepository?: IMetadataRepository;
};
type BaseServiceArgs = ConstructorParameters<typeof BaseService>;
type Constructor<Type, Args extends Array<any>> = {
@@ -54,9 +56,11 @@ type Constructor<Type, Args extends Array<any>> = {
export const newTestService = <T extends BaseService>(
Service: Constructor<T, BaseServiceArgs>,
overrides?: RepositoryOverrides,
overrides?: Overrides,
) => {
const { metadataRepository } = overrides || {};
const { metadataRepository, worker: workerOverride } = overrides || {};
const worker = workerOverride || ImmichWorker.API;
const accessMock = newAccessRepositoryMock();
const loggerMock = newLoggerRepositoryMock();
@@ -98,6 +102,7 @@ export const newTestService = <T extends BaseService>(
const viewMock = newViewRepositoryMock();
const sut = new Service(
worker,
loggerMock,
accessMock,
activityMock,