diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 391c7c3759..3b424e8d79 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -161,6 +161,7 @@ Class | Method | HTTP request | Description *LibrariesApi* | [**scanLibrary**](doc//LibrariesApi.md#scanlibrary) | **POST** /libraries/{id}/scan | Scan a library *LibrariesApi* | [**updateLibrary**](doc//LibrariesApi.md#updatelibrary) | **PUT** /libraries/{id} | Update a library *LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate | Validate library settings +*MaintenanceAdminApi* | [**getIntegrityReport**](doc//MaintenanceAdminApi.md#getintegrityreport) | **GET** /admin/maintenance | Get integrity report *MaintenanceAdminApi* | [**maintenanceLogin**](doc//MaintenanceAdminApi.md#maintenancelogin) | **POST** /admin/maintenance/login | Log into maintenance mode *MaintenanceAdminApi* | [**setMaintenanceMode**](doc//MaintenanceAdminApi.md#setmaintenancemode) | **POST** /admin/maintenance | Set maintenance mode *MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers | Retrieve map markers @@ -416,6 +417,8 @@ Class | Method | HTTP request | Description - [MachineLearningAvailabilityChecksDto](doc//MachineLearningAvailabilityChecksDto.md) - [MaintenanceAction](doc//MaintenanceAction.md) - [MaintenanceAuthDto](doc//MaintenanceAuthDto.md) + - [MaintenanceIntegrityReportDto](doc//MaintenanceIntegrityReportDto.md) + - [MaintenanceIntegrityReportResponseDto](doc//MaintenanceIntegrityReportResponseDto.md) - [MaintenanceLoginDto](doc//MaintenanceLoginDto.md) - [ManualJobName](doc//ManualJobName.md) - [MapMarkerResponseDto](doc//MapMarkerResponseDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 641000daf7..ddacd27a3b 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -168,6 +168,8 @@ part 'model/logout_response_dto.dart'; part 'model/machine_learning_availability_checks_dto.dart'; part 'model/maintenance_action.dart'; part 'model/maintenance_auth_dto.dart'; +part 'model/maintenance_integrity_report_dto.dart'; +part 'model/maintenance_integrity_report_response_dto.dart'; part 'model/maintenance_login_dto.dart'; part 'model/manual_job_name.dart'; part 'model/map_marker_response_dto.dart'; diff --git a/mobile/openapi/lib/api/maintenance_admin_api.dart b/mobile/openapi/lib/api/maintenance_admin_api.dart index 7e46f96c6e..c0a9aa862f 100644 --- a/mobile/openapi/lib/api/maintenance_admin_api.dart +++ b/mobile/openapi/lib/api/maintenance_admin_api.dart @@ -16,6 +16,54 @@ class MaintenanceAdminApi { final ApiClient apiClient; + /// Get integrity report + /// + /// ... + /// + /// Note: This method returns the HTTP [Response]. + Future getIntegrityReportWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/admin/maintenance'; + + // 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, + ); + } + + /// Get integrity report + /// + /// ... + Future getIntegrityReport() async { + final response = await getIntegrityReportWithHttpInfo(); + 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), 'MaintenanceIntegrityReportResponseDto',) as MaintenanceIntegrityReportResponseDto; + + } + return null; + } + /// Log into maintenance mode /// /// Login with maintenance token or cookie to receive current information and perform further actions. diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index ae2fa85a93..c522db3023 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -384,6 +384,10 @@ class ApiClient { return MaintenanceActionTypeTransformer().decode(value); case 'MaintenanceAuthDto': return MaintenanceAuthDto.fromJson(value); + case 'MaintenanceIntegrityReportDto': + return MaintenanceIntegrityReportDto.fromJson(value); + case 'MaintenanceIntegrityReportResponseDto': + return MaintenanceIntegrityReportResponseDto.fromJson(value); case 'MaintenanceLoginDto': return MaintenanceLoginDto.fromJson(value); case 'ManualJobName': diff --git a/mobile/openapi/lib/model/maintenance_integrity_report_dto.dart b/mobile/openapi/lib/model/maintenance_integrity_report_dto.dart new file mode 100644 index 0000000000..c5e4baad86 --- /dev/null +++ b/mobile/openapi/lib/model/maintenance_integrity_report_dto.dart @@ -0,0 +1,192 @@ +// +// 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 MaintenanceIntegrityReportDto { + /// Returns a new [MaintenanceIntegrityReportDto] instance. + MaintenanceIntegrityReportDto({ + required this.id, + required this.path, + required this.type, + }); + + String id; + + String path; + + MaintenanceIntegrityReportDtoTypeEnum type; + + @override + bool operator ==(Object other) => identical(this, other) || other is MaintenanceIntegrityReportDto && + other.id == id && + other.path == path && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (id.hashCode) + + (path.hashCode) + + (type.hashCode); + + @override + String toString() => 'MaintenanceIntegrityReportDto[id=$id, path=$path, type=$type]'; + + Map toJson() { + final json = {}; + json[r'id'] = this.id; + json[r'path'] = this.path; + json[r'type'] = this.type; + return json; + } + + /// Returns a new [MaintenanceIntegrityReportDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MaintenanceIntegrityReportDto? fromJson(dynamic value) { + upgradeDto(value, "MaintenanceIntegrityReportDto"); + if (value is Map) { + final json = value.cast(); + + return MaintenanceIntegrityReportDto( + id: mapValueOfType(json, r'id')!, + path: mapValueOfType(json, r'path')!, + type: MaintenanceIntegrityReportDtoTypeEnum.fromJson(json[r'type'])!, + ); + } + 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 = MaintenanceIntegrityReportDto.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 = MaintenanceIntegrityReportDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MaintenanceIntegrityReportDto-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] = MaintenanceIntegrityReportDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'id', + 'path', + 'type', + }; +} + + +class MaintenanceIntegrityReportDtoTypeEnum { + /// Instantiate a new enum with the provided [value]. + const MaintenanceIntegrityReportDtoTypeEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const orphanFile = MaintenanceIntegrityReportDtoTypeEnum._(r'orphan_file'); + static const missingFile = MaintenanceIntegrityReportDtoTypeEnum._(r'missing_file'); + static const checksumMismatch = MaintenanceIntegrityReportDtoTypeEnum._(r'checksum_mismatch'); + + /// List of all possible values in this [enum][MaintenanceIntegrityReportDtoTypeEnum]. + static const values = [ + orphanFile, + missingFile, + checksumMismatch, + ]; + + static MaintenanceIntegrityReportDtoTypeEnum? fromJson(dynamic value) => MaintenanceIntegrityReportDtoTypeEnumTypeTransformer().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 = MaintenanceIntegrityReportDtoTypeEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [MaintenanceIntegrityReportDtoTypeEnum] to String, +/// and [decode] dynamic data back to [MaintenanceIntegrityReportDtoTypeEnum]. +class MaintenanceIntegrityReportDtoTypeEnumTypeTransformer { + factory MaintenanceIntegrityReportDtoTypeEnumTypeTransformer() => _instance ??= const MaintenanceIntegrityReportDtoTypeEnumTypeTransformer._(); + + const MaintenanceIntegrityReportDtoTypeEnumTypeTransformer._(); + + String encode(MaintenanceIntegrityReportDtoTypeEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a MaintenanceIntegrityReportDtoTypeEnum. + /// + /// 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. + MaintenanceIntegrityReportDtoTypeEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'orphan_file': return MaintenanceIntegrityReportDtoTypeEnum.orphanFile; + case r'missing_file': return MaintenanceIntegrityReportDtoTypeEnum.missingFile; + case r'checksum_mismatch': return MaintenanceIntegrityReportDtoTypeEnum.checksumMismatch; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [MaintenanceIntegrityReportDtoTypeEnumTypeTransformer] instance. + static MaintenanceIntegrityReportDtoTypeEnumTypeTransformer? _instance; +} + + diff --git a/mobile/openapi/lib/model/maintenance_integrity_report_response_dto.dart b/mobile/openapi/lib/model/maintenance_integrity_report_response_dto.dart new file mode 100644 index 0000000000..7d76a05db6 --- /dev/null +++ b/mobile/openapi/lib/model/maintenance_integrity_report_response_dto.dart @@ -0,0 +1,99 @@ +// +// 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 MaintenanceIntegrityReportResponseDto { + /// Returns a new [MaintenanceIntegrityReportResponseDto] instance. + MaintenanceIntegrityReportResponseDto({ + this.items = const [], + }); + + List items; + + @override + bool operator ==(Object other) => identical(this, other) || other is MaintenanceIntegrityReportResponseDto && + _deepEquality.equals(other.items, items); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (items.hashCode); + + @override + String toString() => 'MaintenanceIntegrityReportResponseDto[items=$items]'; + + Map toJson() { + final json = {}; + json[r'items'] = this.items; + return json; + } + + /// Returns a new [MaintenanceIntegrityReportResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MaintenanceIntegrityReportResponseDto? fromJson(dynamic value) { + upgradeDto(value, "MaintenanceIntegrityReportResponseDto"); + if (value is Map) { + final json = value.cast(); + + return MaintenanceIntegrityReportResponseDto( + items: MaintenanceIntegrityReportDto.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 = MaintenanceIntegrityReportResponseDto.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 = MaintenanceIntegrityReportResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MaintenanceIntegrityReportResponseDto-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] = MaintenanceIntegrityReportResponseDto.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/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 0d87727e8a..8e39950f20 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -16967,6 +16967,9 @@ }, "MaintenanceIntegrityReportDto": { "properties": { + "id": { + "type": "string" + }, "path": { "type": "string" }, @@ -16980,6 +16983,7 @@ } }, "required": [ + "id", "path", "type" ], diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index b33276a90b..d4802f2abc 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -40,6 +40,14 @@ export type ActivityStatisticsResponseDto = { comments: number; likes: number; }; +export type MaintenanceIntegrityReportDto = { + id: string; + path: string; + "type": Type; +}; +export type MaintenanceIntegrityReportResponseDto = { + items: MaintenanceIntegrityReportDto[]; +}; export type SetMaintenanceModeDto = { action: MaintenanceAction; }; @@ -1866,6 +1874,17 @@ export function unlinkAllOAuthAccountsAdmin(opts?: Oazapfts.RequestOpts) { method: "POST" })); } +/** + * Get integrity report + */ +export function getIntegrityReport(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: MaintenanceIntegrityReportResponseDto; + }>("/admin/maintenance", { + ...opts + })); +} /** * Set maintenance mode */ @@ -5154,6 +5173,11 @@ export enum UserAvatarColor { Gray = "gray", Amber = "amber" } +export enum Type { + OrphanFile = "orphan_file", + MissingFile = "missing_file", + ChecksumMismatch = "checksum_mismatch" +} export enum MaintenanceAction { Start = "start", End = "end" diff --git a/server/src/dtos/maintenance.dto.ts b/server/src/dtos/maintenance.dto.ts index 6d7f15f55c..444b843e92 100644 --- a/server/src/dtos/maintenance.dto.ts +++ b/server/src/dtos/maintenance.dto.ts @@ -26,6 +26,7 @@ export class MaintenanceGetIntegrityReportDto { } class MaintenanceIntegrityReportDto { + id!: string; @IsEnum(IntegrityReportType) type!: IntegrityReportType; path!: string; diff --git a/server/src/repositories/integrity-report.repository.ts b/server/src/repositories/integrity-report.repository.ts index af36f051de..e3b9af6457 100644 --- a/server/src/repositories/integrity-report.repository.ts +++ b/server/src/repositories/integrity-report.repository.ts @@ -22,7 +22,7 @@ export class IntegrityReportRepository { return { items: await this.db .selectFrom('integrity_report') - .select(['type', 'path']) + .select(['id', 'type', 'path']) .orderBy('createdAt', 'desc') .execute(), }; diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index 4cd238cb52..7b1ae7ebb3 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -22,6 +22,7 @@ export enum AppRoute { ADMIN_USERS = '/admin/users', ADMIN_LIBRARY_MANAGEMENT = '/admin/library-management', ADMIN_SETTINGS = '/admin/system-settings', + ADMIN_MAINTENANCE_SETTINGS = '/admin/maintenance', ADMIN_STATS = '/admin/server-status', ADMIN_JOBS = '/admin/jobs-status', ADMIN_REPAIR = '/admin/repair', diff --git a/web/src/lib/sidebars/AdminSidebar.svelte b/web/src/lib/sidebars/AdminSidebar.svelte index 2fecaebf49..2418aa393e 100644 --- a/web/src/lib/sidebars/AdminSidebar.svelte +++ b/web/src/lib/sidebars/AdminSidebar.svelte @@ -11,6 +11,7 @@ + diff --git a/web/src/routes/admin/maintenance/+page.svelte b/web/src/routes/admin/maintenance/+page.svelte new file mode 100644 index 0000000000..12a0e4067d --- /dev/null +++ b/web/src/routes/admin/maintenance/+page.svelte @@ -0,0 +1,92 @@ + + + + {#snippet buttons()} + + + + {/snippet} + +
+
+ + + + + + + + + + + + {#each data.integrityReport.items as { id, type, path } (id)} + + + + + + {/each} + +
ReasonFile
+ {#if type === 'orphan_file'} + Orphaned File + {:else if type === 'missing_file'} + Missing File + {:else if type === 'checksum_mismatch'} + Checksum Mismatch + {/if} + {path} +
+
+
+
+
+
diff --git a/web/src/routes/admin/maintenance/+page.ts b/web/src/routes/admin/maintenance/+page.ts new file mode 100644 index 0000000000..65ecbf9a77 --- /dev/null +++ b/web/src/routes/admin/maintenance/+page.ts @@ -0,0 +1,17 @@ +import { authenticate } from '$lib/utils/auth'; +import { getFormatter } from '$lib/utils/i18n'; +import { getIntegrityReport } from '@immich/sdk'; +import type { PageLoad } from './$types'; + +export const load = (async ({ url }) => { + await authenticate(url, { admin: true }); + const integrityReport = await getIntegrityReport(); + const $t = await getFormatter(); + + return { + integrityReport, + meta: { + title: $t('admin.system_settings'), + }, + }; +}) satisfies PageLoad;