mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
refactor: split integrity out of maintenance controller/service
This commit is contained in:
@@ -146,6 +146,7 @@ export const endpointTags: Record<ApiTag, string> = {
|
||||
[ApiTag.Duplicates]: 'Endpoints for managing and identifying duplicate assets.',
|
||||
[ApiTag.Faces]:
|
||||
'A face is a detected human face within an asset, which can be associated with a person. Faces are normally detected via machine learning, but can also be created via manually.',
|
||||
[ApiTag.Integrity]: 'Endpoints for viewing and managing integrity reports.',
|
||||
[ApiTag.Jobs]:
|
||||
'Queues and background jobs are used for processing tasks asynchronously. Queues can be paused and resumed as needed.',
|
||||
[ApiTag.Libraries]:
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AuthController } from 'src/controllers/auth.controller';
|
||||
import { DownloadController } from 'src/controllers/download.controller';
|
||||
import { DuplicateController } from 'src/controllers/duplicate.controller';
|
||||
import { FaceController } from 'src/controllers/face.controller';
|
||||
import { IntegrityController } from 'src/controllers/integrity.controller';
|
||||
import { JobController } from 'src/controllers/job.controller';
|
||||
import { LibraryController } from 'src/controllers/library.controller';
|
||||
import { MaintenanceController } from 'src/controllers/maintenance.controller';
|
||||
@@ -49,6 +50,7 @@ export const controllers = [
|
||||
DownloadController,
|
||||
DuplicateController,
|
||||
FaceController,
|
||||
IntegrityController,
|
||||
JobController,
|
||||
LibraryController,
|
||||
MaintenanceController,
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Body, Controller, Delete, Get, Next, Param, Post, Res } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { NextFunction, Response } from 'express';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
MaintenanceGetIntegrityReportDto,
|
||||
MaintenanceIntegrityReportResponseDto,
|
||||
MaintenanceIntegrityReportSummaryResponseDto,
|
||||
} from 'src/dtos/maintenance.dto';
|
||||
import { ApiTag, Permission } from 'src/enum';
|
||||
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { IntegrityService } from 'src/services/integrity.service';
|
||||
import { sendFile } from 'src/utils/file';
|
||||
import { IntegrityReportTypeParamDto, UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags(ApiTag.Maintenance)
|
||||
@Controller('admin/integrity')
|
||||
export class IntegrityController {
|
||||
constructor(
|
||||
private logger: LoggingRepository,
|
||||
private service: IntegrityService,
|
||||
) {}
|
||||
|
||||
@Get('summary')
|
||||
@Endpoint({
|
||||
summary: 'Get integrity report summary',
|
||||
description: '...',
|
||||
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||
})
|
||||
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||
getIntegrityReportSummary(): Promise<MaintenanceIntegrityReportSummaryResponseDto> {
|
||||
return this.service.getIntegrityReportSummary();
|
||||
}
|
||||
|
||||
@Post('report')
|
||||
@Endpoint({
|
||||
summary: 'Get integrity report by type',
|
||||
description: '...',
|
||||
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||
})
|
||||
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||
getIntegrityReport(@Body() dto: MaintenanceGetIntegrityReportDto): Promise<MaintenanceIntegrityReportResponseDto> {
|
||||
return this.service.getIntegrityReport(dto);
|
||||
}
|
||||
|
||||
@Delete('report/:id')
|
||||
@Endpoint({
|
||||
summary: 'Delete report entry and perform corresponding deletion action',
|
||||
description: '...',
|
||||
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||
})
|
||||
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||
async deleteIntegrityReport(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
await this.service.deleteIntegrityReport(auth, id);
|
||||
}
|
||||
|
||||
@Get('report/:type/csv')
|
||||
@Endpoint({
|
||||
summary: 'Export integrity report by type as CSV',
|
||||
description: '...',
|
||||
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||
})
|
||||
@FileResponse()
|
||||
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||
getIntegrityReportCsv(@Param() { type }: IntegrityReportTypeParamDto, @Res() res: Response): void {
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
res.setHeader('Cache-Control', 'private, no-cache, no-transform');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(`${Date.now()}-${type}.csv`)}"`);
|
||||
|
||||
this.service.getIntegrityReportCsv(type).pipe(res);
|
||||
}
|
||||
|
||||
@Get('report/:id/file')
|
||||
@Endpoint({
|
||||
summary: 'Download the orphan/broken file if one exists',
|
||||
description: '...',
|
||||
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||
})
|
||||
@FileResponse()
|
||||
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||
async getIntegrityReportFile(
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Res() res: Response,
|
||||
@Next() next: NextFunction,
|
||||
): Promise<void> {
|
||||
await sendFile(res, next, () => this.service.getIntegrityReportFile(id), this.logger);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,19 @@
|
||||
import { BadRequestException, Body, Controller, Delete, Get, Next, Param, Post, Res } from '@nestjs/common';
|
||||
import { BadRequestException, Body, Controller, Post, Res } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { NextFunction, Response } from 'express';
|
||||
import { Response } from 'express';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
MaintenanceAuthDto,
|
||||
MaintenanceGetIntegrityReportDto,
|
||||
MaintenanceIntegrityReportResponseDto,
|
||||
MaintenanceIntegrityReportSummaryResponseDto,
|
||||
MaintenanceLoginDto,
|
||||
SetMaintenanceModeDto,
|
||||
} from 'src/dtos/maintenance.dto';
|
||||
import { MaintenanceAuthDto, MaintenanceLoginDto, SetMaintenanceModeDto } from 'src/dtos/maintenance.dto';
|
||||
import { ApiTag, ImmichCookie, MaintenanceAction, Permission } from 'src/enum';
|
||||
import { Auth, Authenticated, FileResponse, GetLoginDetails } from 'src/middleware/auth.guard';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
|
||||
import { LoginDetails } from 'src/services/auth.service';
|
||||
import { MaintenanceService } from 'src/services/maintenance.service';
|
||||
import { sendFile } from 'src/utils/file';
|
||||
import { respondWithCookie } from 'src/utils/response';
|
||||
import { IntegrityReportTypeParamDto, UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags(ApiTag.Maintenance)
|
||||
@Controller('admin/maintenance')
|
||||
export class MaintenanceController {
|
||||
constructor(
|
||||
private logger: LoggingRepository,
|
||||
private service: MaintenanceService,
|
||||
) {}
|
||||
constructor(private service: MaintenanceService) {}
|
||||
|
||||
@Post('login')
|
||||
@Endpoint({
|
||||
@@ -59,69 +46,4 @@ export class MaintenanceController {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Get('integrity/summary')
|
||||
@Endpoint({
|
||||
summary: 'Get integrity report summary',
|
||||
description: '...',
|
||||
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||
})
|
||||
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||
getIntegrityReportSummary(): Promise<MaintenanceIntegrityReportSummaryResponseDto> {
|
||||
return this.service.getIntegrityReportSummary();
|
||||
}
|
||||
|
||||
@Post('integrity/report')
|
||||
@Endpoint({
|
||||
summary: 'Get integrity report by type',
|
||||
description: '...',
|
||||
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||
})
|
||||
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||
getIntegrityReport(@Body() dto: MaintenanceGetIntegrityReportDto): Promise<MaintenanceIntegrityReportResponseDto> {
|
||||
return this.service.getIntegrityReport(dto);
|
||||
}
|
||||
|
||||
@Delete('integrity/report/:id')
|
||||
@Endpoint({
|
||||
summary: 'Delete report entry and perform corresponding deletion action',
|
||||
description: '...',
|
||||
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||
})
|
||||
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||
async deleteIntegrityReport(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
await this.service.deleteIntegrityReport(auth, id);
|
||||
}
|
||||
|
||||
@Get('integrity/report/:type/csv')
|
||||
@Endpoint({
|
||||
summary: 'Export integrity report by type as CSV',
|
||||
description: '...',
|
||||
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||
})
|
||||
@FileResponse()
|
||||
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||
getIntegrityReportCsv(@Param() { type }: IntegrityReportTypeParamDto, @Res() res: Response): void {
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
res.setHeader('Cache-Control', 'private, no-cache, no-transform');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(`${Date.now()}-${type}.csv`)}"`);
|
||||
|
||||
this.service.getIntegrityReportCsv(type).pipe(res);
|
||||
}
|
||||
|
||||
@Get('integrity/report/:id/file')
|
||||
@Endpoint({
|
||||
summary: 'Download the orphan/broken file if one exists',
|
||||
description: '...',
|
||||
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||
})
|
||||
@FileResponse()
|
||||
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||
async getIntegrityReportFile(
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Res() res: Response,
|
||||
@Next() next: NextFunction,
|
||||
): Promise<void> {
|
||||
await sendFile(res, next, () => this.service.getIntegrityReportFile(id), this.logger);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,6 +863,7 @@ export enum ApiTag {
|
||||
Download = 'Download',
|
||||
Duplicates = 'Duplicates',
|
||||
Faces = 'Faces',
|
||||
Integrity = 'Integrity (admin)',
|
||||
Jobs = 'Jobs',
|
||||
Libraries = 'Libraries',
|
||||
Maintenance = 'Maintenance (admin)',
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { IntegrityService } from 'src/services/integrity.service';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(IntegrityService.name, () => {
|
||||
let sut: IntegrityService;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, mocks } = newTestService(IntegrityService));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
describe.skip('getIntegrityReportSummary'); // just calls repository
|
||||
describe.skip('getIntegrityReport'); // just calls repository
|
||||
describe.skip('getIntegrityReportCsv'); // just calls repository
|
||||
|
||||
describe.todo('getIntegrityReportFile');
|
||||
describe.todo('deleteIntegrityReport');
|
||||
});
|
||||
@@ -2,13 +2,21 @@ import { Injectable } from '@nestjs/common';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { stat } from 'node:fs/promises';
|
||||
import { Writable } from 'node:stream';
|
||||
import { basename } from 'node:path';
|
||||
import { Readable, Writable } from 'node:stream';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
import { JOBS_LIBRARY_PAGINATION_SIZE } from 'src/constants';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
MaintenanceGetIntegrityReportDto,
|
||||
MaintenanceIntegrityReportResponseDto,
|
||||
MaintenanceIntegrityReportSummaryResponseDto,
|
||||
} from 'src/dtos/maintenance.dto';
|
||||
import {
|
||||
AssetStatus,
|
||||
CacheControl,
|
||||
DatabaseLock,
|
||||
ImmichWorker,
|
||||
IntegrityReportType,
|
||||
@@ -28,6 +36,7 @@ import {
|
||||
IIntegrityPathWithChecksumJob,
|
||||
IIntegrityPathWithReportJob,
|
||||
} from 'src/types';
|
||||
import { ImmichFileResponse } from 'src/utils/file';
|
||||
import { handlePromiseError } from 'src/utils/misc';
|
||||
|
||||
/**
|
||||
@@ -145,6 +154,54 @@ export class IntegrityService extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
getIntegrityReportSummary(): Promise<MaintenanceIntegrityReportSummaryResponseDto> {
|
||||
return this.integrityRepository.getIntegrityReportSummary();
|
||||
}
|
||||
|
||||
async getIntegrityReport(dto: MaintenanceGetIntegrityReportDto): Promise<MaintenanceIntegrityReportResponseDto> {
|
||||
return {
|
||||
items: await this.integrityRepository.getIntegrityReports(dto.type),
|
||||
};
|
||||
}
|
||||
|
||||
getIntegrityReportCsv(type: IntegrityReportType): Readable {
|
||||
return this.integrityRepository.streamIntegrityReportsCSV(type);
|
||||
}
|
||||
|
||||
async getIntegrityReportFile(id: string): Promise<ImmichFileResponse> {
|
||||
const { path } = await this.integrityRepository.getById(id);
|
||||
|
||||
return new ImmichFileResponse({
|
||||
path,
|
||||
fileName: basename(path),
|
||||
contentType: 'application/octet-stream',
|
||||
cacheControl: CacheControl.PrivateWithoutCache,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteIntegrityReport(auth: AuthDto, id: string): Promise<void> {
|
||||
const { path, assetId, fileAssetId } = await this.integrityRepository.getById(id);
|
||||
|
||||
if (assetId) {
|
||||
await this.assetRepository.updateAll([assetId], {
|
||||
deletedAt: new Date(),
|
||||
status: AssetStatus.Trashed,
|
||||
});
|
||||
|
||||
await this.eventRepository.emit('AssetTrashAll', {
|
||||
assetIds: [assetId],
|
||||
userId: auth.user.id,
|
||||
});
|
||||
|
||||
await this.integrityRepository.deleteById(id);
|
||||
} else if (fileAssetId) {
|
||||
await this.assetRepository.deleteFiles([{ id: fileAssetId }]);
|
||||
} else {
|
||||
await this.storageRepository.unlink(path);
|
||||
await this.integrityRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.IntegrityOrphanedFilesQueueAll, queue: QueueName.IntegrityCheck })
|
||||
async handleOrphanedFilesQueueAll({ refreshOnly }: IIntegrityJob = {}): Promise<JobStatus> {
|
||||
this.logger.log(`Checking for out of date orphaned file reports...`);
|
||||
|
||||
@@ -106,7 +106,4 @@ describe(MaintenanceService.name, () => {
|
||||
expect(mocks.systemMetadata.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('getIntegrityReportSummary'); // just calls repository
|
||||
describe.skip('getIntegrityReport'); // just calls repository
|
||||
});
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { basename } from 'node:path';
|
||||
import { Readable } from 'node:stream';
|
||||
import { OnEvent } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
MaintenanceAuthDto,
|
||||
MaintenanceGetIntegrityReportDto,
|
||||
MaintenanceIntegrityReportResponseDto,
|
||||
MaintenanceIntegrityReportSummaryResponseDto,
|
||||
} from 'src/dtos/maintenance.dto';
|
||||
import { AssetStatus, CacheControl, IntegrityReportType, SystemMetadataKey } from 'src/enum';
|
||||
import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto';
|
||||
import { SystemMetadataKey } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { MaintenanceModeState } from 'src/types';
|
||||
import { ImmichFileResponse } from 'src/utils/file';
|
||||
import { createMaintenanceLoginUrl, generateMaintenanceSecret, signMaintenanceJwt } from 'src/utils/maintenance';
|
||||
import { getExternalDomain } from 'src/utils/misc';
|
||||
|
||||
@@ -59,52 +50,4 @@ export class MaintenanceService extends BaseService {
|
||||
|
||||
return await createMaintenanceLoginUrl(baseUrl, auth, secret);
|
||||
}
|
||||
|
||||
getIntegrityReportSummary(): Promise<MaintenanceIntegrityReportSummaryResponseDto> {
|
||||
return this.integrityRepository.getIntegrityReportSummary();
|
||||
}
|
||||
|
||||
async getIntegrityReport(dto: MaintenanceGetIntegrityReportDto): Promise<MaintenanceIntegrityReportResponseDto> {
|
||||
return {
|
||||
items: await this.integrityRepository.getIntegrityReports(dto.type),
|
||||
};
|
||||
}
|
||||
|
||||
getIntegrityReportCsv(type: IntegrityReportType): Readable {
|
||||
return this.integrityRepository.streamIntegrityReportsCSV(type);
|
||||
}
|
||||
|
||||
async getIntegrityReportFile(id: string): Promise<ImmichFileResponse> {
|
||||
const { path } = await this.integrityRepository.getById(id);
|
||||
|
||||
return new ImmichFileResponse({
|
||||
path,
|
||||
fileName: basename(path),
|
||||
contentType: 'application/octet-stream',
|
||||
cacheControl: CacheControl.PrivateWithoutCache,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteIntegrityReport(auth: AuthDto, id: string): Promise<void> {
|
||||
const { path, assetId, fileAssetId } = await this.integrityRepository.getById(id);
|
||||
|
||||
if (assetId) {
|
||||
await this.assetRepository.updateAll([assetId], {
|
||||
deletedAt: new Date(),
|
||||
status: AssetStatus.Trashed,
|
||||
});
|
||||
|
||||
await this.eventRepository.emit('AssetTrashAll', {
|
||||
assetIds: [assetId],
|
||||
userId: auth.user.id,
|
||||
});
|
||||
|
||||
await this.integrityRepository.deleteById(id);
|
||||
} else if (fileAssetId) {
|
||||
await this.assetRepository.deleteFiles([{ id: fileAssetId }]);
|
||||
} else {
|
||||
await this.storageRepository.unlink(path);
|
||||
await this.integrityRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user