From 094a41ac9a552c123f1ffe7d8efc0f9c1790fcc5 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 30 Apr 2025 11:17:23 -0400 Subject: [PATCH] chore: remove audit file report (#17994) --- e2e/src/api/specs/audit.e2e-spec.ts | 43 --- mobile/openapi/README.md | 10 - mobile/openapi/lib/api.dart | 8 - mobile/openapi/lib/api/file_reports_api.dart | 148 -------- mobile/openapi/lib/api_client.dart | 14 - mobile/openapi/lib/api_helper.dart | 6 - .../openapi/lib/model/file_checksum_dto.dart | 101 ----- .../lib/model/file_checksum_response_dto.dart | 107 ------ mobile/openapi/lib/model/file_report_dto.dart | 109 ------ .../lib/model/file_report_fix_dto.dart | 99 ----- .../lib/model/file_report_item_dto.dart | 140 ------- .../openapi/lib/model/path_entity_type.dart | 88 ----- mobile/openapi/lib/model/path_type.dart | 103 ----- open-api/immich-openapi-specs.json | 232 ------------ open-api/typescript-sdk/src/fetch-client.ts | 65 ---- .../src/controllers/file-report.controller.ts | 29 -- server/src/controllers/index.ts | 2 - server/src/dtos/audit.dto.ts | 73 ---- server/src/services/audit.service.spec.ts | 148 +------- server/src/services/audit.service.ts | 203 +--------- web/src/routes/admin/repair/+page.svelte | 358 ------------------ web/src/routes/admin/repair/+page.ts | 18 - 22 files changed, 4 insertions(+), 2100 deletions(-) delete mode 100644 e2e/src/api/specs/audit.e2e-spec.ts delete mode 100644 mobile/openapi/lib/api/file_reports_api.dart delete mode 100644 mobile/openapi/lib/model/file_checksum_dto.dart delete mode 100644 mobile/openapi/lib/model/file_checksum_response_dto.dart delete mode 100644 mobile/openapi/lib/model/file_report_dto.dart delete mode 100644 mobile/openapi/lib/model/file_report_fix_dto.dart delete mode 100644 mobile/openapi/lib/model/file_report_item_dto.dart delete mode 100644 mobile/openapi/lib/model/path_entity_type.dart delete mode 100644 mobile/openapi/lib/model/path_type.dart delete mode 100644 server/src/controllers/file-report.controller.ts delete mode 100644 server/src/dtos/audit.dto.ts delete mode 100644 web/src/routes/admin/repair/+page.svelte delete mode 100644 web/src/routes/admin/repair/+page.ts diff --git a/e2e/src/api/specs/audit.e2e-spec.ts b/e2e/src/api/specs/audit.e2e-spec.ts deleted file mode 100644 index c6a2adbb0a5..00000000000 --- a/e2e/src/api/specs/audit.e2e-spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { deleteAssets, getAuditFiles, updateAsset, type LoginResponseDto } from '@immich/sdk'; -import { asBearerAuth, utils } from 'src/utils'; -import { beforeAll, describe, expect, it } from 'vitest'; - -describe('/audits', () => { - let admin: LoginResponseDto; - - beforeAll(async () => { - await utils.resetDatabase(); - await utils.resetFilesystem(); - - admin = await utils.adminSetup(); - }); - - // TODO: Enable these tests again once #7436 is resolved as these were flaky - describe.skip('GET :/file-report', () => { - it('excludes assets without issues from report', async () => { - const [trashedAsset, archivedAsset] = await Promise.all([ - utils.createAsset(admin.accessToken), - utils.createAsset(admin.accessToken), - utils.createAsset(admin.accessToken), - ]); - - await Promise.all([ - deleteAssets({ assetBulkDeleteDto: { ids: [trashedAsset.id] } }, { headers: asBearerAuth(admin.accessToken) }), - updateAsset( - { - id: archivedAsset.id, - updateAssetDto: { isArchived: true }, - }, - { headers: asBearerAuth(admin.accessToken) }, - ), - ]); - - const body = await getAuditFiles({ - headers: asBearerAuth(admin.accessToken), - }); - - expect(body.orphans).toHaveLength(0); - expect(body.extras).toHaveLength(0); - }); - }); -}); diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index d46945f6409..a4fbbf371a7 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -122,9 +122,6 @@ Class | Method | HTTP request | Description *FacesApi* | [**deleteFace**](doc//FacesApi.md#deleteface) | **DELETE** /faces/{id} | *FacesApi* | [**getFaces**](doc//FacesApi.md#getfaces) | **GET** /faces | *FacesApi* | [**reassignFacesById**](doc//FacesApi.md#reassignfacesbyid) | **PUT** /faces/{id} | -*FileReportsApi* | [**fixAuditFiles**](doc//FileReportsApi.md#fixauditfiles) | **POST** /reports/fix | -*FileReportsApi* | [**getAuditFiles**](doc//FileReportsApi.md#getauditfiles) | **GET** /reports | -*FileReportsApi* | [**getFileChecksums**](doc//FileReportsApi.md#getfilechecksums) | **POST** /reports/checksum | *JobsApi* | [**createJob**](doc//JobsApi.md#createjob) | **POST** /jobs | *JobsApi* | [**getAllJobsStatus**](doc//JobsApi.md#getalljobsstatus) | **GET** /jobs | *JobsApi* | [**sendJobCommand**](doc//JobsApi.md#sendjobcommand) | **PUT** /jobs/{id} | @@ -332,11 +329,6 @@ Class | Method | HTTP request | Description - [ExifResponseDto](doc//ExifResponseDto.md) - [FaceDto](doc//FaceDto.md) - [FacialRecognitionConfig](doc//FacialRecognitionConfig.md) - - [FileChecksumDto](doc//FileChecksumDto.md) - - [FileChecksumResponseDto](doc//FileChecksumResponseDto.md) - - [FileReportDto](doc//FileReportDto.md) - - [FileReportFixDto](doc//FileReportFixDto.md) - - [FileReportItemDto](doc//FileReportItemDto.md) - [FoldersResponse](doc//FoldersResponse.md) - [FoldersUpdate](doc//FoldersUpdate.md) - [ImageFormat](doc//ImageFormat.md) @@ -381,8 +373,6 @@ Class | Method | HTTP request | Description - [OnThisDayDto](doc//OnThisDayDto.md) - [PartnerDirection](doc//PartnerDirection.md) - [PartnerResponseDto](doc//PartnerResponseDto.md) - - [PathEntityType](doc//PathEntityType.md) - - [PathType](doc//PathType.md) - [PeopleResponse](doc//PeopleResponse.md) - [PeopleResponseDto](doc//PeopleResponseDto.md) - [PeopleUpdate](doc//PeopleUpdate.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index ba64363c972..78fa31691de 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -39,7 +39,6 @@ part 'api/deprecated_api.dart'; part 'api/download_api.dart'; part 'api/duplicates_api.dart'; part 'api/faces_api.dart'; -part 'api/file_reports_api.dart'; part 'api/jobs_api.dart'; part 'api/libraries_api.dart'; part 'api/map_api.dart'; @@ -133,11 +132,6 @@ part 'model/email_notifications_update.dart'; part 'model/exif_response_dto.dart'; part 'model/face_dto.dart'; part 'model/facial_recognition_config.dart'; -part 'model/file_checksum_dto.dart'; -part 'model/file_checksum_response_dto.dart'; -part 'model/file_report_dto.dart'; -part 'model/file_report_fix_dto.dart'; -part 'model/file_report_item_dto.dart'; part 'model/folders_response.dart'; part 'model/folders_update.dart'; part 'model/image_format.dart'; @@ -182,8 +176,6 @@ part 'model/o_auth_token_endpoint_auth_method.dart'; part 'model/on_this_day_dto.dart'; part 'model/partner_direction.dart'; part 'model/partner_response_dto.dart'; -part 'model/path_entity_type.dart'; -part 'model/path_type.dart'; part 'model/people_response.dart'; part 'model/people_response_dto.dart'; part 'model/people_update.dart'; diff --git a/mobile/openapi/lib/api/file_reports_api.dart b/mobile/openapi/lib/api/file_reports_api.dart deleted file mode 100644 index 73b3feaedb5..00000000000 --- a/mobile/openapi/lib/api/file_reports_api.dart +++ /dev/null @@ -1,148 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - - -class FileReportsApi { - FileReportsApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; - - final ApiClient apiClient; - - /// Performs an HTTP 'POST /reports/fix' operation and returns the [Response]. - /// Parameters: - /// - /// * [FileReportFixDto] fileReportFixDto (required): - Future fixAuditFilesWithHttpInfo(FileReportFixDto fileReportFixDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/reports/fix'; - - // ignore: prefer_final_locals - Object? postBody = fileReportFixDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [FileReportFixDto] fileReportFixDto (required): - Future fixAuditFiles(FileReportFixDto fileReportFixDto,) async { - final response = await fixAuditFilesWithHttpInfo(fileReportFixDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - } - - /// Performs an HTTP 'GET /reports' operation and returns the [Response]. - Future getAuditFilesWithHttpInfo() async { - // ignore: prefer_const_declarations - final apiPath = r'/reports'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - apiPath, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - Future getAuditFiles() async { - final response = await getAuditFilesWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'FileReportDto',) as FileReportDto; - - } - return null; - } - - /// Performs an HTTP 'POST /reports/checksum' operation and returns the [Response]. - /// Parameters: - /// - /// * [FileChecksumDto] fileChecksumDto (required): - Future getFileChecksumsWithHttpInfo(FileChecksumDto fileChecksumDto,) async { - // ignore: prefer_const_declarations - final apiPath = r'/reports/checksum'; - - // ignore: prefer_final_locals - Object? postBody = fileChecksumDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - apiPath, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [FileChecksumDto] fileChecksumDto (required): - Future?> getFileChecksums(FileChecksumDto fileChecksumDto,) async { - final response = await getFileChecksumsWithHttpInfo(fileChecksumDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } -} diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 6abe576acaf..9d0e80e58b2 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -320,16 +320,6 @@ class ApiClient { return FaceDto.fromJson(value); case 'FacialRecognitionConfig': return FacialRecognitionConfig.fromJson(value); - case 'FileChecksumDto': - return FileChecksumDto.fromJson(value); - case 'FileChecksumResponseDto': - return FileChecksumResponseDto.fromJson(value); - case 'FileReportDto': - return FileReportDto.fromJson(value); - case 'FileReportFixDto': - return FileReportFixDto.fromJson(value); - case 'FileReportItemDto': - return FileReportItemDto.fromJson(value); case 'FoldersResponse': return FoldersResponse.fromJson(value); case 'FoldersUpdate': @@ -418,10 +408,6 @@ class ApiClient { return PartnerDirectionTypeTransformer().decode(value); case 'PartnerResponseDto': return PartnerResponseDto.fromJson(value); - case 'PathEntityType': - return PathEntityTypeTypeTransformer().decode(value); - case 'PathType': - return PathTypeTypeTransformer().decode(value); case 'PeopleResponse': return PeopleResponse.fromJson(value); case 'PeopleResponseDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 5f9d15c0897..eec991e9037 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -112,12 +112,6 @@ String parameterToString(dynamic value) { if (value is PartnerDirection) { return PartnerDirectionTypeTransformer().encode(value).toString(); } - if (value is PathEntityType) { - return PathEntityTypeTypeTransformer().encode(value).toString(); - } - if (value is PathType) { - return PathTypeTypeTransformer().encode(value).toString(); - } if (value is Permission) { return PermissionTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/file_checksum_dto.dart b/mobile/openapi/lib/model/file_checksum_dto.dart deleted file mode 100644 index 7dc9ccdf2f9..00000000000 --- a/mobile/openapi/lib/model/file_checksum_dto.dart +++ /dev/null @@ -1,101 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class FileChecksumDto { - /// Returns a new [FileChecksumDto] instance. - FileChecksumDto({ - this.filenames = const [], - }); - - List filenames; - - @override - bool operator ==(Object other) => identical(this, other) || other is FileChecksumDto && - _deepEquality.equals(other.filenames, filenames); - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (filenames.hashCode); - - @override - String toString() => 'FileChecksumDto[filenames=$filenames]'; - - Map toJson() { - final json = {}; - json[r'filenames'] = this.filenames; - return json; - } - - /// Returns a new [FileChecksumDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static FileChecksumDto? fromJson(dynamic value) { - upgradeDto(value, "FileChecksumDto"); - if (value is Map) { - final json = value.cast(); - - return FileChecksumDto( - filenames: json[r'filenames'] is Iterable - ? (json[r'filenames'] as Iterable).cast().toList(growable: false) - : const [], - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = FileChecksumDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = FileChecksumDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of FileChecksumDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = FileChecksumDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'filenames', - }; -} - diff --git a/mobile/openapi/lib/model/file_checksum_response_dto.dart b/mobile/openapi/lib/model/file_checksum_response_dto.dart deleted file mode 100644 index 7b963c8bd53..00000000000 --- a/mobile/openapi/lib/model/file_checksum_response_dto.dart +++ /dev/null @@ -1,107 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class FileChecksumResponseDto { - /// Returns a new [FileChecksumResponseDto] instance. - FileChecksumResponseDto({ - required this.checksum, - required this.filename, - }); - - String checksum; - - String filename; - - @override - bool operator ==(Object other) => identical(this, other) || other is FileChecksumResponseDto && - other.checksum == checksum && - other.filename == filename; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (checksum.hashCode) + - (filename.hashCode); - - @override - String toString() => 'FileChecksumResponseDto[checksum=$checksum, filename=$filename]'; - - Map toJson() { - final json = {}; - json[r'checksum'] = this.checksum; - json[r'filename'] = this.filename; - return json; - } - - /// Returns a new [FileChecksumResponseDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static FileChecksumResponseDto? fromJson(dynamic value) { - upgradeDto(value, "FileChecksumResponseDto"); - if (value is Map) { - final json = value.cast(); - - return FileChecksumResponseDto( - checksum: mapValueOfType(json, r'checksum')!, - filename: mapValueOfType(json, r'filename')!, - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = FileChecksumResponseDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = FileChecksumResponseDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of FileChecksumResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = FileChecksumResponseDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'checksum', - 'filename', - }; -} - diff --git a/mobile/openapi/lib/model/file_report_dto.dart b/mobile/openapi/lib/model/file_report_dto.dart deleted file mode 100644 index 3dc892e5e7f..00000000000 --- a/mobile/openapi/lib/model/file_report_dto.dart +++ /dev/null @@ -1,109 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class FileReportDto { - /// Returns a new [FileReportDto] instance. - FileReportDto({ - this.extras = const [], - this.orphans = const [], - }); - - List extras; - - List orphans; - - @override - bool operator ==(Object other) => identical(this, other) || other is FileReportDto && - _deepEquality.equals(other.extras, extras) && - _deepEquality.equals(other.orphans, orphans); - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (extras.hashCode) + - (orphans.hashCode); - - @override - String toString() => 'FileReportDto[extras=$extras, orphans=$orphans]'; - - Map toJson() { - final json = {}; - json[r'extras'] = this.extras; - json[r'orphans'] = this.orphans; - return json; - } - - /// Returns a new [FileReportDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static FileReportDto? fromJson(dynamic value) { - upgradeDto(value, "FileReportDto"); - if (value is Map) { - final json = value.cast(); - - return FileReportDto( - extras: json[r'extras'] is Iterable - ? (json[r'extras'] as Iterable).cast().toList(growable: false) - : const [], - orphans: FileReportItemDto.listFromJson(json[r'orphans']), - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = FileReportDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = FileReportDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of FileReportDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = FileReportDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'extras', - 'orphans', - }; -} - diff --git a/mobile/openapi/lib/model/file_report_fix_dto.dart b/mobile/openapi/lib/model/file_report_fix_dto.dart deleted file mode 100644 index d46cdeb4b78..00000000000 --- a/mobile/openapi/lib/model/file_report_fix_dto.dart +++ /dev/null @@ -1,99 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class FileReportFixDto { - /// Returns a new [FileReportFixDto] instance. - FileReportFixDto({ - this.items = const [], - }); - - List items; - - @override - bool operator ==(Object other) => identical(this, other) || other is FileReportFixDto && - _deepEquality.equals(other.items, items); - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (items.hashCode); - - @override - String toString() => 'FileReportFixDto[items=$items]'; - - Map toJson() { - final json = {}; - json[r'items'] = this.items; - return json; - } - - /// Returns a new [FileReportFixDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static FileReportFixDto? fromJson(dynamic value) { - upgradeDto(value, "FileReportFixDto"); - if (value is Map) { - final json = value.cast(); - - return FileReportFixDto( - items: FileReportItemDto.listFromJson(json[r'items']), - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = FileReportFixDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = FileReportFixDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of FileReportFixDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = FileReportFixDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'items', - }; -} - diff --git a/mobile/openapi/lib/model/file_report_item_dto.dart b/mobile/openapi/lib/model/file_report_item_dto.dart deleted file mode 100644 index 1ef08c2b485..00000000000 --- a/mobile/openapi/lib/model/file_report_item_dto.dart +++ /dev/null @@ -1,140 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class FileReportItemDto { - /// Returns a new [FileReportItemDto] instance. - FileReportItemDto({ - this.checksum, - required this.entityId, - required this.entityType, - required this.pathType, - required this.pathValue, - }); - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? checksum; - - String entityId; - - PathEntityType entityType; - - PathType pathType; - - String pathValue; - - @override - bool operator ==(Object other) => identical(this, other) || other is FileReportItemDto && - other.checksum == checksum && - other.entityId == entityId && - other.entityType == entityType && - other.pathType == pathType && - other.pathValue == pathValue; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (checksum == null ? 0 : checksum!.hashCode) + - (entityId.hashCode) + - (entityType.hashCode) + - (pathType.hashCode) + - (pathValue.hashCode); - - @override - String toString() => 'FileReportItemDto[checksum=$checksum, entityId=$entityId, entityType=$entityType, pathType=$pathType, pathValue=$pathValue]'; - - Map toJson() { - final json = {}; - if (this.checksum != null) { - json[r'checksum'] = this.checksum; - } else { - // json[r'checksum'] = null; - } - json[r'entityId'] = this.entityId; - json[r'entityType'] = this.entityType; - json[r'pathType'] = this.pathType; - json[r'pathValue'] = this.pathValue; - return json; - } - - /// Returns a new [FileReportItemDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static FileReportItemDto? fromJson(dynamic value) { - upgradeDto(value, "FileReportItemDto"); - if (value is Map) { - final json = value.cast(); - - return FileReportItemDto( - checksum: mapValueOfType(json, r'checksum'), - entityId: mapValueOfType(json, r'entityId')!, - entityType: PathEntityType.fromJson(json[r'entityType'])!, - pathType: PathType.fromJson(json[r'pathType'])!, - pathValue: mapValueOfType(json, r'pathValue')!, - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = FileReportItemDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = FileReportItemDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of FileReportItemDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = FileReportItemDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'entityId', - 'entityType', - 'pathType', - 'pathValue', - }; -} - diff --git a/mobile/openapi/lib/model/path_entity_type.dart b/mobile/openapi/lib/model/path_entity_type.dart deleted file mode 100644 index fdcdae4f1b5..00000000000 --- a/mobile/openapi/lib/model/path_entity_type.dart +++ /dev/null @@ -1,88 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - - -class PathEntityType { - /// Instantiate a new enum with the provided [value]. - const PathEntityType._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const asset = PathEntityType._(r'asset'); - static const person = PathEntityType._(r'person'); - static const user = PathEntityType._(r'user'); - - /// List of all possible values in this [enum][PathEntityType]. - static const values = [ - asset, - person, - user, - ]; - - static PathEntityType? fromJson(dynamic value) => PathEntityTypeTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = PathEntityType.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [PathEntityType] to String, -/// and [decode] dynamic data back to [PathEntityType]. -class PathEntityTypeTypeTransformer { - factory PathEntityTypeTypeTransformer() => _instance ??= const PathEntityTypeTypeTransformer._(); - - const PathEntityTypeTypeTransformer._(); - - String encode(PathEntityType data) => data.value; - - /// Decodes a [dynamic value][data] to a PathEntityType. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - PathEntityType? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'asset': return PathEntityType.asset; - case r'person': return PathEntityType.person; - case r'user': return PathEntityType.user; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [PathEntityTypeTypeTransformer] instance. - static PathEntityTypeTypeTransformer? _instance; -} - diff --git a/mobile/openapi/lib/model/path_type.dart b/mobile/openapi/lib/model/path_type.dart deleted file mode 100644 index 55453ed1e8e..00000000000 --- a/mobile/openapi/lib/model/path_type.dart +++ /dev/null @@ -1,103 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - - -class PathType { - /// Instantiate a new enum with the provided [value]. - const PathType._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const original = PathType._(r'original'); - static const fullsize = PathType._(r'fullsize'); - static const preview = PathType._(r'preview'); - static const thumbnail = PathType._(r'thumbnail'); - static const encodedVideo = PathType._(r'encoded_video'); - static const sidecar = PathType._(r'sidecar'); - static const face = PathType._(r'face'); - static const profile = PathType._(r'profile'); - - /// List of all possible values in this [enum][PathType]. - static const values = [ - original, - fullsize, - preview, - thumbnail, - encodedVideo, - sidecar, - face, - profile, - ]; - - static PathType? fromJson(dynamic value) => PathTypeTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = PathType.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [PathType] to String, -/// and [decode] dynamic data back to [PathType]. -class PathTypeTypeTransformer { - factory PathTypeTypeTransformer() => _instance ??= const PathTypeTypeTransformer._(); - - const PathTypeTypeTransformer._(); - - String encode(PathType data) => data.value; - - /// Decodes a [dynamic value][data] to a PathType. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - PathType? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'original': return PathType.original; - case r'fullsize': return PathType.fullsize; - case r'preview': return PathType.preview; - case r'thumbnail': return PathType.thumbnail; - case r'encoded_video': return PathType.encodedVideo; - case r'sidecar': return PathType.sidecar; - case r'face': return PathType.face; - case r'profile': return PathType.profile; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [PathTypeTypeTransformer] instance. - static PathTypeTypeTransformer? _instance; -} - diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 826af5a2ecd..adad1308c2d 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4651,118 +4651,6 @@ ] } }, - "/reports": { - "get": { - "operationId": "getAuditFiles", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileReportDto" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "File Reports" - ] - } - }, - "/reports/checksum": { - "post": { - "operationId": "getFileChecksums", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileChecksumDto" - } - } - }, - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/FileChecksumResponseDto" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "File Reports" - ] - } - }, - "/reports/fix": { - "post": { - "operationId": "fixAuditFiles", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileReportFixDto" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "File Reports" - ] - } - }, "/search/cities": { "get": { "operationId": "getAssetsByCity", @@ -9749,105 +9637,6 @@ ], "type": "object" }, - "FileChecksumDto": { - "properties": { - "filenames": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "filenames" - ], - "type": "object" - }, - "FileChecksumResponseDto": { - "properties": { - "checksum": { - "type": "string" - }, - "filename": { - "type": "string" - } - }, - "required": [ - "checksum", - "filename" - ], - "type": "object" - }, - "FileReportDto": { - "properties": { - "extras": { - "items": { - "type": "string" - }, - "type": "array" - }, - "orphans": { - "items": { - "$ref": "#/components/schemas/FileReportItemDto" - }, - "type": "array" - } - }, - "required": [ - "extras", - "orphans" - ], - "type": "object" - }, - "FileReportFixDto": { - "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/FileReportItemDto" - }, - "type": "array" - } - }, - "required": [ - "items" - ], - "type": "object" - }, - "FileReportItemDto": { - "properties": { - "checksum": { - "type": "string" - }, - "entityId": { - "format": "uuid", - "type": "string" - }, - "entityType": { - "allOf": [ - { - "$ref": "#/components/schemas/PathEntityType" - } - ] - }, - "pathType": { - "allOf": [ - { - "$ref": "#/components/schemas/PathType" - } - ] - }, - "pathValue": { - "type": "string" - } - }, - "required": [ - "entityId", - "entityType", - "pathType", - "pathValue" - ], - "type": "object" - }, "FoldersResponse": { "properties": { "enabled": { @@ -10889,27 +10678,6 @@ ], "type": "object" }, - "PathEntityType": { - "enum": [ - "asset", - "person", - "user" - ], - "type": "string" - }, - "PathType": { - "enum": [ - "original", - "fullsize", - "preview", - "thumbnail", - "encoded_video", - "sidecar", - "face", - "profile" - ], - "type": "string" - }, "PeopleResponse": { "properties": { "enabled": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 743eeadf03a..2684d2558f7 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -800,27 +800,6 @@ export type AssetFaceUpdateDto = { export type PersonStatisticsResponseDto = { assets: number; }; -export type FileReportItemDto = { - checksum?: string; - entityId: string; - entityType: PathEntityType; - pathType: PathType; - pathValue: string; -}; -export type FileReportDto = { - extras: string[]; - orphans: FileReportItemDto[]; -}; -export type FileChecksumDto = { - filenames: string[]; -}; -export type FileChecksumResponseDto = { - checksum: string; - filename: string; -}; -export type FileReportFixDto = { - items: FileReportItemDto[]; -}; export type SearchExploreItem = { data: AssetResponseDto; value: string; @@ -2663,35 +2642,6 @@ export function getPersonThumbnail({ id }: { ...opts })); } -export function getAuditFiles(opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: FileReportDto; - }>("/reports", { - ...opts - })); -} -export function getFileChecksums({ fileChecksumDto }: { - fileChecksumDto: FileChecksumDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 201; - data: FileChecksumResponseDto[]; - }>("/reports/checksum", oazapfts.json({ - ...opts, - method: "POST", - body: fileChecksumDto - }))); -} -export function fixAuditFiles({ fileReportFixDto }: { - fileReportFixDto: FileReportFixDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText("/reports/fix", oazapfts.json({ - ...opts, - method: "POST", - body: fileReportFixDto - }))); -} export function getAssetsByCity(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -3751,21 +3701,6 @@ export enum PartnerDirection { SharedBy = "shared-by", SharedWith = "shared-with" } -export enum PathEntityType { - Asset = "asset", - Person = "person", - User = "user" -} -export enum PathType { - Original = "original", - Fullsize = "fullsize", - Preview = "preview", - Thumbnail = "thumbnail", - EncodedVideo = "encoded_video", - Sidecar = "sidecar", - Face = "face", - Profile = "profile" -} export enum SearchSuggestionType { Country = "country", State = "state", diff --git a/server/src/controllers/file-report.controller.ts b/server/src/controllers/file-report.controller.ts deleted file mode 100644 index a51a94a50ee..00000000000 --- a/server/src/controllers/file-report.controller.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Body, Controller, Get, Post } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { FileChecksumDto, FileChecksumResponseDto, FileReportDto, FileReportFixDto } from 'src/dtos/audit.dto'; -import { Authenticated } from 'src/middleware/auth.guard'; -import { AuditService } from 'src/services/audit.service'; - -@ApiTags('File Reports') -@Controller('reports') -export class ReportController { - constructor(private service: AuditService) {} - - @Get() - @Authenticated({ admin: true }) - getAuditFiles(): Promise { - return this.service.getFileReport(); - } - - @Post('checksum') - @Authenticated({ admin: true }) - getFileChecksums(@Body() dto: FileChecksumDto): Promise { - return this.service.getChecksums(dto); - } - - @Post('fix') - @Authenticated({ admin: true }) - fixAuditFiles(@Body() dto: FileReportFixDto): Promise { - return this.service.fixItems(dto.items); - } -} diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index e36793b3d73..9c39e580b6b 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -8,7 +8,6 @@ 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 { ReportController } from 'src/controllers/file-report.controller'; import { JobController } from 'src/controllers/job.controller'; import { LibraryController } from 'src/controllers/library.controller'; import { MapController } from 'src/controllers/map.controller'; @@ -53,7 +52,6 @@ export const controllers = [ OAuthController, PartnerController, PersonController, - ReportController, SearchController, ServerController, SessionController, diff --git a/server/src/dtos/audit.dto.ts b/server/src/dtos/audit.dto.ts deleted file mode 100644 index 434da46eba9..00000000000 --- a/server/src/dtos/audit.dto.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsArray, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator'; -import { AssetPathType, EntityType, PathType, PersonPathType, UserPathType } from 'src/enum'; -import { Optional, ValidateDate, ValidateUUID } from 'src/validation'; - -const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType }); - -export class AuditDeletesDto { - @ValidateDate() - after!: Date; - - @ApiProperty({ enum: EntityType, enumName: 'EntityType' }) - @IsEnum(EntityType) - entityType!: EntityType; - - @Optional() - @IsUUID('4') - @ApiProperty({ format: 'uuid' }) - userId?: string; -} - -export enum PathEntityType { - ASSET = 'asset', - PERSON = 'person', - USER = 'user', -} - -export class AuditDeletesResponseDto { - needsFullSync!: boolean; - ids!: string[]; -} - -export class FileReportDto { - orphans!: FileReportItemDto[]; - extras!: string[]; -} - -export class FileChecksumDto { - @IsString({ each: true }) - filenames!: string[]; -} - -export class FileChecksumResponseDto { - filename!: string; - checksum!: string; -} - -export class FileReportFixDto { - @IsArray() - @ValidateNested({ each: true }) - @Type(() => FileReportItemDto) - items!: FileReportItemDto[]; -} - -// used both as request and response dto -export class FileReportItemDto { - @ValidateUUID() - entityId!: string; - - @ApiProperty({ enumName: 'PathEntityType', enum: PathEntityType }) - @IsEnum(PathEntityType) - entityType!: PathEntityType; - - @ApiProperty({ enumName: 'PathType', enum: PathEnum }) - @IsEnum(PathEnum) - pathType!: PathType; - - @IsString() - pathValue!: string; - - checksum?: string; -} diff --git a/server/src/services/audit.service.spec.ts b/server/src/services/audit.service.spec.ts index 6ef139f506a..381b2ec7e82 100644 --- a/server/src/services/audit.service.spec.ts +++ b/server/src/services/audit.service.spec.ts @@ -1,6 +1,4 @@ -import { BadRequestException } from '@nestjs/common'; -import { FileReportItemDto } from 'src/dtos/audit.dto'; -import { AssetFileType, AssetPathType, JobStatus, PersonPathType, UserPathType } from 'src/enum'; +import { JobStatus } from 'src/enum'; import { AuditService } from 'src/services/audit.service'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -25,148 +23,4 @@ describe(AuditService.name, () => { expect(mocks.audit.removeBefore).toHaveBeenCalledWith(expect.any(Date)); }); }); - - describe('getChecksums', () => { - it('should fail if the file is not in the immich path', async () => { - await expect(sut.getChecksums({ filenames: ['foo/bar'] })).rejects.toBeInstanceOf(BadRequestException); - - expect(mocks.crypto.hashFile).not.toHaveBeenCalled(); - }); - - it('should get checksum for valid file', async () => { - await expect(sut.getChecksums({ filenames: ['./upload/my-file.jpg'] })).resolves.toEqual([ - { filename: './upload/my-file.jpg', checksum: expect.any(String) }, - ]); - - expect(mocks.crypto.hashFile).toHaveBeenCalledWith('./upload/my-file.jpg'); - }); - }); - - describe('fixItems', () => { - it('should fail if the file is not in the immich path', async () => { - await expect( - sut.fixItems([ - { entityId: 'my-id', pathType: AssetPathType.ORIGINAL, pathValue: 'foo/bar' } as FileReportItemDto, - ]), - ).rejects.toBeInstanceOf(BadRequestException); - - expect(mocks.asset.update).not.toHaveBeenCalled(); - expect(mocks.asset.upsertFile).not.toHaveBeenCalled(); - expect(mocks.person.update).not.toHaveBeenCalled(); - expect(mocks.user.update).not.toHaveBeenCalled(); - }); - - it('should update encoded video path', async () => { - await sut.fixItems([ - { - entityId: 'my-id', - pathType: AssetPathType.ENCODED_VIDEO, - pathValue: './upload/my-video.mp4', - } as FileReportItemDto, - ]); - - expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'my-id', encodedVideoPath: './upload/my-video.mp4' }); - expect(mocks.asset.upsertFile).not.toHaveBeenCalled(); - expect(mocks.person.update).not.toHaveBeenCalled(); - expect(mocks.user.update).not.toHaveBeenCalled(); - }); - - it('should update preview path', async () => { - await sut.fixItems([ - { - entityId: 'my-id', - pathType: AssetPathType.PREVIEW, - pathValue: './upload/my-preview.png', - } as FileReportItemDto, - ]); - - expect(mocks.asset.upsertFile).toHaveBeenCalledWith({ - assetId: 'my-id', - type: AssetFileType.PREVIEW, - path: './upload/my-preview.png', - }); - expect(mocks.asset.update).not.toHaveBeenCalled(); - expect(mocks.person.update).not.toHaveBeenCalled(); - expect(mocks.user.update).not.toHaveBeenCalled(); - }); - - it('should update thumbnail path', async () => { - await sut.fixItems([ - { - entityId: 'my-id', - pathType: AssetPathType.THUMBNAIL, - pathValue: './upload/my-thumbnail.webp', - } as FileReportItemDto, - ]); - - expect(mocks.asset.upsertFile).toHaveBeenCalledWith({ - assetId: 'my-id', - type: AssetFileType.THUMBNAIL, - path: './upload/my-thumbnail.webp', - }); - expect(mocks.asset.update).not.toHaveBeenCalled(); - expect(mocks.person.update).not.toHaveBeenCalled(); - expect(mocks.user.update).not.toHaveBeenCalled(); - }); - - it('should update original path', async () => { - await sut.fixItems([ - { - entityId: 'my-id', - pathType: AssetPathType.ORIGINAL, - pathValue: './upload/my-original.png', - } as FileReportItemDto, - ]); - - expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'my-id', originalPath: './upload/my-original.png' }); - expect(mocks.asset.upsertFile).not.toHaveBeenCalled(); - expect(mocks.person.update).not.toHaveBeenCalled(); - expect(mocks.user.update).not.toHaveBeenCalled(); - }); - - it('should update sidecar path', async () => { - await sut.fixItems([ - { - entityId: 'my-id', - pathType: AssetPathType.SIDECAR, - pathValue: './upload/my-sidecar.xmp', - } as FileReportItemDto, - ]); - - expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'my-id', sidecarPath: './upload/my-sidecar.xmp' }); - expect(mocks.asset.upsertFile).not.toHaveBeenCalled(); - expect(mocks.person.update).not.toHaveBeenCalled(); - expect(mocks.user.update).not.toHaveBeenCalled(); - }); - - it('should update face path', async () => { - await sut.fixItems([ - { - entityId: 'my-id', - pathType: PersonPathType.FACE, - pathValue: './upload/my-face.jpg', - } as FileReportItemDto, - ]); - - expect(mocks.person.update).toHaveBeenCalledWith({ id: 'my-id', thumbnailPath: './upload/my-face.jpg' }); - expect(mocks.asset.update).not.toHaveBeenCalled(); - expect(mocks.asset.upsertFile).not.toHaveBeenCalled(); - expect(mocks.user.update).not.toHaveBeenCalled(); - }); - - it('should update profile path', async () => { - await sut.fixItems([ - { - entityId: 'my-id', - pathType: UserPathType.PROFILE, - pathValue: './upload/my-profile-pic.jpg', - } as FileReportItemDto, - ]); - - expect(mocks.user.update).toHaveBeenCalledWith('my-id', { profileImagePath: './upload/my-profile-pic.jpg' }); - expect(mocks.asset.update).not.toHaveBeenCalled(); - expect(mocks.asset.upsertFile).not.toHaveBeenCalled(); - expect(mocks.person.update).not.toHaveBeenCalled(); - }); - }); }); diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts index a049a9c64b3..7c9a070dd0a 100644 --- a/server/src/services/audit.service.ts +++ b/server/src/services/audit.service.ts @@ -1,23 +1,9 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; -import { resolve } from 'node:path'; -import { AUDIT_LOG_MAX_DURATION, JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; -import { StorageCore } from 'src/cores/storage.core'; +import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; import { OnJob } from 'src/decorators'; -import { FileChecksumDto, FileChecksumResponseDto, FileReportItemDto, PathEntityType } from 'src/dtos/audit.dto'; -import { - AssetFileType, - AssetPathType, - JobName, - JobStatus, - PersonPathType, - QueueName, - StorageFolder, - UserPathType, -} from 'src/enum'; +import { JobName, JobStatus, QueueName } from 'src/enum'; import { BaseService } from 'src/services/base.service'; -import { getAssetFiles } from 'src/utils/asset.util'; -import { usePagination } from 'src/utils/pagination'; @Injectable() export class AuditService extends BaseService { @@ -26,187 +12,4 @@ export class AuditService extends BaseService { await this.auditRepository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate()); return JobStatus.SUCCESS; } - - async getChecksums(dto: FileChecksumDto) { - const results: FileChecksumResponseDto[] = []; - for (const filename of dto.filenames) { - if (!StorageCore.isImmichPath(filename)) { - throw new BadRequestException( - `Could not get the checksum of ${filename} because the file isn't accessible by Immich`, - ); - } - - const checksum = await this.cryptoRepository.hashFile(filename); - results.push({ filename, checksum: checksum.toString('base64') }); - } - return results; - } - - async fixItems(items: FileReportItemDto[]) { - for (const { entityId: id, pathType, pathValue } of items) { - if (!StorageCore.isImmichPath(pathValue)) { - throw new BadRequestException( - `Could not fix item ${id} with path ${pathValue} because the file isn't accessible by Immich`, - ); - } - - switch (pathType) { - case AssetPathType.ENCODED_VIDEO: { - await this.assetRepository.update({ id, encodedVideoPath: pathValue }); - break; - } - - case AssetPathType.PREVIEW: { - await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.PREVIEW, path: pathValue }); - break; - } - - case AssetPathType.THUMBNAIL: { - await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.THUMBNAIL, path: pathValue }); - break; - } - - case AssetPathType.ORIGINAL: { - await this.assetRepository.update({ id, originalPath: pathValue }); - break; - } - - case AssetPathType.SIDECAR: { - await this.assetRepository.update({ id, sidecarPath: pathValue }); - break; - } - - case PersonPathType.FACE: { - await this.personRepository.update({ id, thumbnailPath: pathValue }); - break; - } - - case UserPathType.PROFILE: { - await this.userRepository.update(id, { profileImagePath: pathValue }); - break; - } - } - } - } - - private fullPath(filename: string) { - return resolve(filename); - } - - async getFileReport() { - const hasFile = (items: Set, filename: string) => items.has(filename) || items.has(this.fullPath(filename)); - const crawl = async (folder: StorageFolder) => - new Set( - await this.storageRepository.crawl({ - includeHidden: true, - pathsToCrawl: [StorageCore.getBaseFolder(folder)], - }), - ); - - const uploadFiles = await crawl(StorageFolder.UPLOAD); - const libraryFiles = await crawl(StorageFolder.LIBRARY); - const thumbFiles = await crawl(StorageFolder.THUMBNAILS); - const videoFiles = await crawl(StorageFolder.ENCODED_VIDEO); - const profileFiles = await crawl(StorageFolder.PROFILE); - const allFiles = new Set(); - for (const list of [libraryFiles, thumbFiles, videoFiles, profileFiles, uploadFiles]) { - for (const item of list) { - allFiles.add(item); - } - } - - const track = (filename: string | null | undefined) => { - if (!filename) { - return; - } - allFiles.delete(filename); - allFiles.delete(this.fullPath(filename)); - }; - - this.logger.log( - `Found ${libraryFiles.size} original files, ${thumbFiles.size} thumbnails, ${videoFiles.size} encoded videos, ${profileFiles.size} profile files`, - ); - const pagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (options) => - this.assetRepository.getAll(options, { withDeleted: true, withArchived: true }), - ); - - let assetCount = 0; - - const orphans: FileReportItemDto[] = []; - for await (const assets of pagination) { - assetCount += assets.length; - for (const { id, files, originalPath, encodedVideoPath, isExternal, checksum } of assets) { - const { fullsizeFile, previewFile, thumbnailFile } = getAssetFiles(files); - for (const file of [ - originalPath, - fullsizeFile?.path, - previewFile?.path, - encodedVideoPath, - thumbnailFile?.path, - ]) { - track(file); - } - - const entity = { entityId: id, entityType: PathEntityType.ASSET, checksum: checksum.toString('base64') }; - if ( - originalPath && - !hasFile(libraryFiles, originalPath) && - !hasFile(uploadFiles, originalPath) && - // Android motion assets - !hasFile(videoFiles, originalPath) && - // ignore external library assets - !isExternal - ) { - orphans.push({ ...entity, pathType: AssetPathType.ORIGINAL, pathValue: originalPath }); - } - if (previewFile && !hasFile(thumbFiles, previewFile.path)) { - orphans.push({ ...entity, pathType: AssetPathType.PREVIEW, pathValue: previewFile.path }); - } - if (thumbnailFile && !hasFile(thumbFiles, thumbnailFile.path)) { - orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: thumbnailFile.path }); - } - if (encodedVideoPath && !hasFile(videoFiles, encodedVideoPath)) { - orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: encodedVideoPath }); - } - } - } - - const users = await this.userRepository.getList(); - for (const { id, profileImagePath } of users) { - track(profileImagePath); - - const entity = { entityId: id, entityType: PathEntityType.USER }; - if (profileImagePath && !hasFile(profileFiles, profileImagePath)) { - orphans.push({ ...entity, pathType: UserPathType.PROFILE, pathValue: profileImagePath }); - } - } - - let peopleCount = 0; - for await (const { id, thumbnailPath } of this.personRepository.getAll()) { - track(thumbnailPath); - const entity = { entityId: id, entityType: PathEntityType.PERSON }; - if (thumbnailPath && !hasFile(thumbFiles, thumbnailPath)) { - orphans.push({ ...entity, pathType: PersonPathType.FACE, pathValue: thumbnailPath }); - } - - if (peopleCount === JOBS_ASSET_PAGINATION_SIZE) { - this.logger.log(`Found ${assetCount} assets, ${users.length} users, ${peopleCount} people`); - peopleCount = 0; - } - } - - this.logger.log(`Found ${assetCount} assets, ${users.length} users, ${peopleCount} people`); - - const extras: string[] = []; - for (const file of allFiles) { - extras.push(file); - } - - // send as absolute paths - for (const orphan of orphans) { - orphan.pathValue = this.fullPath(orphan.pathValue); - } - - return { orphans, extras }; - } } diff --git a/web/src/routes/admin/repair/+page.svelte b/web/src/routes/admin/repair/+page.svelte deleted file mode 100644 index 2d8ceca4da0..00000000000 --- a/web/src/routes/admin/repair/+page.svelte +++ /dev/null @@ -1,358 +0,0 @@ - - - - {#snippet buttons()} - - - - - - - {/snippet} -
-
- {#if matches.length + extras.length + orphans.length === 0} -
- -
- {:else} -
- - - - - - - - {#each matches as match (match.extra.filename)} - handleSplit(match)} - > - - - - {/each} - -
-
-

- {$t('matches').toUpperCase()} - {matches.length > 0 ? `(${matches.length.toLocaleString($locale)})` : ''} -

-

{$t('admin.these_files_matched_by_checksum')}

-
-
- {match.orphan.pathValue} => - {match.extra.filename} - - ({match.orphan.entityType}/{match.orphan.pathType}) -
- - - - - - - - - {#each orphans as orphan, index (index)} - - - - - - {/each} - -
-
-

- {$t('admin.offline_paths').toUpperCase()} - {orphans.length > 0 ? `(${orphans.length.toLocaleString($locale)})` : ''} -

-

- {$t('admin.offline_paths_description')} -

-
-
copyToClipboard(orphan.pathValue)}> - {}} /> - - {orphan.pathValue} - - ({orphan.entityType}) -
- - - - - - - - - {#each extras as extra (extra.filename)} - handleCheckOne(extra.filename)} - title={extra.filename} - > - - - - {/each} - -
-
-

- {$t('admin.untracked_files').toUpperCase()} - {extras.length > 0 ? `(${extras.length.toLocaleString($locale)})` : ''} -

-

- {$t('admin.untracked_files_description')} -

-
-
copyToClipboard(extra.filename)}> - {}} /> - - {extra.filename} - - {#if extra.checksum} - [sha1:{extra.checksum}] - {/if} - -
-
- {/if} -
-
-
diff --git a/web/src/routes/admin/repair/+page.ts b/web/src/routes/admin/repair/+page.ts deleted file mode 100644 index 9e52abb5734..00000000000 --- a/web/src/routes/admin/repair/+page.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { authenticate } from '$lib/utils/auth'; -import { getFormatter } from '$lib/utils/i18n'; -import { getAuditFiles } from '@immich/sdk'; -import type { PageLoad } from './$types'; - -export const load = (async () => { - await authenticate({ admin: true }); - const { orphans, extras } = await getAuditFiles(); - const $t = await getFormatter(); - - return { - orphans, - extras, - meta: { - title: $t('repair'), - }, - }; -}) satisfies PageLoad;