refactor: better typings for integrity API

This commit is contained in:
izzy
2025-11-24 14:53:52 +00:00
parent f84bdc14d5
commit e2ca0c6f67
12 changed files with 334 additions and 73 deletions

View File

@@ -419,6 +419,7 @@ Class | Method | HTTP request | Description
- [MaintenanceListBackupsResponseDto](doc//MaintenanceListBackupsResponseDto.md) - [MaintenanceListBackupsResponseDto](doc//MaintenanceListBackupsResponseDto.md)
- [MaintenanceLoginDto](doc//MaintenanceLoginDto.md) - [MaintenanceLoginDto](doc//MaintenanceLoginDto.md)
- [MaintenanceStatusResponseDto](doc//MaintenanceStatusResponseDto.md) - [MaintenanceStatusResponseDto](doc//MaintenanceStatusResponseDto.md)
- [MaintenanceStorageFolderIntegrityDto](doc//MaintenanceStorageFolderIntegrityDto.md)
- [ManualJobName](doc//ManualJobName.md) - [ManualJobName](doc//ManualJobName.md)
- [MapMarkerResponseDto](doc//MapMarkerResponseDto.md) - [MapMarkerResponseDto](doc//MapMarkerResponseDto.md)
- [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md) - [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md)
@@ -525,6 +526,7 @@ Class | Method | HTTP request | Description
- [StackResponseDto](doc//StackResponseDto.md) - [StackResponseDto](doc//StackResponseDto.md)
- [StackUpdateDto](doc//StackUpdateDto.md) - [StackUpdateDto](doc//StackUpdateDto.md)
- [StatisticsSearchDto](doc//StatisticsSearchDto.md) - [StatisticsSearchDto](doc//StatisticsSearchDto.md)
- [StorageFolder](doc//StorageFolder.md)
- [SyncAckDeleteDto](doc//SyncAckDeleteDto.md) - [SyncAckDeleteDto](doc//SyncAckDeleteDto.md)
- [SyncAckDto](doc//SyncAckDto.md) - [SyncAckDto](doc//SyncAckDto.md)
- [SyncAckSetDto](doc//SyncAckSetDto.md) - [SyncAckSetDto](doc//SyncAckSetDto.md)

View File

@@ -170,6 +170,7 @@ part 'model/maintenance_integrity_response_dto.dart';
part 'model/maintenance_list_backups_response_dto.dart'; part 'model/maintenance_list_backups_response_dto.dart';
part 'model/maintenance_login_dto.dart'; part 'model/maintenance_login_dto.dart';
part 'model/maintenance_status_response_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/manual_job_name.dart';
part 'model/map_marker_response_dto.dart'; part 'model/map_marker_response_dto.dart';
part 'model/map_reverse_geocode_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_response_dto.dart';
part 'model/stack_update_dto.dart'; part 'model/stack_update_dto.dart';
part 'model/statistics_search_dto.dart'; part 'model/statistics_search_dto.dart';
part 'model/storage_folder.dart';
part 'model/sync_ack_delete_dto.dart'; part 'model/sync_ack_delete_dto.dart';
part 'model/sync_ack_dto.dart'; part 'model/sync_ack_dto.dart';
part 'model/sync_ack_set_dto.dart'; part 'model/sync_ack_set_dto.dart';

View File

@@ -390,6 +390,8 @@ class ApiClient {
return MaintenanceLoginDto.fromJson(value); return MaintenanceLoginDto.fromJson(value);
case 'MaintenanceStatusResponseDto': case 'MaintenanceStatusResponseDto':
return MaintenanceStatusResponseDto.fromJson(value); return MaintenanceStatusResponseDto.fromJson(value);
case 'MaintenanceStorageFolderIntegrityDto':
return MaintenanceStorageFolderIntegrityDto.fromJson(value);
case 'ManualJobName': case 'ManualJobName':
return ManualJobNameTypeTransformer().decode(value); return ManualJobNameTypeTransformer().decode(value);
case 'MapMarkerResponseDto': case 'MapMarkerResponseDto':
@@ -602,6 +604,8 @@ class ApiClient {
return StackUpdateDto.fromJson(value); return StackUpdateDto.fromJson(value);
case 'StatisticsSearchDto': case 'StatisticsSearchDto':
return StatisticsSearchDto.fromJson(value); return StatisticsSearchDto.fromJson(value);
case 'StorageFolder':
return StorageFolderTypeTransformer().decode(value);
case 'SyncAckDeleteDto': case 'SyncAckDeleteDto':
return SyncAckDeleteDto.fromJson(value); return SyncAckDeleteDto.fromJson(value);
case 'SyncAckDto': case 'SyncAckDto':

View File

@@ -151,6 +151,9 @@ String parameterToString(dynamic value) {
if (value is SourceType) { if (value is SourceType) {
return SourceTypeTypeTransformer().encode(value).toString(); return SourceTypeTypeTransformer().encode(value).toString();
} }
if (value is StorageFolder) {
return StorageFolderTypeTransformer().encode(value).toString();
}
if (value is SyncEntityType) { if (value is SyncEntityType) {
return SyncEntityTypeTypeTransformer().encode(value).toString(); return SyncEntityTypeTypeTransformer().encode(value).toString();
} }

View File

@@ -13,32 +13,26 @@ part of openapi.api;
class MaintenanceIntegrityResponseDto { class MaintenanceIntegrityResponseDto {
/// Returns a new [MaintenanceIntegrityResponseDto] instance. /// Returns a new [MaintenanceIntegrityResponseDto] instance.
MaintenanceIntegrityResponseDto({ MaintenanceIntegrityResponseDto({
required this.storageHeuristics, this.storage = const [],
required this.storageIntegrity,
}); });
Object storageHeuristics; List<MaintenanceStorageFolderIntegrityDto> storage;
Object storageIntegrity;
@override @override
bool operator ==(Object other) => identical(this, other) || other is MaintenanceIntegrityResponseDto && bool operator ==(Object other) => identical(this, other) || other is MaintenanceIntegrityResponseDto &&
other.storageHeuristics == storageHeuristics && _deepEquality.equals(other.storage, storage);
other.storageIntegrity == storageIntegrity;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(storageHeuristics.hashCode) + (storage.hashCode);
(storageIntegrity.hashCode);
@override @override
String toString() => 'MaintenanceIntegrityResponseDto[storageHeuristics=$storageHeuristics, storageIntegrity=$storageIntegrity]'; String toString() => 'MaintenanceIntegrityResponseDto[storage=$storage]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'storageHeuristics'] = this.storageHeuristics; json[r'storage'] = this.storage;
json[r'storageIntegrity'] = this.storageIntegrity;
return json; return json;
} }
@@ -51,8 +45,7 @@ class MaintenanceIntegrityResponseDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return MaintenanceIntegrityResponseDto( return MaintenanceIntegrityResponseDto(
storageHeuristics: mapValueOfType<Object>(json, r'storageHeuristics')!, storage: MaintenanceStorageFolderIntegrityDto.listFromJson(json[r'storage']),
storageIntegrity: mapValueOfType<Object>(json, r'storageIntegrity')!,
); );
} }
return null; return null;
@@ -100,8 +93,7 @@ class MaintenanceIntegrityResponseDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'storageHeuristics', 'storage',
'storageIntegrity',
}; };
} }

View File

@@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
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<String, dynamic>();
return MaintenanceStorageFolderIntegrityDto(
files: num.parse('${json[r'files']}'),
folder: StorageFolder.fromJson(json[r'folder'])!,
readable: mapValueOfType<bool>(json, r'readable')!,
writable: mapValueOfType<bool>(json, r'writable')!,
);
}
return null;
}
static List<MaintenanceStorageFolderIntegrityDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <MaintenanceStorageFolderIntegrityDto>[];
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<String, MaintenanceStorageFolderIntegrityDto> mapFromJson(dynamic json) {
final map = <String, MaintenanceStorageFolderIntegrityDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // 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<String, List<MaintenanceStorageFolderIntegrityDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<MaintenanceStorageFolderIntegrityDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
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 = <String>{
'files',
'folder',
'readable',
'writable',
};
}

View File

@@ -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 = <StorageFolder>[
encodedVideo,
library_,
upload,
profile,
thumbs,
backups,
];
static StorageFolder? fromJson(dynamic value) => StorageFolderTypeTransformer().decode(value);
static List<StorageFolder> listFromJson(dynamic json, {bool growable = false,}) {
final result = <StorageFolder>[];
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;
}

View File

@@ -16843,16 +16843,15 @@
}, },
"MaintenanceIntegrityResponseDto": { "MaintenanceIntegrityResponseDto": {
"properties": { "properties": {
"storageHeuristics": { "storage": {
"type": "object" "items": {
"$ref": "#/components/schemas/MaintenanceStorageFolderIntegrityDto"
}, },
"storageIntegrity": { "type": "array"
"type": "object"
} }
}, },
"required": [ "required": [
"storageHeuristics", "storage"
"storageIntegrity"
], ],
"type": "object" "type": "object"
}, },
@@ -16902,6 +16901,33 @@
], ],
"type": "object" "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": { "MaintenanceUploadBackupDto": {
"properties": { "properties": {
"file": { "file": {
@@ -20070,6 +20096,17 @@
}, },
"type": "object" "type": "object"
}, },
"StorageFolder": {
"enum": [
"encoded-video",
"library",
"upload",
"profile",
"thumbs",
"backups"
],
"type": "string"
},
"SyncAckDeleteDto": { "SyncAckDeleteDto": {
"properties": { "properties": {
"types": { "types": {

View File

@@ -50,9 +50,14 @@ export type MaintenanceListBackupsResponseDto = {
export type MaintenanceUploadBackupDto = { export type MaintenanceUploadBackupDto = {
file?: Blob; file?: Blob;
}; };
export type MaintenanceStorageFolderIntegrityDto = {
files: number;
folder: StorageFolder;
readable: boolean;
writable: boolean;
};
export type MaintenanceIntegrityResponseDto = { export type MaintenanceIntegrityResponseDto = {
storageHeuristics: object; storage: MaintenanceStorageFolderIntegrityDto[];
storageIntegrity: object;
}; };
export type MaintenanceLoginDto = { export type MaintenanceLoginDto = {
token?: string; token?: string;
@@ -5151,6 +5156,14 @@ export enum MaintenanceAction {
End = "end", End = "end",
RestoreDatabase = "restore_database" RestoreDatabase = "restore_database"
} }
export enum StorageFolder {
EncodedVideo = "encoded-video",
Library = "library",
Upload = "upload",
Profile = "profile",
Thumbs = "thumbs",
Backups = "backups"
}
export enum NotificationLevel { export enum NotificationLevel {
Success = "success", Success = "success",
Error = "error", Error = "error",

View File

@@ -28,20 +28,16 @@ export class MaintenanceStatusResponseDto {
error?: string; error?: string;
} }
export class MaintenanceStorageFolderIntegrityDto {
@ValidateEnum({ enum: StorageFolder, name: 'StorageFolder' })
folder!: StorageFolder;
readable!: boolean;
writable!: boolean;
files!: number;
}
export class MaintenanceIntegrityResponseDto { export class MaintenanceIntegrityResponseDto {
storageIntegrity!: Record< storage!: MaintenanceStorageFolderIntegrityDto[];
StorageFolder,
{
readable: boolean;
writable: boolean;
}
>;
storageHeuristics!: Record<
StorageFolder,
{
files: number;
}
>;
} }
export class MaintenanceListBackupsResponseDto { export class MaintenanceListBackupsResponseDto {

View File

@@ -79,44 +79,36 @@ export function generateMaintenanceSecret(): string {
export async function integrityCheck(storageRepository: StorageRepository): Promise<MaintenanceIntegrityResponseDto> { export async function integrityCheck(storageRepository: StorageRepository): Promise<MaintenanceIntegrityResponseDto> {
return { return {
storageIntegrity: Object.fromEntries( storage: await Promise.all(
await Promise.all(
Object.values(StorageFolder).map(async (folder) => {
const path = join(StorageCore.getBaseFolder(folder), '.immich');
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 }];
}
} catch {
return [folder, { readable: false, writable: false }];
}
}),
),
),
storageHeuristics: Object.fromEntries(
await Promise.all(
Object.values(StorageFolder).map(async (folder) => { Object.values(StorageFolder).map(async (folder) => {
const path = StorageCore.getBaseFolder(folder); const path = StorageCore.getBaseFolder(folder);
const files = await storageRepository.readdir(path); const files = await storageRepository.readdir(path);
const fn = join(StorageCore.getBaseFolder(folder), '.immich');
let readable = false,
writable = false;
try { try {
return [ await storageRepository.readFile(fn);
folder, readable = true;
{
files: files.filter((fn) => fn !== '.immich').length, try {
}, await storageRepository.overwriteFile(fn, Buffer.from(`${Date.now()}`));
]; writable = true;
} catch { } catch {
return [folder, { files: 0 }]; // no-op
} }
} catch {
// no-op
}
return {
folder,
readable,
writable,
files: files.filter((fn) => fn !== '.immich').length,
};
}), }),
), ),
),
}; };
} }

View File

@@ -39,7 +39,7 @@
<CardBody> <CardBody>
<Stack> <Stack>
{#if integrity} {#if integrity}
{#each Object.entries(integrity.storageIntegrity) as [folder, { readable, writable }] (folder)} {#each integrity.storage as { folder, readable, writable } (folder)}
<HStack> <HStack>
<Icon <Icon
icon={writable ? mdiCheck : mdiClose} icon={writable ? mdiCheck : mdiClose}
@@ -54,7 +54,7 @@
> >
</HStack> </HStack>
{/each} {/each}
{#each Object.entries(integrity.storageHeuristics) as [folder, { files }] (folder)} {#each integrity.storage as { folder, files } (folder)}
{#if folder !== 'backups'} {#if folder !== 'backups'}
<HStack class="items-start"> <HStack class="items-start">
<Icon <Icon