From 0fdc7b44488c54c1e7bccdcb0641d1dfd54f311f Mon Sep 17 00:00:00 2001 From: izzy Date: Thu, 27 Nov 2025 17:23:54 +0000 Subject: [PATCH] feat: draft controller entry chore: lint & format --- open-api/immich-openapi-specs.json | 79 +++++++++++++++++++ server/src/config.ts | 2 +- .../src/controllers/maintenance.controller.ts | 21 ++++- server/src/dtos/maintenance.dto.ts | 22 +++++- server/src/repositories/asset.repository.ts | 4 +- .../integrity-report.repository.ts | 11 +++ server/src/services/integrity.service.ts | 22 +++--- server/src/services/maintenance.service.ts | 10 ++- .../services/system-config.service.spec.ts | 2 +- 9 files changed, 154 insertions(+), 19 deletions(-) diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index f1a61bc4ea..0d87727e8a 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -323,6 +323,51 @@ } }, "/admin/maintenance": { + "get": { + "description": "...", + "operationId": "getIntegrityReport", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MaintenanceIntegrityReportResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Get integrity report", + "tags": [ + "Maintenance (admin)" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v9.9.9", + "state": "Added" + }, + { + "version": "v9.9.9", + "state": "Alpha" + } + ], + "x-immich-permission": "maintenance", + "x-immich-state": "Alpha" + }, "post": { "description": "Put Immich into or take it out of maintenance mode", "operationId": "setMaintenanceMode", @@ -16920,6 +16965,40 @@ ], "type": "object" }, + "MaintenanceIntegrityReportDto": { + "properties": { + "path": { + "type": "string" + }, + "type": { + "enum": [ + "orphan_file", + "missing_file", + "checksum_mismatch" + ], + "type": "string" + } + }, + "required": [ + "path", + "type" + ], + "type": "object" + }, + "MaintenanceIntegrityReportResponseDto": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/MaintenanceIntegrityReportDto" + }, + "type": "array" + } + }, + "required": [ + "items" + ], + "type": "object" + }, "MaintenanceLoginDto": { "properties": { "token": { diff --git a/server/src/config.ts b/server/src/config.ts index bd3c745daf..bca84ef40b 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -251,7 +251,7 @@ export const defaults = Object.freeze({ enabled: true, cronExpression: CronExpression.EVERY_DAY_AT_3AM, timeLimit: 60 * 60 * 1000, // 1 hour - percentageLimit: 1.0, // 100% of assets + percentageLimit: 1, // 100% of assets }, }, job: { diff --git a/server/src/controllers/maintenance.controller.ts b/server/src/controllers/maintenance.controller.ts index 7b2aa17582..36e8003521 100644 --- a/server/src/controllers/maintenance.controller.ts +++ b/server/src/controllers/maintenance.controller.ts @@ -1,9 +1,15 @@ -import { BadRequestException, Body, Controller, Post, Res } from '@nestjs/common'; +import { BadRequestException, Body, Controller, Get, Post, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Response } from 'express'; import { Endpoint, HistoryBuilder } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; -import { MaintenanceAuthDto, MaintenanceLoginDto, SetMaintenanceModeDto } from 'src/dtos/maintenance.dto'; +import { + MaintenanceAuthDto, + MaintenanceGetIntegrityReportDto, + MaintenanceIntegrityReportResponseDto, + MaintenanceLoginDto, + SetMaintenanceModeDto, +} from 'src/dtos/maintenance.dto'; import { ApiTag, ImmichCookie, MaintenanceAction, Permission } from 'src/enum'; import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; import { LoginDetails } from 'src/services/auth.service'; @@ -46,4 +52,15 @@ export class MaintenanceController { }); } } + + @Get() + @Endpoint({ + summary: 'Get integrity report', + description: '...', + history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'), + }) + @Authenticated({ permission: Permission.Maintenance, admin: true }) + getIntegrityReport(dto: MaintenanceGetIntegrityReportDto): Promise { + return this.service.getIntegrityReport(dto); + } } diff --git a/server/src/dtos/maintenance.dto.ts b/server/src/dtos/maintenance.dto.ts index fe6960c0a4..6d7f15f55c 100644 --- a/server/src/dtos/maintenance.dto.ts +++ b/server/src/dtos/maintenance.dto.ts @@ -1,4 +1,5 @@ -import { MaintenanceAction } from 'src/enum'; +import { IsEnum } from 'class-validator'; +import { IntegrityReportType, MaintenanceAction } from 'src/enum'; import { ValidateEnum, ValidateString } from 'src/validation'; export class SetMaintenanceModeDto { @@ -14,3 +15,22 @@ export class MaintenanceLoginDto { export class MaintenanceAuthDto { username!: string; } + +export class MaintenanceGetIntegrityReportDto { + // todo: paginate + // @IsInt() + // @Min(1) + // @Type(() => Number) + // @Optional() + // page?: number; +} + +class MaintenanceIntegrityReportDto { + @IsEnum(IntegrityReportType) + type!: IntegrityReportType; + path!: string; +} + +export class MaintenanceIntegrityReportResponseDto { + items!: MaintenanceIntegrityReportDto[]; +} diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 226d021745..afdc29876e 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -382,11 +382,11 @@ export class AssetRepository { return items.map((asset) => asset.deviceAssetId); } - async getAllAssetPaths() { + getAllAssetPaths() { return this.db.selectFrom('asset').select(['originalPath', 'encodedVideoPath']).stream(); } - async getAllAssetFilePaths() { + getAllAssetFilePaths() { return this.db.selectFrom('asset_file').select(['path']).stream(); } diff --git a/server/src/repositories/integrity-report.repository.ts b/server/src/repositories/integrity-report.repository.ts index 25194731d6..af36f051de 100644 --- a/server/src/repositories/integrity-report.repository.ts +++ b/server/src/repositories/integrity-report.repository.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { Insertable, Kysely } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; +import { MaintenanceGetIntegrityReportDto, MaintenanceIntegrityReportResponseDto } from 'src/dtos/maintenance.dto'; import { DB } from 'src/schema'; import { IntegrityReportTable } from 'src/schema/tables/integrity-report.table'; @@ -17,6 +18,16 @@ export class IntegrityReportRepository { .executeTakeFirst(); } + async getIntegrityReport(_dto: MaintenanceGetIntegrityReportDto): Promise { + return { + items: await this.db + .selectFrom('integrity_report') + .select(['type', 'path']) + .orderBy('createdAt', 'desc') + .execute(), + }; + } + deleteById(id: string) { return this.db.deleteFrom('integrity_report').where('id', '=', id).execute(); } diff --git a/server/src/services/integrity.service.ts b/server/src/services/integrity.service.ts index b876004385..fcc7589642 100644 --- a/server/src/services/integrity.service.ts +++ b/server/src/services/integrity.service.ts @@ -33,7 +33,7 @@ async function* chunk(generator: AsyncIterableIterator, n: number) { } } - if (chunk.length) { + if (chunk.length > 0) { yield chunk; } } @@ -83,17 +83,17 @@ export class IntegrityService extends BaseService { // debug: run on boot setImmediate(() => { - this.jobRepository.queue({ + void this.jobRepository.queue({ name: JobName.IntegrityOrphanedFilesQueueAll, data: {}, }); - this.jobRepository.queue({ + void this.jobRepository.queue({ name: JobName.IntegrityMissingFilesQueueAll, data: {}, }); - this.jobRepository.queue({ + void this.jobRepository.queue({ name: JobName.IntegrityChecksumFiles, data: {}, }); @@ -101,7 +101,7 @@ export class IntegrityService extends BaseService { } @OnEvent({ name: 'ConfigUpdate', server: true }) - async onConfigUpdate({ + onConfigUpdate({ newConfig: { integrityChecks: { orphanedFiles, missingFiles, checksumFiles }, }, @@ -237,9 +237,9 @@ export class IntegrityService extends BaseService { ), ); - const reportIds = results.filter((reportId) => reportId) as string[]; + const reportIds = results.filter(Boolean) as string[]; - if (reportIds.length) { + if (reportIds.length > 0) { await this.integrityReportRepository.deleteByIds(reportIds); } @@ -285,12 +285,12 @@ export class IntegrityService extends BaseService { .filter(({ exists, reportId }) => exists && reportId) .map(({ reportId }) => reportId!); - if (outdatedReports.length) { + if (outdatedReports.length > 0) { await this.integrityReportRepository.deleteByIds(outdatedReports); } const missingFiles = results.filter(({ exists }) => !exists); - if (missingFiles.length) { + if (missingFiles.length > 0) { await this.integrityReportRepository.create( missingFiles.map(({ path }) => ({ type: IntegrityReportType.MissingFile, @@ -306,7 +306,7 @@ export class IntegrityService extends BaseService { @OnJob({ name: JobName.IntegrityChecksumFiles, queue: QueueName.BackgroundTask }) async handleChecksumFiles(): Promise { const timeLimit = 60 * 60 * 1000; // 1000; - const percentageLimit = 1.0; // 0.25; + const percentageLimit = 1; // 0.25; this.logger.log( `Checking file checksums... (will run for up to ${(timeLimit / (60 * 60 * 1000)).toFixed(2)} hours or until ${(percentageLimit * 100).toFixed(2)}% of assets are processed)`, @@ -390,7 +390,7 @@ export class IntegrityService extends BaseService { } } while (endMarker); - this.systemMetadataRepository.set(SystemMetadataKey.IntegrityChecksumCheckpoint, { + await this.systemMetadataRepository.set(SystemMetadataKey.IntegrityChecksumCheckpoint, { date: lastCreatedAt?.toISOString(), }); diff --git a/server/src/services/maintenance.service.ts b/server/src/services/maintenance.service.ts index e6808300bc..7438acfc1d 100644 --- a/server/src/services/maintenance.service.ts +++ b/server/src/services/maintenance.service.ts @@ -1,6 +1,10 @@ import { Injectable } from '@nestjs/common'; import { OnEvent } from 'src/decorators'; -import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto'; +import { + MaintenanceAuthDto, + MaintenanceGetIntegrityReportDto, + MaintenanceIntegrityReportResponseDto, +} from 'src/dtos/maintenance.dto'; import { SystemMetadataKey } from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { MaintenanceModeState } from 'src/types'; @@ -50,4 +54,8 @@ export class MaintenanceService extends BaseService { return await createMaintenanceLoginUrl(baseUrl, auth, secret); } + + getIntegrityReport(dto: MaintenanceGetIntegrityReportDto): Promise { + return this.integrityReportRepository.getIntegrityReport(dto); + } } diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 8f5ed468de..7fe13ad84f 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -85,7 +85,7 @@ const updatedConfig = Object.freeze({ enabled: true, cronExpression: '0 03 * * *', timeLimit: 60 * 60 * 1000, - percentageLimit: 1.0, + percentageLimit: 1, }, }, logging: {