diff --git a/mobile/openapi/lib/model/all_job_status_response_dto.dart b/mobile/openapi/lib/model/all_job_status_response_dto.dart index 787d02dd0e..993c4c1ddb 100644 --- a/mobile/openapi/lib/model/all_job_status_response_dto.dart +++ b/mobile/openapi/lib/model/all_job_status_response_dto.dart @@ -18,6 +18,7 @@ class AllJobStatusResponseDto { required this.duplicateDetection, required this.faceDetection, required this.facialRecognition, + required this.integrityDatabaseCheck, required this.library_, required this.metadataExtraction, required this.migration, @@ -40,6 +41,8 @@ class AllJobStatusResponseDto { JobStatusDto facialRecognition; + JobStatusDto integrityDatabaseCheck; + JobStatusDto library_; JobStatusDto metadataExtraction; @@ -67,6 +70,7 @@ class AllJobStatusResponseDto { other.duplicateDetection == duplicateDetection && other.faceDetection == faceDetection && other.facialRecognition == facialRecognition && + other.integrityDatabaseCheck == integrityDatabaseCheck && other.library_ == library_ && other.metadataExtraction == metadataExtraction && other.migration == migration && @@ -86,6 +90,7 @@ class AllJobStatusResponseDto { (duplicateDetection.hashCode) + (faceDetection.hashCode) + (facialRecognition.hashCode) + + (integrityDatabaseCheck.hashCode) + (library_.hashCode) + (metadataExtraction.hashCode) + (migration.hashCode) + @@ -98,7 +103,7 @@ class AllJobStatusResponseDto { (videoConversion.hashCode); @override - String toString() => 'AllJobStatusResponseDto[backgroundTask=$backgroundTask, backupDatabase=$backupDatabase, duplicateDetection=$duplicateDetection, faceDetection=$faceDetection, facialRecognition=$facialRecognition, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion]'; + String toString() => 'AllJobStatusResponseDto[backgroundTask=$backgroundTask, backupDatabase=$backupDatabase, duplicateDetection=$duplicateDetection, faceDetection=$faceDetection, facialRecognition=$facialRecognition, integrityDatabaseCheck=$integrityDatabaseCheck, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion]'; Map toJson() { final json = {}; @@ -107,6 +112,7 @@ class AllJobStatusResponseDto { json[r'duplicateDetection'] = this.duplicateDetection; json[r'faceDetection'] = this.faceDetection; json[r'facialRecognition'] = this.facialRecognition; + json[r'integrityDatabaseCheck'] = this.integrityDatabaseCheck; json[r'library'] = this.library_; json[r'metadataExtraction'] = this.metadataExtraction; json[r'migration'] = this.migration; @@ -134,6 +140,7 @@ class AllJobStatusResponseDto { duplicateDetection: JobStatusDto.fromJson(json[r'duplicateDetection'])!, faceDetection: JobStatusDto.fromJson(json[r'faceDetection'])!, facialRecognition: JobStatusDto.fromJson(json[r'facialRecognition'])!, + integrityDatabaseCheck: JobStatusDto.fromJson(json[r'integrityDatabaseCheck'])!, library_: JobStatusDto.fromJson(json[r'library'])!, metadataExtraction: JobStatusDto.fromJson(json[r'metadataExtraction'])!, migration: JobStatusDto.fromJson(json[r'migration'])!, @@ -196,6 +203,7 @@ class AllJobStatusResponseDto { 'duplicateDetection', 'faceDetection', 'facialRecognition', + 'integrityDatabaseCheck', 'library', 'metadataExtraction', 'migration', diff --git a/mobile/openapi/lib/model/job_name.dart b/mobile/openapi/lib/model/job_name.dart index 6b9a002cbe..842ca51d69 100644 --- a/mobile/openapi/lib/model/job_name.dart +++ b/mobile/openapi/lib/model/job_name.dart @@ -38,6 +38,7 @@ class JobName { static const library_ = JobName._(r'library'); static const notifications = JobName._(r'notifications'); static const backupDatabase = JobName._(r'backupDatabase'); + static const integrityDatabaseCheck = JobName._(r'integrityDatabaseCheck'); /// List of all possible values in this [enum][JobName]. static const values = [ @@ -56,6 +57,7 @@ class JobName { library_, notifications, backupDatabase, + integrityDatabaseCheck, ]; static JobName? fromJson(dynamic value) => JobNameTypeTransformer().decode(value); @@ -109,6 +111,7 @@ class JobNameTypeTransformer { case r'library': return JobName.library_; case r'notifications': return JobName.notifications; case r'backupDatabase': return JobName.backupDatabase; + case r'integrityDatabaseCheck': return JobName.integrityDatabaseCheck; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/mobile/openapi/lib/model/manual_job_name.dart b/mobile/openapi/lib/model/manual_job_name.dart index 311215ad9e..1ecbd488fb 100644 --- a/mobile/openapi/lib/model/manual_job_name.dart +++ b/mobile/openapi/lib/model/manual_job_name.dart @@ -29,6 +29,7 @@ class ManualJobName { static const memoryCleanup = ManualJobName._(r'memory-cleanup'); static const memoryCreate = ManualJobName._(r'memory-create'); static const backupDatabase = ManualJobName._(r'backup-database'); + static const integrityDatabaseCheck = ManualJobName._(r'integrity-database-check'); /// List of all possible values in this [enum][ManualJobName]. static const values = [ @@ -38,6 +39,7 @@ class ManualJobName { memoryCleanup, memoryCreate, backupDatabase, + integrityDatabaseCheck, ]; static ManualJobName? fromJson(dynamic value) => ManualJobNameTypeTransformer().decode(value); @@ -82,6 +84,7 @@ class ManualJobNameTypeTransformer { case r'memory-cleanup': return ManualJobName.memoryCleanup; case r'memory-create': return ManualJobName.memoryCreate; case r'backup-database': return ManualJobName.backupDatabase; + case r'integrity-database-check': return ManualJobName.integrityDatabaseCheck; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 5de3987367..0ad1e95aeb 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8532,6 +8532,9 @@ "facialRecognition": { "$ref": "#/components/schemas/JobStatusDto" }, + "integrityDatabaseCheck": { + "$ref": "#/components/schemas/JobStatusDto" + }, "library": { "$ref": "#/components/schemas/JobStatusDto" }, @@ -8569,6 +8572,7 @@ "duplicateDetection", "faceDetection", "facialRecognition", + "integrityDatabaseCheck", "library", "metadataExtraction", "migration", @@ -10101,7 +10105,8 @@ "sidecar", "library", "notifications", - "backupDatabase" + "backupDatabase", + "integrityDatabaseCheck" ], "type": "string" }, @@ -10336,7 +10341,8 @@ "user-cleanup", "memory-cleanup", "memory-create", - "backup-database" + "backup-database", + "integrity-database-check" ], "type": "string" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index c293b2aa6c..8f695bec44 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -622,6 +622,7 @@ export type AllJobStatusResponseDto = { duplicateDetection: JobStatusDto; faceDetection: JobStatusDto; facialRecognition: JobStatusDto; + integrityDatabaseCheck: JobStatusDto; library: JobStatusDto; metadataExtraction: JobStatusDto; migration: JobStatusDto; @@ -3789,7 +3790,8 @@ export enum ManualJobName { UserCleanup = "user-cleanup", MemoryCleanup = "memory-cleanup", MemoryCreate = "memory-create", - BackupDatabase = "backup-database" + BackupDatabase = "backup-database", + IntegrityDatabaseCheck = "integrity-database-check" } export enum JobName { ThumbnailGeneration = "thumbnailGeneration", @@ -3806,7 +3808,8 @@ export enum JobName { Sidecar = "sidecar", Library = "library", Notifications = "notifications", - BackupDatabase = "backupDatabase" + BackupDatabase = "backupDatabase", + IntegrityDatabaseCheck = "integrityDatabaseCheck" } export enum JobCommand { Start = "start", diff --git a/server/src/dtos/job.dto.ts b/server/src/dtos/job.dto.ts index ce6aad4c06..89a62ad9a4 100644 --- a/server/src/dtos/job.dto.ts +++ b/server/src/dtos/job.dto.ts @@ -99,4 +99,7 @@ export class AllJobStatusResponseDto implements Record @ApiProperty({ type: JobStatusDto }) [QueueName.BACKUP_DATABASE]!: JobStatusDto; + + @ApiProperty({ type: JobStatusDto }) + [QueueName.DATABASE_INTEGRITY_CHECK]!: JobStatusDto; } diff --git a/server/src/enum.ts b/server/src/enum.ts index e49f1636a0..dac5da2886 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -251,6 +251,7 @@ export enum ManualJobName { MEMORY_CLEANUP = 'memory-cleanup', MEMORY_CREATE = 'memory-create', BACKUP_DATABASE = 'backup-database', + INTEGRITY_DATABASE_CHECK = 'integrity-database-check', } export enum AssetPathType { @@ -441,6 +442,7 @@ export enum QueueName { LIBRARY = 'library', NOTIFICATION = 'notifications', BACKUP_DATABASE = 'backupDatabase', + DATABASE_INTEGRITY_CHECK = 'integrityDatabaseCheck', } export enum JobName { @@ -532,6 +534,9 @@ export enum JobName { // Version check VERSION_CHECK = 'version-check', + + // Integrity + DATABASE_INTEGRITY_CHECK = 'database-integrity-check', } export enum JobCommand { diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 4564971ac2..85b0f7df76 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -463,3 +463,12 @@ where and "libraryId" = $2::uuid and "isExternal" = $3 ) + +-- AssetRepository.integrityCheckExif +select + "id" +from + "assets" + left join "exif" on "assets"."id" = "exif"."assetId" +where + "exif"."assetId" is null diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index d49124b04b..692ec86453 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -875,4 +875,16 @@ export class AssetRepository { return count; } + + @GenerateSql() + async integrityCheckExif(): Promise { + const result = await this.db + .selectFrom('assets') + .select('id') + .leftJoin('exif', 'assets.id', 'exif.assetId') + .where('exif.assetId', 'is', null) + .execute(); + + return result.map((row) => row.id); + } } diff --git a/server/src/services/index.ts b/server/src/services/index.ts index 88b68d2c13..27e8192b2f 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -11,6 +11,7 @@ import { CliService } from 'src/services/cli.service'; import { DatabaseService } from 'src/services/database.service'; import { DownloadService } from 'src/services/download.service'; import { DuplicateService } from 'src/services/duplicate.service'; +import { IntegrityService } from 'src/services/integrity.service'; import { JobService } from 'src/services/job.service'; import { LibraryService } from 'src/services/library.service'; import { MapService } from 'src/services/map.service'; @@ -54,6 +55,7 @@ export const services = [ DatabaseService, DownloadService, DuplicateService, + IntegrityService, JobService, LibraryService, MapService, diff --git a/server/src/services/integrity.service.ts b/server/src/services/integrity.service.ts new file mode 100644 index 0000000000..8cc3226496 --- /dev/null +++ b/server/src/services/integrity.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@nestjs/common'; +import { OnJob } from 'src/decorators'; +import { JobName, JobStatus, QueueName } from 'src/enum'; +import { BaseService } from 'src/services/base.service'; + +@Injectable() +export class IntegrityService extends BaseService { + @OnJob({ name: JobName.DATABASE_INTEGRITY_CHECK, queue: QueueName.DATABASE_INTEGRITY_CHECK }) + async handleDatabaseIntegrityCheck(): Promise { + console.log(JSON.stringify(await this.assetRepository.integrityCheckExif())); + return JobStatus.SUCCESS; + } +} diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index fd573d9b97..4b9fed4a3b 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -46,6 +46,10 @@ const asJobItem = (dto: JobCreateDto): JobItem => { return { name: JobName.BACKUP_DATABASE }; } + case ManualJobName.INTEGRITY_DATABASE_CHECK: { + return { name: JobName.DATABASE_INTEGRITY_CHECK }; + } + default: { throw new BadRequestException('Invalid job name'); } @@ -228,6 +232,7 @@ export class JobService extends BaseService { QueueName.STORAGE_TEMPLATE_MIGRATION, QueueName.DUPLICATE_DETECTION, QueueName.BACKUP_DATABASE, + QueueName.DATABASE_INTEGRITY_CHECK, ].includes(name); } diff --git a/server/src/types.ts b/server/src/types.ts index 2f5bfad02c..626110eb51 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -164,6 +164,7 @@ export type ConcurrentQueueName = Exclude< | QueueName.FACIAL_RECOGNITION | QueueName.DUPLICATE_DETECTION | QueueName.BACKUP_DATABASE + | QueueName.DATABASE_INTEGRITY_CHECK >; export type Jobs = { [K in JobItem['name']]: (JobItem & { name: K })['data'] }; @@ -363,9 +364,8 @@ export type JobItem = // Version check | { name: JobName.VERSION_CHECK; data: IBaseJob } - // Memories - | { name: JobName.MEMORIES_CLEANUP; data?: IBaseJob } - | { name: JobName.MEMORIES_CREATE; data?: IBaseJob }; + // Integrity + | { name: JobName.DATABASE_INTEGRITY_CHECK; data?: IBaseJob }; export type VectorExtension = DatabaseExtension.VECTOR | DatabaseExtension.VECTORS; diff --git a/web/src/lib/modals/JobCreateModal.svelte b/web/src/lib/modals/JobCreateModal.svelte index dbb97fdcf7..d39d7a73ab 100644 --- a/web/src/lib/modals/JobCreateModal.svelte +++ b/web/src/lib/modals/JobCreateModal.svelte @@ -20,6 +20,7 @@ { title: $t('admin.memory_cleanup_job'), value: ManualJobName.MemoryCleanup }, { title: $t('admin.memory_generate_job'), value: ManualJobName.MemoryCreate }, { title: $t('admin.backup_database'), value: ManualJobName.BackupDatabase }, + { title: 'integrity test', value: ManualJobName.IntegrityDatabaseCheck }, ].map(({ value, title }) => ({ id: value, label: title, value })); let selectedJob: ComboBoxOption | undefined = $state(undefined);