You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	feat(mobile) duplicated asset upload handling mechanism (#853)
This commit is contained in:
		| @@ -24,6 +24,7 @@ | ||||
|   "backup_controller_page_backup_selected": "Ausgewählt: ", | ||||
|   "backup_controller_page_backup_sub": "Gesicherte Fotos und Videos", | ||||
|   "backup_controller_page_cancel": "Abbrechen", | ||||
|   "backup_background_service_default_notification": "Suche nach neuen assets…", | ||||
|   "backup_controller_page_created": "Erstellt: {}", | ||||
|   "backup_controller_page_desc_backup": "Aktiviere die Sicherung um Elemente automatisch auf den Server zu laden.", | ||||
|   "backup_controller_page_excluded": "Ausgeschlossen: ", | ||||
| @@ -123,4 +124,4 @@ | ||||
|   "version_announcement_overlay_text_2": "Bitte nehm dir die Zeit und lese das ", | ||||
|   "version_announcement_overlay_text_3": " und achte darauf, dass deine docker-compose und .env Dateien aktuell sind, vor allem wenn du ein System für automatische Updates benutzt (z.B. Watchtower).", | ||||
|   "version_announcement_overlay_title": "Neue Server-Version verfügbar \uD83C\uDF89" | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -25,3 +25,7 @@ const String backgroundBackupInfoBox = "immichBackgroundBackupInfoBox"; // Box | ||||
| const String backupFailedSince = "immichBackupFailedSince"; // Key 1 | ||||
| const String backupRequireWifi = "immichBackupRequireWifi"; // Key 2 | ||||
| const String backupRequireCharging = "immichBackupRequireCharging"; // Key 3 | ||||
|  | ||||
| // Duplicate asset | ||||
| const String duplicatedAssetsBox = "immichDuplicatedAssetsBox"; // Box | ||||
| const String duplicatedAssetsKey = "immichDuplicatedAssetsKey"; // Key 1 | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/locales.dart'; | ||||
| import 'package:immich_mobile/modules/backup/background_service/background.service.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; | ||||
| import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
| @@ -30,12 +31,14 @@ void main() async { | ||||
|  | ||||
|   Hive.registerAdapter(HiveSavedLoginInfoAdapter()); | ||||
|   Hive.registerAdapter(HiveBackupAlbumsAdapter()); | ||||
|   Hive.registerAdapter(HiveDuplicatedAssetsAdapter()); | ||||
|  | ||||
|   await Hive.openBox(userInfoBox); | ||||
|   await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox); | ||||
|   await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox); | ||||
|   await Hive.openBox(hiveGithubReleaseInfoBox); | ||||
|   await Hive.openBox(userSettingInfoBox); | ||||
|   await Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox); | ||||
|  | ||||
|   SystemChrome.setSystemUIOverlayStyle( | ||||
|     const SystemUiOverlayStyle( | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import 'package:immich_mobile/modules/backup/background_service/localization.dar | ||||
| import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/services/backup.service.dart'; | ||||
| import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; | ||||
| import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; | ||||
| @@ -316,10 +317,13 @@ class BackgroundService { | ||||
|  | ||||
|     Hive.registerAdapter(HiveSavedLoginInfoAdapter()); | ||||
|     Hive.registerAdapter(HiveBackupAlbumsAdapter()); | ||||
|     Hive.registerAdapter(HiveDuplicatedAssetsAdapter()); | ||||
|  | ||||
|     await Hive.openBox(userInfoBox); | ||||
|     await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox); | ||||
|     await Hive.openBox(userSettingInfoBox); | ||||
|     await Hive.openBox(backgroundBackupInfoBox); | ||||
|     await Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox); | ||||
|  | ||||
|     ApiService apiService = ApiService(); | ||||
|     apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey)); | ||||
| @@ -410,7 +414,7 @@ class BackgroundService { | ||||
|     final bool ok = await backupService.backupAsset( | ||||
|       toUpload, | ||||
|       _cancellationToken!, | ||||
|       notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId) {}, | ||||
|       notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId, isDup) {}, | ||||
|       notifySingleProgress ? _onProgress : (sent, total) {}, | ||||
|       notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {}, | ||||
|       _onBackupError, | ||||
| @@ -429,7 +433,7 @@ class BackgroundService { | ||||
|     return "$percent% ($_uploadedAssetsCount/$_assetsToUploadCount)"; | ||||
|   } | ||||
|  | ||||
|   void _onAssetUploaded(String deviceAssetId, String deviceId) { | ||||
|   void _onAssetUploaded(String deviceAssetId, String deviceId, bool isDup) { | ||||
|     debugPrint("Uploaded $deviceAssetId from $deviceId"); | ||||
|     _uploadedAssetsCount++; | ||||
|     _updateNotification( | ||||
|   | ||||
| @@ -45,8 +45,10 @@ class ErrorUploadAsset extends Equatable { | ||||
|   List<Object> get props { | ||||
|     return [ | ||||
|       id, | ||||
|       createdAt, | ||||
|       fileName, | ||||
|       fileType, | ||||
|       asset, | ||||
|       errorMessage, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -0,0 +1,57 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:hive/hive.dart'; | ||||
|  | ||||
| part 'hive_duplicated_assets.model.g.dart'; | ||||
|  | ||||
| @HiveType(typeId: 2) | ||||
| class HiveDuplicatedAssets { | ||||
|   @HiveField(0, defaultValue: []) | ||||
|   List<String> duplicatedAssetIds; | ||||
|  | ||||
|   HiveDuplicatedAssets({ | ||||
|     required this.duplicatedAssetIds, | ||||
|   }); | ||||
|  | ||||
|   HiveDuplicatedAssets copyWith({ | ||||
|     List<String>? duplicatedAssetIds, | ||||
|   }) { | ||||
|     return HiveDuplicatedAssets( | ||||
|       duplicatedAssetIds: duplicatedAssetIds ?? this.duplicatedAssetIds, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Map<String, dynamic> toMap() { | ||||
|     return { | ||||
|       'duplicatedAssetIds': duplicatedAssetIds, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   factory HiveDuplicatedAssets.fromMap(Map<String, dynamic> map) { | ||||
|     return HiveDuplicatedAssets( | ||||
|       duplicatedAssetIds: List<String>.from(map['duplicatedAssetIds']), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   String toJson() => json.encode(toMap()); | ||||
|  | ||||
|   factory HiveDuplicatedAssets.fromJson(String source) => | ||||
|       HiveDuplicatedAssets.fromMap(json.decode(source)); | ||||
|  | ||||
|   @override | ||||
|   String toString() => | ||||
|       'HiveDuplicatedAssets(duplicatedAssetIds: $duplicatedAssetIds)'; | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     if (identical(this, other)) return true; | ||||
|     final listEquals = const DeepCollectionEquality().equals; | ||||
|  | ||||
|     return other is HiveDuplicatedAssets && | ||||
|         listEquals(other.duplicatedAssetIds, duplicatedAssetIds); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode => duplicatedAssetIds.hashCode; | ||||
| } | ||||
| @@ -0,0 +1,42 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'hive_duplicated_assets.model.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // TypeAdapterGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| class HiveDuplicatedAssetsAdapter extends TypeAdapter<HiveDuplicatedAssets> { | ||||
|   @override | ||||
|   final int typeId = 2; | ||||
|  | ||||
|   @override | ||||
|   HiveDuplicatedAssets read(BinaryReader reader) { | ||||
|     final numOfFields = reader.readByte(); | ||||
|     final fields = <int, dynamic>{ | ||||
|       for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), | ||||
|     }; | ||||
|     return HiveDuplicatedAssets( | ||||
|       duplicatedAssetIds: | ||||
|           fields[0] == null ? [] : (fields[0] as List).cast<String>(), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void write(BinaryWriter writer, HiveDuplicatedAssets obj) { | ||||
|     writer | ||||
|       ..writeByte(1) | ||||
|       ..writeByte(0) | ||||
|       ..write(obj.duplicatedAssetIds); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode => typeId.hashCode; | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) => | ||||
|       identical(this, other) || | ||||
|       other is HiveDuplicatedAssetsAdapter && | ||||
|           runtimeType == other.runtimeType && | ||||
|           typeId == other.typeId; | ||||
| } | ||||
| @@ -10,6 +10,7 @@ import 'package:immich_mobile/modules/backup/models/backup_state.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart'; | ||||
| import 'package:immich_mobile/modules/backup/background_service/background.service.dart'; | ||||
| import 'package:immich_mobile/modules/backup/services/backup.service.dart'; | ||||
| @@ -296,6 +297,7 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|   /// Those assets are unique and are used as the total assets | ||||
|   /// | ||||
|   Future<void> _updateBackupAssetCount() async { | ||||
|     Set<String> duplicatedAssetIds = _backupService.getDuplicatedAssetIds(); | ||||
|     Set<AssetEntity> assetsFromSelectedAlbums = {}; | ||||
|     Set<AssetEntity> assetsFromExcludedAlbums = {}; | ||||
|  | ||||
| @@ -326,9 +328,15 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|     // Find asset that were backup from selected albums | ||||
|     Set<String> selectedAlbumsBackupAssets = | ||||
|         Set.from(allUniqueAssets.map((e) => e.id)); | ||||
|  | ||||
|     selectedAlbumsBackupAssets | ||||
|         .removeWhere((assetId) => !allAssetsInDatabase.contains(assetId)); | ||||
|  | ||||
|     // Remove duplicated asset from all unique assets | ||||
|     allUniqueAssets.removeWhere( | ||||
|       (asset) => duplicatedAssetIds.contains(asset.id), | ||||
|     ); | ||||
|  | ||||
|     if (allUniqueAssets.isEmpty) { | ||||
|       debugPrint("No Asset On Device"); | ||||
|       state = state.copyWith( | ||||
| @@ -455,14 +463,26 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void _onAssetUploaded(String deviceAssetId, String deviceId) { | ||||
|     state = state.copyWith( | ||||
|       selectedAlbumsBackupAssetsIds: { | ||||
|         ...state.selectedAlbumsBackupAssetsIds, | ||||
|         deviceAssetId | ||||
|       }, | ||||
|       allAssetsInDatabase: [...state.allAssetsInDatabase, deviceAssetId], | ||||
|     ); | ||||
|   void _onAssetUploaded( | ||||
|     String deviceAssetId, | ||||
|     String deviceId, | ||||
|     bool isDuplicated, | ||||
|   ) { | ||||
|     if (isDuplicated) { | ||||
|       state = state.copyWith( | ||||
|         allUniqueAssets: state.allUniqueAssets | ||||
|             .where((asset) => asset.id != deviceAssetId) | ||||
|             .toSet(), | ||||
|       ); | ||||
|     } else { | ||||
|       state = state.copyWith( | ||||
|         selectedAlbumsBackupAssetsIds: { | ||||
|           ...state.selectedAlbumsBackupAssetsIds, | ||||
|           deviceAssetId | ||||
|         }, | ||||
|         allAssetsInDatabase: [...state.allAssetsInDatabase, deviceAssetId], | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (state.allUniqueAssets.length - | ||||
|             state.selectedAlbumsBackupAssetsIds.length == | ||||
| @@ -564,6 +584,7 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|           albums.lastExcludedBackupTime, | ||||
|         ); | ||||
|       } | ||||
|       await Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox); | ||||
|       final Box backgroundBox = await Hive.openBox(backgroundBackupInfoBox); | ||||
|       state = state.copyWith( | ||||
|         backupProgress: previous, | ||||
| @@ -608,6 +629,13 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|       } catch (error) { | ||||
|         debugPrint("[_notifyBackgroundServiceCanRun] failed to close box"); | ||||
|       } | ||||
|       try { | ||||
|         if (Hive.isBoxOpen(duplicatedAssetsBox)) { | ||||
|           await Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox).close(); | ||||
|         } | ||||
|       } catch (error) { | ||||
|         debugPrint("[_notifyBackgroundServiceCanRun] failed to close box"); | ||||
|       } | ||||
|       try { | ||||
|         if (Hive.isBoxOpen(backgroundBackupInfoBox)) { | ||||
|           await Hive.box(backgroundBackupInfoBox).close(); | ||||
|   | ||||
| @@ -19,6 +19,8 @@ import 'package:http_parser/http_parser.dart'; | ||||
| import 'package:path/path.dart' as p; | ||||
| import 'package:cancellation_token_http/http.dart' as http; | ||||
|  | ||||
| import '../models/hive_duplicated_assets.model.dart'; | ||||
|  | ||||
| final backupServiceProvider = Provider( | ||||
|   (ref) => BackupService( | ||||
|     ref.watch(apiServiceProvider), | ||||
| @@ -41,6 +43,29 @@ class BackupService { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _saveDuplicatedAssetIdToLocalStorage(List<String> deviceAssetIds) { | ||||
|     HiveDuplicatedAssets duplicatedAssets = | ||||
|         Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox) | ||||
|                 .get(duplicatedAssetsKey) ?? | ||||
|             HiveDuplicatedAssets(duplicatedAssetIds: []); | ||||
|  | ||||
|     duplicatedAssets.duplicatedAssetIds = | ||||
|         {...duplicatedAssets.duplicatedAssetIds, ...deviceAssetIds}.toList(); | ||||
|  | ||||
|     Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox) | ||||
|         .put(duplicatedAssetsKey, duplicatedAssets); | ||||
|   } | ||||
|  | ||||
|   /// Get duplicated asset id from Hive storage | ||||
|   Set<String> getDuplicatedAssetIds() { | ||||
|     HiveDuplicatedAssets duplicatedAssets = | ||||
|         Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox) | ||||
|                 .get(duplicatedAssetsKey) ?? | ||||
|             HiveDuplicatedAssets(duplicatedAssetIds: []); | ||||
|  | ||||
|     return duplicatedAssets.duplicatedAssetIds.toSet(); | ||||
|   } | ||||
|  | ||||
|   /// Returns all assets newer than the last successful backup per album | ||||
|   Future<List<AssetEntity>> buildUploadCandidates( | ||||
|     HiveBackupAlbums backupAlbums, | ||||
| @@ -140,34 +165,47 @@ class BackupService { | ||||
|   Future<List<AssetEntity>> removeAlreadyUploadedAssets( | ||||
|     List<AssetEntity> candidates, | ||||
|   ) async { | ||||
|     final String deviceId = Hive.box(userInfoBox).get(deviceIdKey); | ||||
|     if (candidates.length < 10) { | ||||
|       final List<CheckDuplicateAssetResponseDto?> duplicateResponse = | ||||
|           await Future.wait( | ||||
|         candidates.map( | ||||
|           (e) => _apiService.assetApi.checkDuplicateAsset( | ||||
|             CheckDuplicateAssetDto(deviceAssetId: e.id, deviceId: deviceId), | ||||
|           ), | ||||
|     if (candidates.isEmpty) { | ||||
|       return candidates; | ||||
|     } | ||||
|     final Set<String> duplicatedAssetIds = getDuplicatedAssetIds(); | ||||
|     candidates = duplicatedAssetIds.isEmpty | ||||
|         ? candidates | ||||
|         : candidates | ||||
|             .whereNot((asset) => duplicatedAssetIds.contains(asset.id)) | ||||
|             .toList(); | ||||
|     if (candidates.isEmpty) { | ||||
|       return candidates; | ||||
|     } | ||||
|     final Set<String> existing = {}; | ||||
|     try { | ||||
|       final String deviceId = Hive.box(userInfoBox).get(deviceIdKey); | ||||
|       final CheckExistingAssetsResponseDto? duplicates = | ||||
|           await _apiService.assetApi.checkExistingAssets( | ||||
|         CheckExistingAssetsDto( | ||||
|           deviceAssetIds: candidates.map((e) => e.id).toList(), | ||||
|           deviceId: deviceId, | ||||
|         ), | ||||
|       ); | ||||
|       return candidates | ||||
|           .whereIndexed((i, e) => duplicateResponse[i]?.isExist == false) | ||||
|           .toList(); | ||||
|     } else { | ||||
|       final List<String>? allAssetsInDatabase = await getDeviceBackupAsset(); | ||||
|  | ||||
|       if (allAssetsInDatabase == null) { | ||||
|         return candidates; | ||||
|       if (duplicates != null) { | ||||
|         existing.addAll(duplicates.existingIds); | ||||
|       } | ||||
|     } on ApiException { | ||||
|       // workaround for older server versions or when checking for too many assets at once | ||||
|       final List<String>? allAssetsInDatabase = await getDeviceBackupAsset(); | ||||
|       if (allAssetsInDatabase != null) { | ||||
|         existing.addAll(allAssetsInDatabase); | ||||
|       } | ||||
|       final Set<String> inDb = allAssetsInDatabase.toSet(); | ||||
|       return candidates.whereNot((e) => inDb.contains(e.id)).toList(); | ||||
|     } | ||||
|     return existing.isEmpty | ||||
|         ? candidates | ||||
|         : candidates.whereNot((e) => existing.contains(e.id)).toList(); | ||||
|   } | ||||
|  | ||||
|   Future<bool> backupAsset( | ||||
|     Iterable<AssetEntity> assetList, | ||||
|     http.CancellationToken cancelToken, | ||||
|     Function(String, String) singleAssetDoneCb, | ||||
|     Function(String, String, bool) uploadSuccessCb, | ||||
|     Function(int, int) uploadProgressCb, | ||||
|     Function(CurrentUploadAsset) setCurrentUploadAssetCb, | ||||
|     Function(ErrorUploadAsset) errorCb, | ||||
| @@ -176,6 +214,7 @@ class BackupService { | ||||
|     String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey); | ||||
|     File? file; | ||||
|     bool anyErrors = false; | ||||
|     final List<String> duplicatedAssetIds = []; | ||||
|  | ||||
|     for (var entity in assetList) { | ||||
|       try { | ||||
| @@ -235,8 +274,13 @@ class BackupService { | ||||
|  | ||||
|           var response = await req.send(cancellationToken: cancelToken); | ||||
|  | ||||
|           if (response.statusCode == 201) { | ||||
|             singleAssetDoneCb(entity.id, deviceId); | ||||
|           if (response.statusCode == 200) { | ||||
|             // asset is a duplicate (already exists on the server) | ||||
|             duplicatedAssetIds.add(entity.id); | ||||
|             uploadSuccessCb(entity.id, deviceId, true); | ||||
|           } else if (response.statusCode == 201) { | ||||
|             // stored a new asset on the server | ||||
|             uploadSuccessCb(entity.id, deviceId, false); | ||||
|           } else { | ||||
|             var data = await response.stream.bytesToString(); | ||||
|             var error = jsonDecode(data); | ||||
| @@ -260,7 +304,8 @@ class BackupService { | ||||
|         } | ||||
|       } on http.CancelledException { | ||||
|         debugPrint("Backup was cancelled by the user"); | ||||
|         return false; | ||||
|         anyErrors = true; | ||||
|         break; | ||||
|       } catch (e) { | ||||
|         debugPrint("ERROR backupAsset: ${e.toString()}"); | ||||
|         anyErrors = true; | ||||
| @@ -271,6 +316,9 @@ class BackupService { | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     if (duplicatedAssetIds.isNotEmpty) { | ||||
|       _saveDuplicatedAssetIdToLocalStorage(duplicatedAssetIds); | ||||
|     } | ||||
|     return !anyErrors; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -419,7 +419,6 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|               ActionChip( | ||||
|                 avatar: Icon( | ||||
|                   Icons.info, | ||||
|                   size: 24, | ||||
|                   color: Colors.red[400], | ||||
|                 ), | ||||
|                 elevation: 1, | ||||
|   | ||||
| @@ -19,6 +19,8 @@ doc/AssetTypeEnum.md | ||||
| doc/AuthenticationApi.md | ||||
| doc/CheckDuplicateAssetDto.md | ||||
| doc/CheckDuplicateAssetResponseDto.md | ||||
| doc/CheckExistingAssetsDto.md | ||||
| doc/CheckExistingAssetsResponseDto.md | ||||
| doc/CreateAlbumDto.md | ||||
| doc/CreateDeviceInfoDto.md | ||||
| doc/CreateProfileImageResponseDto.md | ||||
| @@ -93,6 +95,8 @@ lib/model/asset_response_dto.dart | ||||
| lib/model/asset_type_enum.dart | ||||
| lib/model/check_duplicate_asset_dto.dart | ||||
| lib/model/check_duplicate_asset_response_dto.dart | ||||
| lib/model/check_existing_assets_dto.dart | ||||
| lib/model/check_existing_assets_response_dto.dart | ||||
| lib/model/create_album_dto.dart | ||||
| lib/model/create_device_info_dto.dart | ||||
| lib/model/create_profile_image_response_dto.dart | ||||
| @@ -133,3 +137,5 @@ lib/model/user_count_response_dto.dart | ||||
| lib/model/user_response_dto.dart | ||||
| lib/model/validate_access_token_response_dto.dart | ||||
| pubspec.yaml | ||||
| test/check_existing_assets_dto_test.dart | ||||
| test/check_existing_assets_response_dto_test.dart | ||||
|   | ||||
| @@ -76,6 +76,7 @@ Class | Method | HTTP request | Description | ||||
| *AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{albumId}/user/{userId} |  | ||||
| *AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{albumId} |  | ||||
| *AssetApi* | [**checkDuplicateAsset**](doc//AssetApi.md#checkduplicateasset) | **POST** /asset/check |  | ||||
| *AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist |  | ||||
| *AssetApi* | [**deleteAsset**](doc//AssetApi.md#deleteasset) | **DELETE** /asset |  | ||||
| *AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **GET** /asset/download |  | ||||
| *AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset |  | ||||
| @@ -130,6 +131,8 @@ Class | Method | HTTP request | Description | ||||
|  - [AssetTypeEnum](doc//AssetTypeEnum.md) | ||||
|  - [CheckDuplicateAssetDto](doc//CheckDuplicateAssetDto.md) | ||||
|  - [CheckDuplicateAssetResponseDto](doc//CheckDuplicateAssetResponseDto.md) | ||||
|  - [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md) | ||||
|  - [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md) | ||||
|  - [CreateAlbumDto](doc//CreateAlbumDto.md) | ||||
|  - [CreateDeviceInfoDto](doc//CreateDeviceInfoDto.md) | ||||
|  - [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md) | ||||
|   | ||||
| @@ -10,6 +10,7 @@ All URIs are relative to */api* | ||||
| Method | HTTP request | Description | ||||
| ------------- | ------------- | ------------- | ||||
| [**checkDuplicateAsset**](AssetApi.md#checkduplicateasset) | **POST** /asset/check |  | ||||
| [**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist |  | ||||
| [**deleteAsset**](AssetApi.md#deleteasset) | **DELETE** /asset |  | ||||
| [**downloadFile**](AssetApi.md#downloadfile) | **GET** /asset/download |  | ||||
| [**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset |  | ||||
| @@ -76,6 +77,55 @@ Name | Type | Description  | Notes | ||||
|  | ||||
| [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) | ||||
|  | ||||
| # **checkExistingAssets** | ||||
| > CheckExistingAssetsResponseDto checkExistingAssets(checkExistingAssetsDto) | ||||
|  | ||||
|  | ||||
|  | ||||
| Checks if multiple assets exist on the server and returns all existing - used by background backup | ||||
|  | ||||
| ### Example | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| // TODO Configure HTTP Bearer authorization: bearer | ||||
| // Case 1. Use String Token | ||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); | ||||
| // Case 2. Use Function which generate token. | ||||
| // String yourTokenGeneratorFunction() { ... } | ||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction); | ||||
|  | ||||
| final api_instance = AssetApi(); | ||||
| final checkExistingAssetsDto = CheckExistingAssetsDto(); // CheckExistingAssetsDto |  | ||||
|  | ||||
| try { | ||||
|     final result = api_instance.checkExistingAssets(checkExistingAssetsDto); | ||||
|     print(result); | ||||
| } catch (e) { | ||||
|     print('Exception when calling AssetApi->checkExistingAssets: $e\n'); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Parameters | ||||
|  | ||||
| Name | Type | Description  | Notes | ||||
| ------------- | ------------- | ------------- | ------------- | ||||
|  **checkExistingAssetsDto** | [**CheckExistingAssetsDto**](CheckExistingAssetsDto.md)|  |  | ||||
|  | ||||
| ### Return type | ||||
|  | ||||
| [**CheckExistingAssetsResponseDto**](CheckExistingAssetsResponseDto.md) | ||||
|  | ||||
| ### Authorization | ||||
|  | ||||
| [bearer](../README.md#bearer) | ||||
|  | ||||
| ### HTTP request headers | ||||
|  | ||||
|  - **Content-Type**: application/json | ||||
|  - **Accept**: application/json | ||||
|  | ||||
| [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) | ||||
|  | ||||
| # **deleteAsset** | ||||
| > List<DeleteAssetResponseDto> deleteAsset(deleteAssetDto) | ||||
|  | ||||
|   | ||||
							
								
								
									
										16
									
								
								mobile/openapi/doc/CheckExistingAssetsDto.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								mobile/openapi/doc/CheckExistingAssetsDto.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| # openapi.model.CheckExistingAssetsDto | ||||
|  | ||||
| ## Load the model package | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| ``` | ||||
|  | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **deviceAssetIds** | **List<String>** |  | [default to const []] | ||||
| **deviceId** | **String** |  |  | ||||
|  | ||||
| [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | ||||
|  | ||||
|  | ||||
							
								
								
									
										15
									
								
								mobile/openapi/doc/CheckExistingAssetsResponseDto.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								mobile/openapi/doc/CheckExistingAssetsResponseDto.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| # openapi.model.CheckExistingAssetsResponseDto | ||||
|  | ||||
| ## Load the model package | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| ``` | ||||
|  | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **existingIds** | **List<String>** |  | [default to const []] | ||||
|  | ||||
| [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | ||||
|  | ||||
|  | ||||
| @@ -49,6 +49,8 @@ part 'model/asset_response_dto.dart'; | ||||
| part 'model/asset_type_enum.dart'; | ||||
| part 'model/check_duplicate_asset_dto.dart'; | ||||
| part 'model/check_duplicate_asset_response_dto.dart'; | ||||
| part 'model/check_existing_assets_dto.dart'; | ||||
| part 'model/check_existing_assets_response_dto.dart'; | ||||
| part 'model/create_album_dto.dart'; | ||||
| part 'model/create_device_info_dto.dart'; | ||||
| part 'model/create_profile_image_response_dto.dart'; | ||||
|   | ||||
| @@ -72,6 +72,62 @@ class AssetApi { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   ///  | ||||
|   /// | ||||
|   /// Checks if multiple assets exist on the server and returns all existing - used by background backup | ||||
|   /// | ||||
|   /// Note: This method returns the HTTP [Response]. | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [CheckExistingAssetsDto] checkExistingAssetsDto (required): | ||||
|   Future<Response> checkExistingAssetsWithHttpInfo(CheckExistingAssetsDto checkExistingAssetsDto,) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final path = r'/asset/exist'; | ||||
|  | ||||
|     // ignore: prefer_final_locals | ||||
|     Object? postBody = checkExistingAssetsDto; | ||||
|  | ||||
|     final queryParams = <QueryParam>[]; | ||||
|     final headerParams = <String, String>{}; | ||||
|     final formParams = <String, String>{}; | ||||
|  | ||||
|     const contentTypes = <String>['application/json']; | ||||
|  | ||||
|  | ||||
|     return apiClient.invokeAPI( | ||||
|       path, | ||||
|       'POST', | ||||
|       queryParams, | ||||
|       postBody, | ||||
|       headerParams, | ||||
|       formParams, | ||||
|       contentTypes.isEmpty ? null : contentTypes.first, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   ///  | ||||
|   /// | ||||
|   /// Checks if multiple assets exist on the server and returns all existing - used by background backup | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [CheckExistingAssetsDto] checkExistingAssetsDto (required): | ||||
|   Future<CheckExistingAssetsResponseDto?> checkExistingAssets(CheckExistingAssetsDto checkExistingAssetsDto,) async { | ||||
|     final response = await checkExistingAssetsWithHttpInfo(checkExistingAssetsDto,); | ||||
|     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), 'CheckExistingAssetsResponseDto',) as CheckExistingAssetsResponseDto; | ||||
|      | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   /// Performs an HTTP 'DELETE /asset' operation and returns the [Response]. | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   | ||||
| @@ -220,6 +220,10 @@ class ApiClient { | ||||
|           return CheckDuplicateAssetDto.fromJson(value); | ||||
|         case 'CheckDuplicateAssetResponseDto': | ||||
|           return CheckDuplicateAssetResponseDto.fromJson(value); | ||||
|         case 'CheckExistingAssetsDto': | ||||
|           return CheckExistingAssetsDto.fromJson(value); | ||||
|         case 'CheckExistingAssetsResponseDto': | ||||
|           return CheckExistingAssetsResponseDto.fromJson(value); | ||||
|         case 'CreateAlbumDto': | ||||
|           return CreateAlbumDto.fromJson(value); | ||||
|         case 'CreateDeviceInfoDto': | ||||
|   | ||||
							
								
								
									
										121
									
								
								mobile/openapi/lib/model/check_existing_assets_dto.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								mobile/openapi/lib/model/check_existing_assets_dto.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
|  | ||||
| // 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 CheckExistingAssetsDto { | ||||
|   /// Returns a new [CheckExistingAssetsDto] instance. | ||||
|   CheckExistingAssetsDto({ | ||||
|     this.deviceAssetIds = const [], | ||||
|     required this.deviceId, | ||||
|   }); | ||||
|  | ||||
|   List<String> deviceAssetIds; | ||||
|  | ||||
|   String deviceId; | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is CheckExistingAssetsDto && | ||||
|      other.deviceAssetIds == deviceAssetIds && | ||||
|      other.deviceId == deviceId; | ||||
|  | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (deviceAssetIds.hashCode) + | ||||
|     (deviceId.hashCode); | ||||
|  | ||||
|   @override | ||||
|   String toString() => 'CheckExistingAssetsDto[deviceAssetIds=$deviceAssetIds, deviceId=$deviceId]'; | ||||
|  | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final _json = <String, dynamic>{}; | ||||
|       _json[r'deviceAssetIds'] = deviceAssetIds; | ||||
|       _json[r'deviceId'] = deviceId; | ||||
|     return _json; | ||||
|   } | ||||
|  | ||||
|   /// Returns a new [CheckExistingAssetsDto] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static CheckExistingAssetsDto? fromJson(dynamic value) { | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
|  | ||||
|       // Ensure that the map contains the required keys. | ||||
|       // Note 1: the values aren't checked for validity beyond being non-null. | ||||
|       // Note 2: this code is stripped in release mode! | ||||
|       assert(() { | ||||
|         requiredKeys.forEach((key) { | ||||
|           assert(json.containsKey(key), 'Required key "CheckExistingAssetsDto[$key]" is missing from JSON.'); | ||||
|           assert(json[key] != null, 'Required key "CheckExistingAssetsDto[$key]" has a null value in JSON.'); | ||||
|         }); | ||||
|         return true; | ||||
|       }()); | ||||
|  | ||||
|       return CheckExistingAssetsDto( | ||||
|         deviceAssetIds: json[r'deviceAssetIds'] is List | ||||
|             ? (json[r'deviceAssetIds'] as List).cast<String>() | ||||
|             : const [], | ||||
|         deviceId: mapValueOfType<String>(json, r'deviceId')!, | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   static List<CheckExistingAssetsDto>? listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <CheckExistingAssetsDto>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = CheckExistingAssetsDto.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
|  | ||||
|   static Map<String, CheckExistingAssetsDto> mapFromJson(dynamic json) { | ||||
|     final map = <String, CheckExistingAssetsDto>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = CheckExistingAssetsDto.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
|  | ||||
|   // maps a json object with a list of CheckExistingAssetsDto-objects as value to a dart map | ||||
|   static Map<String, List<CheckExistingAssetsDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<CheckExistingAssetsDto>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = CheckExistingAssetsDto.listFromJson(entry.value, growable: growable,); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
|  | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'deviceAssetIds', | ||||
|     'deviceId', | ||||
|   }; | ||||
| } | ||||
|  | ||||
							
								
								
									
										113
									
								
								mobile/openapi/lib/model/check_existing_assets_response_dto.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								mobile/openapi/lib/model/check_existing_assets_response_dto.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
|  | ||||
| // 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 CheckExistingAssetsResponseDto { | ||||
|   /// Returns a new [CheckExistingAssetsResponseDto] instance. | ||||
|   CheckExistingAssetsResponseDto({ | ||||
|     this.existingIds = const [], | ||||
|   }); | ||||
|  | ||||
|   List<String> existingIds; | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is CheckExistingAssetsResponseDto && | ||||
|      other.existingIds == existingIds; | ||||
|  | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (existingIds.hashCode); | ||||
|  | ||||
|   @override | ||||
|   String toString() => 'CheckExistingAssetsResponseDto[existingIds=$existingIds]'; | ||||
|  | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final _json = <String, dynamic>{}; | ||||
|       _json[r'existingIds'] = existingIds; | ||||
|     return _json; | ||||
|   } | ||||
|  | ||||
|   /// Returns a new [CheckExistingAssetsResponseDto] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static CheckExistingAssetsResponseDto? fromJson(dynamic value) { | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
|  | ||||
|       // Ensure that the map contains the required keys. | ||||
|       // Note 1: the values aren't checked for validity beyond being non-null. | ||||
|       // Note 2: this code is stripped in release mode! | ||||
|       assert(() { | ||||
|         requiredKeys.forEach((key) { | ||||
|           assert(json.containsKey(key), 'Required key "CheckExistingAssetsResponseDto[$key]" is missing from JSON.'); | ||||
|           assert(json[key] != null, 'Required key "CheckExistingAssetsResponseDto[$key]" has a null value in JSON.'); | ||||
|         }); | ||||
|         return true; | ||||
|       }()); | ||||
|  | ||||
|       return CheckExistingAssetsResponseDto( | ||||
|         existingIds: json[r'existingIds'] is List | ||||
|             ? (json[r'existingIds'] as List).cast<String>() | ||||
|             : const [], | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   static List<CheckExistingAssetsResponseDto>? listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <CheckExistingAssetsResponseDto>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = CheckExistingAssetsResponseDto.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
|  | ||||
|   static Map<String, CheckExistingAssetsResponseDto> mapFromJson(dynamic json) { | ||||
|     final map = <String, CheckExistingAssetsResponseDto>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = CheckExistingAssetsResponseDto.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
|  | ||||
|   // maps a json object with a list of CheckExistingAssetsResponseDto-objects as value to a dart map | ||||
|   static Map<String, List<CheckExistingAssetsResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<CheckExistingAssetsResponseDto>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = CheckExistingAssetsResponseDto.listFromJson(entry.value, growable: growable,); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
|  | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'existingIds', | ||||
|   }; | ||||
| } | ||||
|  | ||||
							
								
								
									
										32
									
								
								mobile/openapi/test/check_existing_assets_dto_test.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								mobile/openapi/test/check_existing_assets_dto_test.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
|  | ||||
| // 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 | ||||
|  | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:test/test.dart'; | ||||
|  | ||||
| // tests for CheckExistingAssetsDto | ||||
| void main() { | ||||
|   // final instance = CheckExistingAssetsDto(); | ||||
|  | ||||
|   group('test CheckExistingAssetsDto', () { | ||||
|     // List<String> deviceAssetIds (default value: const []) | ||||
|     test('to test the property `deviceAssetIds`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
|  | ||||
|     // String deviceId | ||||
|     test('to test the property `deviceId`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
|  | ||||
|  | ||||
|   }); | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,27 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
|  | ||||
| // 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 | ||||
|  | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:test/test.dart'; | ||||
|  | ||||
| // tests for CheckExistingAssetsResponseDto | ||||
| void main() { | ||||
|   // final instance = CheckExistingAssetsResponseDto(); | ||||
|  | ||||
|   group('test CheckExistingAssetsResponseDto', () { | ||||
|     // List<String> existingIds (default value: const []) | ||||
|     test('to test the property `existingIds`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
|  | ||||
|  | ||||
|   }); | ||||
|  | ||||
| } | ||||
| @@ -137,6 +137,7 @@ describe('Album service', () => { | ||||
|       getAssetWithNoEXIF: jest.fn(), | ||||
|       getAssetWithNoThumbnail: jest.fn(), | ||||
|       getAssetWithNoSmartInfo: jest.fn(), | ||||
|       getExistingAssets: jest.fn(), | ||||
|     }; | ||||
|  | ||||
|     sut = new AlbumService(albumRepositoryMock, assetRepositoryMock); | ||||
|   | ||||
| @@ -10,6 +10,9 @@ import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group | ||||
| import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto'; | ||||
| import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto'; | ||||
| import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto'; | ||||
| import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto'; | ||||
| import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto'; | ||||
| import { In } from 'typeorm/find-options/operator/In'; | ||||
|  | ||||
| export interface IAssetRepository { | ||||
|   create( | ||||
| @@ -32,6 +35,7 @@ export interface IAssetRepository { | ||||
|   getAssetWithNoThumbnail(): Promise<AssetEntity[]>; | ||||
|   getAssetWithNoEXIF(): Promise<AssetEntity[]>; | ||||
|   getAssetWithNoSmartInfo(): Promise<AssetEntity[]>; | ||||
|   getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<CheckExistingAssetsResponseDto>; | ||||
| } | ||||
|  | ||||
| export const ASSET_REPOSITORY = 'ASSET_REPOSITORY'; | ||||
| @@ -279,4 +283,17 @@ export class AssetRepository implements IAssetRepository { | ||||
|       relations: ['exifInfo'], | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   async getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<CheckExistingAssetsResponseDto> { | ||||
|     const existingAssets = await this.assetRepository.find({ | ||||
|       select: {deviceAssetId: true}, | ||||
|       where: { | ||||
|         deviceAssetId: In(checkDuplicateAssetDto.deviceAssetIds), | ||||
|         deviceId: checkDuplicateAssetDto.deviceId, | ||||
|         userId, | ||||
|       }, | ||||
|     }); | ||||
|     return new CheckExistingAssetsResponseDto(existingAssets.map(a => a.deviceAssetId)); | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -48,6 +48,8 @@ import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-buck | ||||
| import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto'; | ||||
| import { QueryFailedError } from 'typeorm'; | ||||
| import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto'; | ||||
| import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto'; | ||||
| import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto'; | ||||
|  | ||||
| @UseGuards(JwtAuthGuard) | ||||
| @ApiBearerAuth() | ||||
| @@ -74,6 +76,7 @@ export class AssetController { | ||||
|     @GetAuthUser() authUser: AuthUserDto, | ||||
|     @UploadedFile() file: Express.Multer.File, | ||||
|     @Body(ValidationPipe) assetInfo: CreateAssetDto, | ||||
|     @Response({ passthrough: true }) res: Res, | ||||
|   ): Promise<AssetFileUploadResponseDto> { | ||||
|     const checksum = await this.assetService.calculateChecksum(file.path); | ||||
|  | ||||
| @@ -111,6 +114,7 @@ export class AssetController { | ||||
|  | ||||
|       if (err instanceof QueryFailedError && (err as any).constraint === 'UQ_userid_checksum') { | ||||
|         const existedAsset = await this.assetService.getAssetByChecksum(authUser.id, checksum); | ||||
|         res.status(200); // normal POST is 201. we use 200 to indicate the asset already exists | ||||
|         return new AssetFileUploadResponseDto(existedAsset.id); | ||||
|       } | ||||
|  | ||||
| @@ -254,4 +258,16 @@ export class AssetController { | ||||
|   ): Promise<CheckDuplicateAssetResponseDto> { | ||||
|     return await this.assetService.checkDuplicatedAsset(authUser, checkDuplicateAssetDto); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Checks if multiple assets exist on the server and returns all existing - used by background backup  | ||||
|    */ | ||||
|   @Post('/exist') | ||||
|   @HttpCode(200) | ||||
|   async checkExistingAssets( | ||||
|     @GetAuthUser() authUser: AuthUserDto, | ||||
|     @Body(ValidationPipe) checkExistingAssetsDto: CheckExistingAssetsDto, | ||||
|   ): Promise<CheckExistingAssetsResponseDto> { | ||||
|     return await this.assetService.checkExistingAssets(authUser, checkExistingAssetsDto); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -110,6 +110,7 @@ describe('AssetService', () => { | ||||
|       getAssetWithNoEXIF: jest.fn(), | ||||
|       getAssetWithNoThumbnail: jest.fn(), | ||||
|       getAssetWithNoSmartInfo: jest.fn(), | ||||
|       getExistingAssets: jest.fn(), | ||||
|     }; | ||||
|  | ||||
|     sui = new AssetService(assetRepositoryMock, a); | ||||
|   | ||||
| @@ -37,6 +37,8 @@ import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-buck | ||||
| import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto'; | ||||
| import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto'; | ||||
| import { timeUtils } from '@app/common/utils'; | ||||
| import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto'; | ||||
| import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto'; | ||||
|  | ||||
| const fileInfo = promisify(stat); | ||||
|  | ||||
| @@ -466,6 +468,13 @@ export class AssetService { | ||||
|     return new CheckDuplicateAssetResponseDto(isDuplicated, res?.id); | ||||
|   } | ||||
|  | ||||
|   async checkExistingAssets( | ||||
|     authUser: AuthUserDto, | ||||
|     checkExistingAssetsDto: CheckExistingAssetsDto, | ||||
|   ): Promise<CheckExistingAssetsResponseDto> { | ||||
|     return this._assetRepository.getExistingAssets(authUser.id, checkExistingAssetsDto); | ||||
|   } | ||||
|  | ||||
|   async getAssetCountByTimeBucket( | ||||
|     authUser: AuthUserDto, | ||||
|     getAssetCountByTimeBucketDto: GetAssetCountByTimeBucketDto, | ||||
|   | ||||
| @@ -0,0 +1,9 @@ | ||||
| import { IsNotEmpty } from 'class-validator'; | ||||
|  | ||||
| export class CheckExistingAssetsDto { | ||||
|   @IsNotEmpty() | ||||
|   deviceAssetIds!: string[]; | ||||
|  | ||||
|   @IsNotEmpty() | ||||
|   deviceId!: string; | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| export class CheckExistingAssetsResponseDto { | ||||
|     constructor(existingIds: string[]) { | ||||
|       this.existingIds = existingIds; | ||||
|     } | ||||
|     existingIds: string[]; | ||||
|   } | ||||
|    | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -452,6 +452,38 @@ export interface CheckDuplicateAssetResponseDto { | ||||
|      */ | ||||
|     'id'?: string; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface CheckExistingAssetsDto | ||||
|  */ | ||||
| export interface CheckExistingAssetsDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<string>} | ||||
|      * @memberof CheckExistingAssetsDto | ||||
|      */ | ||||
|     'deviceAssetIds': Array<string>; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {string} | ||||
|      * @memberof CheckExistingAssetsDto | ||||
|      */ | ||||
|     'deviceId': string; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface CheckExistingAssetsResponseDto | ||||
|  */ | ||||
| export interface CheckExistingAssetsResponseDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<string>} | ||||
|      * @memberof CheckExistingAssetsResponseDto | ||||
|      */ | ||||
|     'existingIds': Array<string>; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
| @@ -2334,6 +2366,46 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | ||||
|                 options: localVarRequestOptions, | ||||
|             }; | ||||
|         }, | ||||
|         /** | ||||
|          * Checks if multiple assets exist on the server and returns all existing - used by background backup | ||||
|          * @summary  | ||||
|          * @param {CheckExistingAssetsDto} checkExistingAssetsDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         checkExistingAssets: async (checkExistingAssetsDto: CheckExistingAssetsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             // verify required parameter 'checkExistingAssetsDto' is not null or undefined | ||||
|             assertParamExists('checkExistingAssets', 'checkExistingAssetsDto', checkExistingAssetsDto) | ||||
|             const localVarPath = `/asset/exist`; | ||||
|             // use dummy base URL string because the URL constructor only accepts absolute URLs. | ||||
|             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); | ||||
|             let baseOptions; | ||||
|             if (configuration) { | ||||
|                 baseOptions = configuration.baseOptions; | ||||
|             } | ||||
|  | ||||
|             const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; | ||||
|             const localVarHeaderParameter = {} as any; | ||||
|             const localVarQueryParameter = {} as any; | ||||
|  | ||||
|             // authentication bearer required | ||||
|             // http bearer authentication required | ||||
|             await setBearerAuthToObject(localVarHeaderParameter, configuration) | ||||
|  | ||||
|  | ||||
|      | ||||
|             localVarHeaderParameter['Content-Type'] = 'application/json'; | ||||
|  | ||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); | ||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; | ||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; | ||||
|             localVarRequestOptions.data = serializeDataIfNeeded(checkExistingAssetsDto, localVarRequestOptions, configuration) | ||||
|  | ||||
|             return { | ||||
|                 url: toPathString(localVarUrlObj), | ||||
|                 options: localVarRequestOptions, | ||||
|             }; | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {DeleteAssetDto} deleteAssetDto  | ||||
| @@ -2953,6 +3025,17 @@ export const AssetApiFp = function(configuration?: Configuration) { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.checkDuplicateAsset(checkDuplicateAssetDto, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
|          * Checks if multiple assets exist on the server and returns all existing - used by background backup | ||||
|          * @summary  | ||||
|          * @param {CheckExistingAssetsDto} checkExistingAssetsDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async checkExistingAssets(checkExistingAssetsDto: CheckExistingAssetsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CheckExistingAssetsResponseDto>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.checkExistingAssets(checkExistingAssetsDto, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {DeleteAssetDto} deleteAssetDto  | ||||
| @@ -3128,6 +3211,16 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath | ||||
|         checkDuplicateAsset(checkDuplicateAssetDto: CheckDuplicateAssetDto, options?: any): AxiosPromise<CheckDuplicateAssetResponseDto> { | ||||
|             return localVarFp.checkDuplicateAsset(checkDuplicateAssetDto, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          * Checks if multiple assets exist on the server and returns all existing - used by background backup | ||||
|          * @summary  | ||||
|          * @param {CheckExistingAssetsDto} checkExistingAssetsDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         checkExistingAssets(checkExistingAssetsDto: CheckExistingAssetsDto, options?: any): AxiosPromise<CheckExistingAssetsResponseDto> { | ||||
|             return localVarFp.checkExistingAssets(checkExistingAssetsDto, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {DeleteAssetDto} deleteAssetDto  | ||||
| @@ -3290,6 +3383,18 @@ export class AssetApi extends BaseAPI { | ||||
|         return AssetApiFp(this.configuration).checkDuplicateAsset(checkDuplicateAssetDto, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Checks if multiple assets exist on the server and returns all existing - used by background backup | ||||
|      * @summary  | ||||
|      * @param {CheckExistingAssetsDto} checkExistingAssetsDto  | ||||
|      * @param {*} [options] Override http request option. | ||||
|      * @throws {RequiredError} | ||||
|      * @memberof AssetApi | ||||
|      */ | ||||
|     public checkExistingAssets(checkExistingAssetsDto: CheckExistingAssetsDto, options?: AxiosRequestConfig) { | ||||
|         return AssetApiFp(this.configuration).checkExistingAssets(checkExistingAssetsDto, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      *  | ||||
|      * @param {DeleteAssetDto} deleteAssetDto  | ||||
|   | ||||
| @@ -200,12 +200,12 @@ async function fileUploader(asset: File, uploadType: UploadType) { | ||||
| } | ||||
| // TODO: This should have a proper type | ||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||
| function handleUploadError(asset: File, respBody?: any) { | ||||
| 	const extraMsg = respBody ? ' ' + respBody.message : ''; | ||||
| function handleUploadError(asset: File, respBody: any, extraMessage?: string) { | ||||
| 	const extraMsg = respBody ? ' ' + respBody?.message : ''; | ||||
|  | ||||
| 	notificationController.show({ | ||||
| 		type: NotificationType.Error, | ||||
| 		message: `Cannot upload file ${asset.name}!${extraMsg}`, | ||||
| 		message: `Cannot upload file ${asset.name} ${extraMsg}${extraMessage}`, | ||||
| 		timeout: 5000 | ||||
| 	}); | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user