diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 5305f02d32..4b33352032 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -419,6 +419,7 @@ Class | Method | HTTP request | Description - [MaintenanceListBackupsResponseDto](doc//MaintenanceListBackupsResponseDto.md) - [MaintenanceLoginDto](doc//MaintenanceLoginDto.md) - [MaintenanceStatusResponseDto](doc//MaintenanceStatusResponseDto.md) + - [MaintenanceStorageFolderIntegrityDto](doc//MaintenanceStorageFolderIntegrityDto.md) - [ManualJobName](doc//ManualJobName.md) - [MapMarkerResponseDto](doc//MapMarkerResponseDto.md) - [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md) @@ -525,6 +526,7 @@ Class | Method | HTTP request | Description - [StackResponseDto](doc//StackResponseDto.md) - [StackUpdateDto](doc//StackUpdateDto.md) - [StatisticsSearchDto](doc//StatisticsSearchDto.md) + - [StorageFolder](doc//StorageFolder.md) - [SyncAckDeleteDto](doc//SyncAckDeleteDto.md) - [SyncAckDto](doc//SyncAckDto.md) - [SyncAckSetDto](doc//SyncAckSetDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 7361f0f7e3..606c2a0870 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -170,6 +170,7 @@ part 'model/maintenance_integrity_response_dto.dart'; part 'model/maintenance_list_backups_response_dto.dart'; part 'model/maintenance_login_dto.dart'; part 'model/maintenance_status_response_dto.dart'; +part 'model/maintenance_storage_folder_integrity_dto.dart'; part 'model/manual_job_name.dart'; part 'model/map_marker_response_dto.dart'; part 'model/map_reverse_geocode_response_dto.dart'; @@ -276,6 +277,7 @@ part 'model/stack_create_dto.dart'; part 'model/stack_response_dto.dart'; part 'model/stack_update_dto.dart'; part 'model/statistics_search_dto.dart'; +part 'model/storage_folder.dart'; part 'model/sync_ack_delete_dto.dart'; part 'model/sync_ack_dto.dart'; part 'model/sync_ack_set_dto.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 19f835b6a5..68abddd201 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -390,6 +390,8 @@ class ApiClient { return MaintenanceLoginDto.fromJson(value); case 'MaintenanceStatusResponseDto': return MaintenanceStatusResponseDto.fromJson(value); + case 'MaintenanceStorageFolderIntegrityDto': + return MaintenanceStorageFolderIntegrityDto.fromJson(value); case 'ManualJobName': return ManualJobNameTypeTransformer().decode(value); case 'MapMarkerResponseDto': @@ -602,6 +604,8 @@ class ApiClient { return StackUpdateDto.fromJson(value); case 'StatisticsSearchDto': return StatisticsSearchDto.fromJson(value); + case 'StorageFolder': + return StorageFolderTypeTransformer().decode(value); case 'SyncAckDeleteDto': return SyncAckDeleteDto.fromJson(value); case 'SyncAckDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index e6d39d5eb7..a5f9851443 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -151,6 +151,9 @@ String parameterToString(dynamic value) { if (value is SourceType) { return SourceTypeTypeTransformer().encode(value).toString(); } + if (value is StorageFolder) { + return StorageFolderTypeTransformer().encode(value).toString(); + } if (value is SyncEntityType) { return SyncEntityTypeTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/maintenance_integrity_response_dto.dart b/mobile/openapi/lib/model/maintenance_integrity_response_dto.dart index fc07764657..1c47a6989b 100644 --- a/mobile/openapi/lib/model/maintenance_integrity_response_dto.dart +++ b/mobile/openapi/lib/model/maintenance_integrity_response_dto.dart @@ -13,32 +13,26 @@ part of openapi.api; class MaintenanceIntegrityResponseDto { /// Returns a new [MaintenanceIntegrityResponseDto] instance. MaintenanceIntegrityResponseDto({ - required this.storageHeuristics, - required this.storageIntegrity, + this.storage = const [], }); - Object storageHeuristics; - - Object storageIntegrity; + List storage; @override bool operator ==(Object other) => identical(this, other) || other is MaintenanceIntegrityResponseDto && - other.storageHeuristics == storageHeuristics && - other.storageIntegrity == storageIntegrity; + _deepEquality.equals(other.storage, storage); @override int get hashCode => // ignore: unnecessary_parenthesis - (storageHeuristics.hashCode) + - (storageIntegrity.hashCode); + (storage.hashCode); @override - String toString() => 'MaintenanceIntegrityResponseDto[storageHeuristics=$storageHeuristics, storageIntegrity=$storageIntegrity]'; + String toString() => 'MaintenanceIntegrityResponseDto[storage=$storage]'; Map toJson() { final json = {}; - json[r'storageHeuristics'] = this.storageHeuristics; - json[r'storageIntegrity'] = this.storageIntegrity; + json[r'storage'] = this.storage; return json; } @@ -51,8 +45,7 @@ class MaintenanceIntegrityResponseDto { final json = value.cast(); return MaintenanceIntegrityResponseDto( - storageHeuristics: mapValueOfType(json, r'storageHeuristics')!, - storageIntegrity: mapValueOfType(json, r'storageIntegrity')!, + storage: MaintenanceStorageFolderIntegrityDto.listFromJson(json[r'storage']), ); } return null; @@ -100,8 +93,7 @@ class MaintenanceIntegrityResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'storageHeuristics', - 'storageIntegrity', + 'storage', }; } diff --git a/mobile/openapi/lib/model/maintenance_storage_folder_integrity_dto.dart b/mobile/openapi/lib/model/maintenance_storage_folder_integrity_dto.dart new file mode 100644 index 0000000000..364e096e5b --- /dev/null +++ b/mobile/openapi/lib/model/maintenance_storage_folder_integrity_dto.dart @@ -0,0 +1,123 @@ +// +// 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 MaintenanceStorageFolderIntegrityDto { + /// Returns a new [MaintenanceStorageFolderIntegrityDto] instance. + MaintenanceStorageFolderIntegrityDto({ + required this.files, + required this.folder, + required this.readable, + required this.writable, + }); + + num files; + + StorageFolder folder; + + bool readable; + + bool writable; + + @override + bool operator ==(Object other) => identical(this, other) || other is MaintenanceStorageFolderIntegrityDto && + other.files == files && + other.folder == folder && + other.readable == readable && + other.writable == writable; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (files.hashCode) + + (folder.hashCode) + + (readable.hashCode) + + (writable.hashCode); + + @override + String toString() => 'MaintenanceStorageFolderIntegrityDto[files=$files, folder=$folder, readable=$readable, writable=$writable]'; + + Map toJson() { + final json = {}; + json[r'files'] = this.files; + json[r'folder'] = this.folder; + json[r'readable'] = this.readable; + json[r'writable'] = this.writable; + return json; + } + + /// Returns a new [MaintenanceStorageFolderIntegrityDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MaintenanceStorageFolderIntegrityDto? fromJson(dynamic value) { + upgradeDto(value, "MaintenanceStorageFolderIntegrityDto"); + if (value is Map) { + final json = value.cast(); + + return MaintenanceStorageFolderIntegrityDto( + files: num.parse('${json[r'files']}'), + folder: StorageFolder.fromJson(json[r'folder'])!, + readable: mapValueOfType(json, r'readable')!, + writable: mapValueOfType(json, r'writable')!, + ); + } + 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 = MaintenanceStorageFolderIntegrityDto.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 = MaintenanceStorageFolderIntegrityDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MaintenanceStorageFolderIntegrityDto-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] = MaintenanceStorageFolderIntegrityDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'files', + 'folder', + 'readable', + 'writable', + }; +} + diff --git a/mobile/openapi/lib/model/storage_folder.dart b/mobile/openapi/lib/model/storage_folder.dart new file mode 100644 index 0000000000..df66bc187a --- /dev/null +++ b/mobile/openapi/lib/model/storage_folder.dart @@ -0,0 +1,97 @@ +// +// 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 StorageFolder { + /// Instantiate a new enum with the provided [value]. + const StorageFolder._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const encodedVideo = StorageFolder._(r'encoded-video'); + static const library_ = StorageFolder._(r'library'); + static const upload = StorageFolder._(r'upload'); + static const profile = StorageFolder._(r'profile'); + static const thumbs = StorageFolder._(r'thumbs'); + static const backups = StorageFolder._(r'backups'); + + /// List of all possible values in this [enum][StorageFolder]. + static const values = [ + encodedVideo, + library_, + upload, + profile, + thumbs, + backups, + ]; + + static StorageFolder? fromJson(dynamic value) => StorageFolderTypeTransformer().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 = StorageFolder.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [StorageFolder] to String, +/// and [decode] dynamic data back to [StorageFolder]. +class StorageFolderTypeTransformer { + factory StorageFolderTypeTransformer() => _instance ??= const StorageFolderTypeTransformer._(); + + const StorageFolderTypeTransformer._(); + + String encode(StorageFolder data) => data.value; + + /// Decodes a [dynamic value][data] to a StorageFolder. + /// + /// 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. + StorageFolder? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'encoded-video': return StorageFolder.encodedVideo; + case r'library': return StorageFolder.library_; + case r'upload': return StorageFolder.upload; + case r'profile': return StorageFolder.profile; + case r'thumbs': return StorageFolder.thumbs; + case r'backups': return StorageFolder.backups; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [StorageFolderTypeTransformer] instance. + static StorageFolderTypeTransformer? _instance; +} + diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index e7901bce3f..e00bd2d8e7 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -16843,16 +16843,15 @@ }, "MaintenanceIntegrityResponseDto": { "properties": { - "storageHeuristics": { - "type": "object" - }, - "storageIntegrity": { - "type": "object" + "storage": { + "items": { + "$ref": "#/components/schemas/MaintenanceStorageFolderIntegrityDto" + }, + "type": "array" } }, "required": [ - "storageHeuristics", - "storageIntegrity" + "storage" ], "type": "object" }, @@ -16902,6 +16901,33 @@ ], "type": "object" }, + "MaintenanceStorageFolderIntegrityDto": { + "properties": { + "files": { + "type": "number" + }, + "folder": { + "allOf": [ + { + "$ref": "#/components/schemas/StorageFolder" + } + ] + }, + "readable": { + "type": "boolean" + }, + "writable": { + "type": "boolean" + } + }, + "required": [ + "files", + "folder", + "readable", + "writable" + ], + "type": "object" + }, "MaintenanceUploadBackupDto": { "properties": { "file": { @@ -20070,6 +20096,17 @@ }, "type": "object" }, + "StorageFolder": { + "enum": [ + "encoded-video", + "library", + "upload", + "profile", + "thumbs", + "backups" + ], + "type": "string" + }, "SyncAckDeleteDto": { "properties": { "types": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 2ade47e79e..eb3ddeba38 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -50,9 +50,14 @@ export type MaintenanceListBackupsResponseDto = { export type MaintenanceUploadBackupDto = { file?: Blob; }; +export type MaintenanceStorageFolderIntegrityDto = { + files: number; + folder: StorageFolder; + readable: boolean; + writable: boolean; +}; export type MaintenanceIntegrityResponseDto = { - storageHeuristics: object; - storageIntegrity: object; + storage: MaintenanceStorageFolderIntegrityDto[]; }; export type MaintenanceLoginDto = { token?: string; @@ -5151,6 +5156,14 @@ export enum MaintenanceAction { End = "end", RestoreDatabase = "restore_database" } +export enum StorageFolder { + EncodedVideo = "encoded-video", + Library = "library", + Upload = "upload", + Profile = "profile", + Thumbs = "thumbs", + Backups = "backups" +} export enum NotificationLevel { Success = "success", Error = "error", diff --git a/server/src/dtos/maintenance.dto.ts b/server/src/dtos/maintenance.dto.ts index 2c87cf69b0..3b36148709 100644 --- a/server/src/dtos/maintenance.dto.ts +++ b/server/src/dtos/maintenance.dto.ts @@ -28,20 +28,16 @@ export class MaintenanceStatusResponseDto { error?: string; } +export class MaintenanceStorageFolderIntegrityDto { + @ValidateEnum({ enum: StorageFolder, name: 'StorageFolder' }) + folder!: StorageFolder; + readable!: boolean; + writable!: boolean; + files!: number; +} + export class MaintenanceIntegrityResponseDto { - storageIntegrity!: Record< - StorageFolder, - { - readable: boolean; - writable: boolean; - } - >; - storageHeuristics!: Record< - StorageFolder, - { - files: number; - } - >; + storage!: MaintenanceStorageFolderIntegrityDto[]; } export class MaintenanceListBackupsResponseDto { diff --git a/server/src/utils/maintenance.ts b/server/src/utils/maintenance.ts index 869214ade5..5ee701eb0b 100644 --- a/server/src/utils/maintenance.ts +++ b/server/src/utils/maintenance.ts @@ -79,44 +79,36 @@ export function generateMaintenanceSecret(): string { export async function integrityCheck(storageRepository: StorageRepository): Promise { return { - storageIntegrity: Object.fromEntries( - await Promise.all( - Object.values(StorageFolder).map(async (folder) => { - const path = join(StorageCore.getBaseFolder(folder), '.immich'); + storage: await Promise.all( + Object.values(StorageFolder).map(async (folder) => { + const path = StorageCore.getBaseFolder(folder); + const files = await storageRepository.readdir(path); + const fn = join(StorageCore.getBaseFolder(folder), '.immich'); + + let readable = false, + writable = false; + + try { + await storageRepository.readFile(fn); + readable = true; try { - await storageRepository.readFile(path); - - try { - await storageRepository.overwriteFile(path, Buffer.from(`${Date.now()}`)); - return [folder, { readable: true, writable: true }]; - } catch { - return [folder, { readable: true, writable: false }]; - } + await storageRepository.overwriteFile(fn, Buffer.from(`${Date.now()}`)); + writable = true; } catch { - return [folder, { readable: false, writable: false }]; + // no-op } - }), - ), - ), - storageHeuristics: Object.fromEntries( - await Promise.all( - Object.values(StorageFolder).map(async (folder) => { - const path = StorageCore.getBaseFolder(folder); - const files = await storageRepository.readdir(path); + } catch { + // no-op + } - try { - return [ - folder, - { - files: files.filter((fn) => fn !== '.immich').length, - }, - ]; - } catch { - return [folder, { files: 0 }]; - } - }), - ), + return { + folder, + readable, + writable, + files: files.filter((fn) => fn !== '.immich').length, + }; + }), ), }; } diff --git a/web/src/lib/components/maintenance/MaintenanceRestoreFlow.svelte b/web/src/lib/components/maintenance/MaintenanceRestoreFlow.svelte index ce5a43dbf1..63769ce90f 100644 --- a/web/src/lib/components/maintenance/MaintenanceRestoreFlow.svelte +++ b/web/src/lib/components/maintenance/MaintenanceRestoreFlow.svelte @@ -39,7 +39,7 @@ {#if integrity} - {#each Object.entries(integrity.storageIntegrity) as [folder, { readable, writable }] (folder)} + {#each integrity.storage as { folder, readable, writable } (folder)} {/each} - {#each Object.entries(integrity.storageHeuristics) as [folder, { files }] (folder)} + {#each integrity.storage as { folder, files } (folder)} {#if folder !== 'backups'}