forked from Cutlery/immich
		
	Add information for uploading asset and error indication with error message for each failed upload. (#315)
* Added info box * Fixed upload endpoint doesn't report error status code * Added chip to show update error * Added chip to show failed upload * Add duplication check for upload * Better duplication-checking placement * Remove check for duplicated asset * Added failed backup status route * added page * Display error card with thumbnail * Improved styling * Set thumbnail with better quality * Remove force upload error
This commit is contained in:
		
							parent
							
								
									357f7d1c31
								
							
						
					
					
						commit
						58ec7553ea
					
				| @ -19,7 +19,7 @@ platform :ios do | ||||
|   desc "iOS Beta" | ||||
|   lane :beta do | ||||
|     increment_version_number( | ||||
|       version_number: "1.16.1" | ||||
|       version_number: "1.17.0" | ||||
|     ) | ||||
|     increment_build_number( | ||||
|       build_number: latest_testflight_build_number + 1, | ||||
|  | ||||
| @ -1,13 +1,14 @@ | ||||
| import 'package:cancellation_token_http/http.dart'; | ||||
| import 'package:equatable/equatable.dart'; | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:photo_manager/photo_manager.dart'; | ||||
| 
 | ||||
| import 'package:immich_mobile/modules/backup/models/available_album.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart'; | ||||
| import 'package:immich_mobile/shared/models/server_info.model.dart'; | ||||
| 
 | ||||
| enum BackUpProgressEnum { idle, inProgress, done } | ||||
| 
 | ||||
| class BackUpState extends Equatable { | ||||
| class BackUpState { | ||||
|   // enum | ||||
|   final BackUpProgressEnum backupProgress; | ||||
|   final List<String> allAssetsInDatabase; | ||||
| @ -26,6 +27,9 @@ class BackUpState extends Equatable { | ||||
|   /// All assets from the selected albums that have been backup | ||||
|   final Set<String> selectedAlbumsBackupAssetsIds; | ||||
| 
 | ||||
|   // Current Backup Asset | ||||
|   final CurrentUploadAsset currentUploadAsset; | ||||
| 
 | ||||
|   const BackUpState({ | ||||
|     required this.backupProgress, | ||||
|     required this.allAssetsInDatabase, | ||||
| @ -37,6 +41,7 @@ class BackUpState extends Equatable { | ||||
|     required this.excludedBackupAlbums, | ||||
|     required this.allUniqueAssets, | ||||
|     required this.selectedAlbumsBackupAssetsIds, | ||||
|     required this.currentUploadAsset, | ||||
|   }); | ||||
| 
 | ||||
|   BackUpState copyWith({ | ||||
| @ -50,6 +55,7 @@ class BackUpState extends Equatable { | ||||
|     Set<AssetPathEntity>? excludedBackupAlbums, | ||||
|     Set<AssetEntity>? allUniqueAssets, | ||||
|     Set<String>? selectedAlbumsBackupAssetsIds, | ||||
|     CurrentUploadAsset? currentUploadAsset, | ||||
|   }) { | ||||
|     return BackUpState( | ||||
|       backupProgress: backupProgress ?? this.backupProgress, | ||||
| @ -63,27 +69,47 @@ class BackUpState extends Equatable { | ||||
|       allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets, | ||||
|       selectedAlbumsBackupAssetsIds: | ||||
|           selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds, | ||||
|       currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds)'; | ||||
|     return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)'; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   List<Object> get props { | ||||
|     return [ | ||||
|       backupProgress, | ||||
|       allAssetsInDatabase, | ||||
|       progressInPercentage, | ||||
|       cancelToken, | ||||
|       serverInfo, | ||||
|       availableAlbums, | ||||
|       selectedBackupAlbums, | ||||
|       excludedBackupAlbums, | ||||
|       allUniqueAssets, | ||||
|       selectedAlbumsBackupAssetsIds, | ||||
|     ]; | ||||
|   bool operator ==(Object other) { | ||||
|     if (identical(this, other)) return true; | ||||
|     final collectionEquals = const DeepCollectionEquality().equals; | ||||
| 
 | ||||
|     return other is BackUpState && | ||||
|         other.backupProgress == backupProgress && | ||||
|         collectionEquals(other.allAssetsInDatabase, allAssetsInDatabase) && | ||||
|         other.progressInPercentage == progressInPercentage && | ||||
|         other.cancelToken == cancelToken && | ||||
|         other.serverInfo == serverInfo && | ||||
|         collectionEquals(other.availableAlbums, availableAlbums) && | ||||
|         collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) && | ||||
|         collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) && | ||||
|         collectionEquals(other.allUniqueAssets, allUniqueAssets) && | ||||
|         collectionEquals(other.selectedAlbumsBackupAssetsIds, | ||||
|             selectedAlbumsBackupAssetsIds) && | ||||
|         other.currentUploadAsset == currentUploadAsset; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     return backupProgress.hashCode ^ | ||||
|         allAssetsInDatabase.hashCode ^ | ||||
|         progressInPercentage.hashCode ^ | ||||
|         cancelToken.hashCode ^ | ||||
|         serverInfo.hashCode ^ | ||||
|         availableAlbums.hashCode ^ | ||||
|         selectedBackupAlbums.hashCode ^ | ||||
|         excludedBackupAlbums.hashCode ^ | ||||
|         allUniqueAssets.hashCode ^ | ||||
|         selectedAlbumsBackupAssetsIds.hashCode ^ | ||||
|         currentUploadAsset.hashCode; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,48 @@ | ||||
| import 'dart:convert'; | ||||
| 
 | ||||
| class CheckDuplicateAssetResponse { | ||||
|   final bool isExist; | ||||
|   CheckDuplicateAssetResponse({ | ||||
|     required this.isExist, | ||||
|   }); | ||||
| 
 | ||||
|   CheckDuplicateAssetResponse copyWith({ | ||||
|     bool? isExist, | ||||
|   }) { | ||||
|     return CheckDuplicateAssetResponse( | ||||
|       isExist: isExist ?? this.isExist, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   Map<String, dynamic> toMap() { | ||||
|     final result = <String, dynamic>{}; | ||||
| 
 | ||||
|     result.addAll({'isExist': isExist}); | ||||
| 
 | ||||
|     return result; | ||||
|   } | ||||
| 
 | ||||
|   factory CheckDuplicateAssetResponse.fromMap(Map<String, dynamic> map) { | ||||
|     return CheckDuplicateAssetResponse( | ||||
|       isExist: map['isExist'] ?? false, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   String toJson() => json.encode(toMap()); | ||||
| 
 | ||||
|   factory CheckDuplicateAssetResponse.fromJson(String source) => | ||||
|       CheckDuplicateAssetResponse.fromMap(json.decode(source)); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'CheckDuplicateAssetResponse(isExist: $isExist)'; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     if (identical(this, other)) return true; | ||||
| 
 | ||||
|     return other is CheckDuplicateAssetResponse && other.isExist == isExist; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => isExist.hashCode; | ||||
| } | ||||
| @ -0,0 +1,78 @@ | ||||
| import 'dart:convert'; | ||||
| 
 | ||||
| class CurrentUploadAsset { | ||||
|   final String id; | ||||
|   final DateTime createdAt; | ||||
|   final String fileName; | ||||
|   final String fileType; | ||||
| 
 | ||||
|   CurrentUploadAsset({ | ||||
|     required this.id, | ||||
|     required this.createdAt, | ||||
|     required this.fileName, | ||||
|     required this.fileType, | ||||
|   }); | ||||
| 
 | ||||
|   CurrentUploadAsset copyWith({ | ||||
|     String? id, | ||||
|     DateTime? createdAt, | ||||
|     String? fileName, | ||||
|     String? fileType, | ||||
|   }) { | ||||
|     return CurrentUploadAsset( | ||||
|       id: id ?? this.id, | ||||
|       createdAt: createdAt ?? this.createdAt, | ||||
|       fileName: fileName ?? this.fileName, | ||||
|       fileType: fileType ?? this.fileType, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   Map<String, dynamic> toMap() { | ||||
|     final result = <String, dynamic>{}; | ||||
| 
 | ||||
|     result.addAll({'id': id}); | ||||
|     result.addAll({'createdAt': createdAt.millisecondsSinceEpoch}); | ||||
|     result.addAll({'fileName': fileName}); | ||||
|     result.addAll({'fileType': fileType}); | ||||
| 
 | ||||
|     return result; | ||||
|   } | ||||
| 
 | ||||
|   factory CurrentUploadAsset.fromMap(Map<String, dynamic> map) { | ||||
|     return CurrentUploadAsset( | ||||
|       id: map['id'] ?? '', | ||||
|       createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt']), | ||||
|       fileName: map['fileName'] ?? '', | ||||
|       fileType: map['fileType'] ?? '', | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   String toJson() => json.encode(toMap()); | ||||
| 
 | ||||
|   factory CurrentUploadAsset.fromJson(String source) => | ||||
|       CurrentUploadAsset.fromMap(json.decode(source)); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'CurrentUploadAsset(id: $id, createdAt: $createdAt, fileName: $fileName, fileType: $fileType)'; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     if (identical(this, other)) return true; | ||||
| 
 | ||||
|     return other is CurrentUploadAsset && | ||||
|         other.id == id && | ||||
|         other.createdAt == createdAt && | ||||
|         other.fileName == fileName && | ||||
|         other.fileType == fileType; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     return id.hashCode ^ | ||||
|         createdAt.hashCode ^ | ||||
|         fileName.hashCode ^ | ||||
|         fileType.hashCode; | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,53 @@ | ||||
| import 'package:equatable/equatable.dart'; | ||||
| import 'package:photo_manager/photo_manager.dart'; | ||||
| 
 | ||||
| class ErrorUploadAsset extends Equatable { | ||||
|   final String id; | ||||
|   final DateTime createdAt; | ||||
|   final String fileName; | ||||
|   final String fileType; | ||||
|   final AssetEntity asset; | ||||
|   final String errorMessage; | ||||
| 
 | ||||
|   const ErrorUploadAsset({ | ||||
|     required this.id, | ||||
|     required this.createdAt, | ||||
|     required this.fileName, | ||||
|     required this.fileType, | ||||
|     required this.asset, | ||||
|     required this.errorMessage, | ||||
|   }); | ||||
| 
 | ||||
|   ErrorUploadAsset copyWith({ | ||||
|     String? id, | ||||
|     DateTime? createdAt, | ||||
|     String? fileName, | ||||
|     String? fileType, | ||||
|     AssetEntity? asset, | ||||
|     String? errorMessage, | ||||
|   }) { | ||||
|     return ErrorUploadAsset( | ||||
|       id: id ?? this.id, | ||||
|       createdAt: createdAt ?? this.createdAt, | ||||
|       fileName: fileName ?? this.fileName, | ||||
|       fileType: fileType ?? this.fileType, | ||||
|       asset: asset ?? this.asset, | ||||
|       errorMessage: errorMessage ?? this.errorMessage, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'ErrorUploadAsset(id: $id, createdAt: $createdAt, fileName: $fileName, fileType: $fileType, asset: $asset, errorMessage: $errorMessage)'; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   List<Object> get props { | ||||
|     return [ | ||||
|       id, | ||||
|       fileName, | ||||
|       fileType, | ||||
|       errorMessage, | ||||
|     ]; | ||||
|   } | ||||
| } | ||||
| @ -5,7 +5,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/available_album.model.dart'; | ||||
| 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/providers/error_backup_list.provider.dart'; | ||||
| import 'package:immich_mobile/modules/backup/services/backup.service.dart'; | ||||
| import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
| @ -14,8 +17,12 @@ import 'package:immich_mobile/shared/services/server_info.service.dart'; | ||||
| import 'package:photo_manager/photo_manager.dart'; | ||||
| 
 | ||||
| class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|   BackupNotifier(this._backupService, this._serverInfoService, this._authState) | ||||
|       : super( | ||||
|   BackupNotifier( | ||||
|     this._backupService, | ||||
|     this._serverInfoService, | ||||
|     this._authState, | ||||
|     this.ref, | ||||
|   ) : super( | ||||
|           BackUpState( | ||||
|             backupProgress: BackUpProgressEnum.idle, | ||||
|             allAssetsInDatabase: const [], | ||||
| @ -35,6 +42,12 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|             excludedBackupAlbums: const {}, | ||||
|             allUniqueAssets: const {}, | ||||
|             selectedAlbumsBackupAssetsIds: const {}, | ||||
|             currentUploadAsset: CurrentUploadAsset( | ||||
|               id: '...', | ||||
|               createdAt: DateTime.parse('2020-10-04'), | ||||
|               fileName: '...', | ||||
|               fileType: '...', | ||||
|             ), | ||||
|           ), | ||||
|         ) { | ||||
|     getBackupInfo(); | ||||
| @ -43,6 +56,7 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|   final BackupService _backupService; | ||||
|   final ServerInfoService _serverInfoService; | ||||
|   final AuthenticationState _authState; | ||||
|   final Ref ref; | ||||
| 
 | ||||
|   /// | ||||
|   /// UI INTERACTION | ||||
| @ -235,8 +249,11 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|   /// and then update the UI according to those information | ||||
|   /// | ||||
|   Future<void> getBackupInfo() async { | ||||
|     await _getBackupAlbumsInfo(); | ||||
|     await _updateServerInfo(); | ||||
|     await Future.wait([ | ||||
|       _getBackupAlbumsInfo(), | ||||
|       _updateServerInfo(), | ||||
|     ]); | ||||
| 
 | ||||
|     await _updateBackupAssetCount(); | ||||
|   } | ||||
| 
 | ||||
| @ -287,13 +304,27 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
| 
 | ||||
|       // Perform Backup | ||||
|       state = state.copyWith(cancelToken: CancellationToken()); | ||||
|       _backupService.backupAsset(assetsWillBeBackup, state.cancelToken, | ||||
|           _onAssetUploaded, _onUploadProgress); | ||||
|       _backupService.backupAsset( | ||||
|         assetsWillBeBackup, | ||||
|         state.cancelToken, | ||||
|         _onAssetUploaded, | ||||
|         _onUploadProgress, | ||||
|         _onSetCurrentBackupAsset, | ||||
|         _onBackupError, | ||||
|       ); | ||||
|     } else { | ||||
|       PhotoManager.openSetting(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   void _onBackupError(ErrorUploadAsset errorAssetInfo) { | ||||
|     ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo); | ||||
|   } | ||||
| 
 | ||||
|   void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) { | ||||
|     state = state.copyWith(currentUploadAsset: currentUploadAsset); | ||||
|   } | ||||
| 
 | ||||
|   void cancelBackup() { | ||||
|     state.cancelToken.cancel(); | ||||
|     state = state.copyWith( | ||||
| @ -375,5 +406,6 @@ final backupProvider = | ||||
|     ref.watch(backupServiceProvider), | ||||
|     ref.watch(serverInfoServiceProvider), | ||||
|     ref.watch(authenticationProvider), | ||||
|     ref, | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| @ -0,0 +1,23 @@ | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart'; | ||||
| 
 | ||||
| class ErrorBackupListNotifier extends StateNotifier<Set<ErrorUploadAsset>> { | ||||
|   ErrorBackupListNotifier() : super({}); | ||||
| 
 | ||||
|   add(ErrorUploadAsset errorAsset) { | ||||
|     state = state.union({errorAsset}); | ||||
|   } | ||||
| 
 | ||||
|   remove(ErrorUploadAsset errorAsset) { | ||||
|     state = state.difference({errorAsset}); | ||||
|   } | ||||
| 
 | ||||
|   empty() { | ||||
|     state = {}; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| final errorBackupListProvider = | ||||
|     StateNotifierProvider<ErrorBackupListNotifier, Set<ErrorUploadAsset>>( | ||||
|   (ref) => ErrorBackupListNotifier(), | ||||
| ); | ||||
| @ -7,6 +7,9 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:hive/hive.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/check_duplicate_asset_response.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/shared/services/network.service.dart'; | ||||
| import 'package:immich_mobile/shared/models/device_info.model.dart'; | ||||
| import 'package:immich_mobile/utils/files_helper.dart'; | ||||
| @ -20,6 +23,7 @@ final backupServiceProvider = | ||||
| 
 | ||||
| class BackupService { | ||||
|   final NetworkService _networkService; | ||||
| 
 | ||||
|   BackupService(this._networkService); | ||||
| 
 | ||||
|   Future<List<String>> getDeviceBackupAsset() async { | ||||
| @ -32,17 +36,40 @@ class BackupService { | ||||
|     return result.cast<String>(); | ||||
|   } | ||||
| 
 | ||||
|   Future<bool> checkDuplicateAsset(String deviceAssetId) async { | ||||
|     String deviceId = Hive.box(userInfoBox).get(deviceIdKey); | ||||
| 
 | ||||
|     try { | ||||
|       Response response = | ||||
|           await _networkService.postRequest(url: "asset/check", data: { | ||||
|         "deviceId": deviceId, | ||||
|         "deviceAssetId": deviceAssetId, | ||||
|       }); | ||||
| 
 | ||||
|       if (response.statusCode == 200) { | ||||
|         var result = CheckDuplicateAssetResponse.fromJson(response.toString()); | ||||
| 
 | ||||
|         return result.isExist; | ||||
|       } else { | ||||
|         return false; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   backupAsset( | ||||
|       Set<AssetEntity> assetList, | ||||
|       http.CancellationToken cancelToken, | ||||
|       Function(String, String) singleAssetDoneCb, | ||||
|       Function(int, int) uploadProgress) async { | ||||
|     Set<AssetEntity> assetList, | ||||
|     http.CancellationToken cancelToken, | ||||
|     Function(String, String) singleAssetDoneCb, | ||||
|     Function(int, int) uploadProgressCb, | ||||
|     Function(CurrentUploadAsset) setCurrentUploadAssetCb, | ||||
|     Function(ErrorUploadAsset) errorCb, | ||||
|   ) async { | ||||
|     String deviceId = Hive.box(userInfoBox).get(deviceIdKey); | ||||
|     String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey); | ||||
|     File? file; | ||||
| 
 | ||||
|     http.MultipartFile? thumbnailUploadData; | ||||
| 
 | ||||
|     for (var entity in assetList) { | ||||
|       try { | ||||
|         if (entity.type == AssetType.video) { | ||||
| @ -74,7 +101,7 @@ class BackupService { | ||||
|           var req = MultipartRequest( | ||||
|               'POST', Uri.parse('$savedEndpoint/asset/upload'), | ||||
|               onProgress: ((bytes, totalBytes) => | ||||
|                   uploadProgress(bytes, totalBytes))); | ||||
|                   uploadProgressCb(bytes, totalBytes))); | ||||
|           req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}"; | ||||
| 
 | ||||
|           req.fields['deviceAssetId'] = entity.id; | ||||
| @ -88,10 +115,35 @@ class BackupService { | ||||
| 
 | ||||
|           req.files.add(assetRawUploadData); | ||||
| 
 | ||||
|           var res = await req.send(cancellationToken: cancelToken); | ||||
|           setCurrentUploadAssetCb( | ||||
|             CurrentUploadAsset( | ||||
|               id: entity.id, | ||||
|               createdAt: entity.createDateTime, | ||||
|               fileName: originalFileName, | ||||
|               fileType: _getAssetType(entity.type), | ||||
|             ), | ||||
|           ); | ||||
| 
 | ||||
|           if (res.statusCode == 201) { | ||||
|           var response = await req.send(cancellationToken: cancelToken); | ||||
| 
 | ||||
|           if (response.statusCode == 201) { | ||||
|             singleAssetDoneCb(entity.id, deviceId); | ||||
|           } else { | ||||
|             var data = await response.stream.bytesToString(); | ||||
|             var error = jsonDecode(data); | ||||
| 
 | ||||
|             debugPrint( | ||||
|                 "Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}"); | ||||
| 
 | ||||
|             errorCb(ErrorUploadAsset( | ||||
|               asset: entity, | ||||
|               id: entity.id, | ||||
|               createdAt: entity.createDateTime, | ||||
|               fileName: originalFileName, | ||||
|               fileType: _getAssetType(entity.type), | ||||
|               errorMessage: error['error'], | ||||
|             )); | ||||
|             continue; | ||||
|           } | ||||
|         } | ||||
|       } on http.CancelledException { | ||||
| @ -108,6 +160,8 @@ class BackupService { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   void sendBackupRequest(AssetEntity entity) {} | ||||
| 
 | ||||
|   String _getAssetType(AssetType assetType) { | ||||
|     switch (assetType) { | ||||
|       case AssetType.audio: | ||||
|  | ||||
| @ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart'; | ||||
| import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/backup_state.model.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
| @ -9,6 +10,7 @@ import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/shared/providers/websocket.provider.dart'; | ||||
| import 'package:immich_mobile/modules/backup/ui/backup_info_card.dart'; | ||||
| import 'package:intl/intl.dart'; | ||||
| import 'package:percent_indicator/linear_percent_indicator.dart'; | ||||
| 
 | ||||
| class BackupControllerPage extends HookConsumerWidget { | ||||
| @ -42,7 +44,7 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|           color: Theme.of(context).primaryColor, | ||||
|         ), | ||||
|         title: const Text( | ||||
|           "Server Storage", | ||||
|           "Server storage", | ||||
|           style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), | ||||
|         ), | ||||
|         subtitle: Padding( | ||||
| @ -56,7 +58,7 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|                   padding: | ||||
|                       const EdgeInsets.symmetric(horizontal: 0, vertical: 0), | ||||
|                   barRadius: const Radius.circular(2), | ||||
|                   lineHeight: 6.0, | ||||
|                   lineHeight: 10.0, | ||||
|                   percent: backupState.serverInfo.diskUsagePercentage / 100.0, | ||||
|                   backgroundColor: Colors.grey, | ||||
|                   progressColor: Theme.of(context).primaryColor, | ||||
| @ -246,6 +248,141 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     _buildCurrentBackupAssetInfoCard() { | ||||
|       return ListTile( | ||||
|         leading: Icon( | ||||
|           Icons.info_outline_rounded, | ||||
|           color: Theme.of(context).primaryColor, | ||||
|         ), | ||||
|         title: Row( | ||||
|           mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|           children: [ | ||||
|             const Text( | ||||
|               "Uploading file info", | ||||
|               style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), | ||||
|             ), | ||||
|             if (ref.watch(errorBackupListProvider).isNotEmpty) | ||||
|               ActionChip( | ||||
|                 avatar: Icon( | ||||
|                   Icons.info, | ||||
|                   size: 24, | ||||
|                   color: Colors.red[400], | ||||
|                 ), | ||||
|                 elevation: 1, | ||||
|                 visualDensity: VisualDensity.compact, | ||||
|                 label: Text( | ||||
|                   "Failed (${ref.watch(errorBackupListProvider).length})", | ||||
|                   style: TextStyle( | ||||
|                     color: Colors.red[400], | ||||
|                     fontWeight: FontWeight.bold, | ||||
|                     fontSize: 11, | ||||
|                   ), | ||||
|                 ), | ||||
|                 backgroundColor: Colors.white, | ||||
|                 onPressed: () { | ||||
|                   AutoRouter.of(context).push(const FailedBackupStatusRoute()); | ||||
|                 }, | ||||
|               ), | ||||
|           ], | ||||
|         ), | ||||
|         subtitle: Column( | ||||
|           children: [ | ||||
|             Padding( | ||||
|               padding: const EdgeInsets.only(top: 8.0), | ||||
|               child: LinearPercentIndicator( | ||||
|                 padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0), | ||||
|                 barRadius: const Radius.circular(2), | ||||
|                 lineHeight: 10.0, | ||||
|                 trailing: Text( | ||||
|                   " ${backupState.progressInPercentage.toStringAsFixed(0)}%", | ||||
|                   style: const TextStyle(fontSize: 12), | ||||
|                 ), | ||||
|                 percent: backupState.progressInPercentage / 100.0, | ||||
|                 backgroundColor: Colors.grey, | ||||
|                 progressColor: Theme.of(context).primaryColor, | ||||
|               ), | ||||
|             ), | ||||
|             Padding( | ||||
|               padding: const EdgeInsets.only(top: 8.0), | ||||
|               child: Table( | ||||
|                 border: TableBorder.all( | ||||
|                   color: Colors.black12, | ||||
|                   width: 1, | ||||
|                 ), | ||||
|                 children: [ | ||||
|                   TableRow( | ||||
|                     decoration: BoxDecoration( | ||||
|                       color: Colors.grey[100], | ||||
|                     ), | ||||
|                     children: [ | ||||
|                       TableCell( | ||||
|                         verticalAlignment: TableCellVerticalAlignment.middle, | ||||
|                         child: Padding( | ||||
|                           padding: const EdgeInsets.all(6.0), | ||||
|                           child: Text( | ||||
|                             'File name: ${backupState.currentUploadAsset.fileName} [${backupState.currentUploadAsset.fileType.toLowerCase()}]', | ||||
|                             style: const TextStyle( | ||||
|                                 fontWeight: FontWeight.bold, fontSize: 10.0), | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                   TableRow( | ||||
|                     decoration: BoxDecoration( | ||||
|                       color: Colors.grey[200], | ||||
|                     ), | ||||
|                     children: [ | ||||
|                       TableCell( | ||||
|                         verticalAlignment: TableCellVerticalAlignment.middle, | ||||
|                         child: Padding( | ||||
|                           padding: const EdgeInsets.all(6.0), | ||||
|                           child: Text( | ||||
|                             "Created on: ${DateFormat.yMMMMd('en_US').format( | ||||
|                               DateTime.parse( | ||||
|                                 backupState.currentUploadAsset.createdAt | ||||
|                                     .toString(), | ||||
|                               ), | ||||
|                             )}", | ||||
|                             style: const TextStyle( | ||||
|                                 fontWeight: FontWeight.bold, fontSize: 10.0), | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                   TableRow( | ||||
|                     decoration: BoxDecoration( | ||||
|                       color: Colors.grey[100], | ||||
|                     ), | ||||
|                     children: [ | ||||
|                       TableCell( | ||||
|                         child: Padding( | ||||
|                           padding: const EdgeInsets.all(6.0), | ||||
|                           child: Text( | ||||
|                             "ID: ${backupState.currentUploadAsset.id}", | ||||
|                             style: const TextStyle( | ||||
|                               fontWeight: FontWeight.bold, | ||||
|                               fontSize: 10.0, | ||||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     void startBackup() { | ||||
|       ref.watch(errorBackupListProvider.notifier).empty(); | ||||
|       ref.watch(backupProvider.notifier).startBackupProcess(); | ||||
|     } | ||||
| 
 | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         elevation: 0, | ||||
| @ -264,7 +401,7 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|             )), | ||||
|       ), | ||||
|       body: Padding( | ||||
|         padding: const EdgeInsets.all(16.0), | ||||
|         padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32), | ||||
|         child: ListView( | ||||
|           // crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
| @ -297,23 +434,11 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|             const Divider(), | ||||
|             _buildStorageInformation(), | ||||
|             const Divider(), | ||||
|             _buildCurrentBackupAssetInfoCard(), | ||||
|             Padding( | ||||
|               padding: const EdgeInsets.all(8.0), | ||||
|               child: Text( | ||||
|                   "Asset that were being backup: ${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length} [${backupState.progressInPercentage.toStringAsFixed(0)}%]"), | ||||
|             ), | ||||
|             Padding( | ||||
|               padding: const EdgeInsets.only(left: 8.0), | ||||
|               child: Row(children: [ | ||||
|                 const Text("Backup Progress:"), | ||||
|                 const Padding(padding: EdgeInsets.symmetric(horizontal: 2)), | ||||
|                 backupState.backupProgress == BackUpProgressEnum.inProgress | ||||
|                     ? const CircularProgressIndicator.adaptive() | ||||
|                     : const Text("Done"), | ||||
|               ]), | ||||
|             ), | ||||
|             Padding( | ||||
|               padding: const EdgeInsets.all(8.0), | ||||
|               padding: const EdgeInsets.only( | ||||
|                 top: 24, | ||||
|               ), | ||||
|               child: Container( | ||||
|                 child: | ||||
|                     backupState.backupProgress == BackUpProgressEnum.inProgress | ||||
| @ -321,25 +446,33 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|                             style: ElevatedButton.styleFrom( | ||||
|                               primary: Colors.red[300], | ||||
|                               onPrimary: Colors.grey[50], | ||||
|                               padding: const EdgeInsets.all(14), | ||||
|                             ), | ||||
|                             onPressed: () { | ||||
|                               ref.read(backupProvider.notifier).cancelBackup(); | ||||
|                             }, | ||||
|                             child: const Text("Cancel"), | ||||
|                             child: const Text( | ||||
|                               "CANCEL", | ||||
|                               style: TextStyle( | ||||
|                                 fontSize: 14, | ||||
|                                 fontWeight: FontWeight.bold, | ||||
|                               ), | ||||
|                             ), | ||||
|                           ) | ||||
|                         : ElevatedButton( | ||||
|                             style: ElevatedButton.styleFrom( | ||||
|                               primary: Theme.of(context).primaryColor, | ||||
|                               onPrimary: Colors.grey[50], | ||||
|                               padding: const EdgeInsets.all(14), | ||||
|                             ), | ||||
|                             onPressed: shouldBackup ? startBackup : null, | ||||
|                             child: const Text( | ||||
|                               "START BACKUP", | ||||
|                               style: TextStyle( | ||||
|                                 fontSize: 14, | ||||
|                                 fontWeight: FontWeight.bold, | ||||
|                               ), | ||||
|                             ), | ||||
|                             onPressed: shouldBackup | ||||
|                                 ? () { | ||||
|                                     ref | ||||
|                                         .read(backupProvider.notifier) | ||||
|                                         .startBackupProcess(); | ||||
|                                   } | ||||
|                                 : null, | ||||
|                             child: const Text("Start Backup"), | ||||
|                           ), | ||||
|               ), | ||||
|             ) | ||||
|  | ||||
							
								
								
									
										139
									
								
								mobile/lib/modules/backup/views/failed_backup_status_page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								mobile/lib/modules/backup/views/failed_backup_status_page.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,139 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart'; | ||||
| import 'package:intl/intl.dart'; | ||||
| import 'package:photo_manager/photo_manager.dart'; | ||||
| 
 | ||||
| class FailedBackupStatusPage extends HookConsumerWidget { | ||||
|   const FailedBackupStatusPage({Key? key}) : super(key: key); | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final errorBackupList = ref.watch(errorBackupListProvider); | ||||
| 
 | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         elevation: 0, | ||||
|         title: Text( | ||||
|           "Failed Backup (${errorBackupList.length})", | ||||
|           style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), | ||||
|         ), | ||||
|         leading: IconButton( | ||||
|             onPressed: () { | ||||
|               AutoRouter.of(context).pop(true); | ||||
|             }, | ||||
|             splashRadius: 24, | ||||
|             icon: const Icon( | ||||
|               Icons.arrow_back_ios_rounded, | ||||
|             )), | ||||
|       ), | ||||
|       body: ListView.builder( | ||||
|         shrinkWrap: true, | ||||
|         itemCount: errorBackupList.length, | ||||
|         itemBuilder: ((context, index) { | ||||
|           var errorAsset = errorBackupList.elementAt(index); | ||||
| 
 | ||||
|           return Padding( | ||||
|             padding: const EdgeInsets.symmetric( | ||||
|               horizontal: 12.0, | ||||
|               vertical: 4, | ||||
|             ), | ||||
|             child: Card( | ||||
|               shape: RoundedRectangleBorder( | ||||
|                 borderRadius: BorderRadius.circular(15), // if you need this | ||||
|                 side: const BorderSide( | ||||
|                   color: Colors.black12, | ||||
|                   width: 1, | ||||
|                 ), | ||||
|               ), | ||||
|               elevation: 0, | ||||
|               child: Row( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                 mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                 children: [ | ||||
|                   ConstrainedBox( | ||||
|                     constraints: const BoxConstraints( | ||||
|                       minWidth: 100, | ||||
|                       minHeight: 150, | ||||
|                       maxWidth: 100, | ||||
|                       maxHeight: 200, | ||||
|                     ), | ||||
|                     child: ClipRRect( | ||||
|                       borderRadius: const BorderRadius.only( | ||||
|                         bottomLeft: Radius.circular(15), | ||||
|                         topLeft: Radius.circular(15), | ||||
|                       ), | ||||
|                       clipBehavior: Clip.hardEdge, | ||||
|                       child: Image( | ||||
|                         fit: BoxFit.cover, | ||||
|                         image: AssetEntityImageProvider( | ||||
|                           errorAsset.asset, | ||||
|                           isOriginal: false, | ||||
|                           thumbnailSize: const ThumbnailSize.square(512), | ||||
|                           thumbnailFormat: ThumbnailFormat.jpeg, | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                   Expanded( | ||||
|                     child: Padding( | ||||
|                       padding: const EdgeInsets.all(16.0), | ||||
|                       child: Column( | ||||
|                         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                         mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||||
|                         children: [ | ||||
|                           Row( | ||||
|                             mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                             children: [ | ||||
|                               Text( | ||||
|                                 DateFormat.yMMMMd('en_US').format( | ||||
|                                   DateTime.parse( | ||||
|                                     errorAsset.createdAt.toString(), | ||||
|                                   ), | ||||
|                                 ), | ||||
|                                 style: TextStyle( | ||||
|                                     fontSize: 12, | ||||
|                                     fontWeight: FontWeight.w600, | ||||
|                                     color: Colors.grey[700]), | ||||
|                               ), | ||||
|                               Icon( | ||||
|                                 Icons.error, | ||||
|                                 color: Colors.red.withAlpha(200), | ||||
|                                 size: 18, | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                           Padding( | ||||
|                             padding: const EdgeInsets.symmetric(vertical: 8.0), | ||||
|                             child: Text( | ||||
|                               errorAsset.fileName, | ||||
|                               maxLines: 1, | ||||
|                               overflow: TextOverflow.ellipsis, | ||||
|                               style: TextStyle( | ||||
|                                 fontWeight: FontWeight.bold, | ||||
|                                 fontSize: 12, | ||||
|                                 color: Theme.of(context).primaryColor, | ||||
|                               ), | ||||
|                             ), | ||||
|                           ), | ||||
|                           Text( | ||||
|                             errorAsset.errorMessage, | ||||
|                             style: TextStyle( | ||||
|                               fontSize: 12, | ||||
|                               fontWeight: FontWeight.w500, | ||||
|                               color: Colors.grey[800], | ||||
|                             ), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ), | ||||
|                   ) | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           ); | ||||
|         }), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:immich_mobile/modules/backup/views/album_preview_page.dart'; | ||||
| import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart'; | ||||
| import 'package:immich_mobile/modules/backup/views/failed_backup_status_page.dart'; | ||||
| import 'package:immich_mobile/modules/login/views/change_password_page.dart'; | ||||
| import 'package:immich_mobile/modules/login/views/login_page.dart'; | ||||
| import 'package:immich_mobile/modules/home/views/home_page.dart'; | ||||
| @ -65,6 +66,11 @@ part 'router.gr.dart'; | ||||
|     ), | ||||
|     AutoRoute(page: BackupAlbumSelectionPage, guards: [AuthGuard]), | ||||
|     AutoRoute(page: AlbumPreviewPage, guards: [AuthGuard]), | ||||
|     CustomRoute( | ||||
|       page: FailedBackupStatusPage, | ||||
|       guards: [AuthGuard], | ||||
|       transitionsBuilder: TransitionsBuilders.slideBottom, | ||||
|     ), | ||||
|   ], | ||||
| ) | ||||
| class AppRouter extends _$AppRouter { | ||||
|  | ||||
| @ -115,6 +115,14 @@ class _$AppRouter extends RootStackRouter { | ||||
|           routeData: routeData, | ||||
|           child: AlbumPreviewPage(key: args.key, album: args.album)); | ||||
|     }, | ||||
|     FailedBackupStatusRoute.name: (routeData) { | ||||
|       return CustomPage<dynamic>( | ||||
|           routeData: routeData, | ||||
|           child: const FailedBackupStatusPage(), | ||||
|           transitionsBuilder: TransitionsBuilders.slideBottom, | ||||
|           opaque: true, | ||||
|           barrierDismissible: false); | ||||
|     }, | ||||
|     HomeRoute.name: (routeData) { | ||||
|       return MaterialPageX<dynamic>( | ||||
|           routeData: routeData, child: const HomePage()); | ||||
| @ -177,7 +185,9 @@ class _$AppRouter extends RootStackRouter { | ||||
|         RouteConfig(BackupAlbumSelectionRoute.name, | ||||
|             path: '/backup-album-selection-page', guards: [authGuard]), | ||||
|         RouteConfig(AlbumPreviewRoute.name, | ||||
|             path: '/album-preview-page', guards: [authGuard]) | ||||
|             path: '/album-preview-page', guards: [authGuard]), | ||||
|         RouteConfig(FailedBackupStatusRoute.name, | ||||
|             path: '/failed-backup-status-page', guards: [authGuard]) | ||||
|       ]; | ||||
| } | ||||
| 
 | ||||
| @ -437,6 +447,15 @@ class AlbumPreviewRouteArgs { | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /// generated route for | ||||
| /// [FailedBackupStatusPage] | ||||
| class FailedBackupStatusRoute extends PageRouteInfo<void> { | ||||
|   const FailedBackupStatusRoute() | ||||
|       : super(FailedBackupStatusRoute.name, path: '/failed-backup-status-page'); | ||||
| 
 | ||||
|   static const String name = 'FailedBackupStatusRoute'; | ||||
| } | ||||
| 
 | ||||
| /// generated route for | ||||
| /// [HomePage] | ||||
| class HomeRoute extends PageRouteInfo<void> { | ||||
|  | ||||
| @ -79,7 +79,7 @@ class NetworkService { | ||||
| 
 | ||||
|       return res; | ||||
|     } on DioError catch (e) { | ||||
|       debugPrint("DioError: ${e.response}"); | ||||
|       debugPrint("[postRequest] DioError: ${e.response}"); | ||||
|       return null; | ||||
|     } catch (e) { | ||||
|       debugPrint("ERROR PostRequest: $e"); | ||||
|  | ||||
| @ -2,7 +2,7 @@ name: immich_mobile | ||||
| description: Immich - selfhosted backup media file on mobile phone | ||||
| 
 | ||||
| publish_to: "none" | ||||
| version: 1.16.1+24 | ||||
| version: 1.17.0+25 | ||||
| 
 | ||||
| environment: | ||||
|   sdk: ">=2.17.0 <3.0.0" | ||||
|  | ||||
| @ -15,6 +15,7 @@ import { | ||||
|   Delete, | ||||
|   Logger, | ||||
|   HttpCode, | ||||
|   BadRequestException, | ||||
| } from '@nestjs/common'; | ||||
| import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard'; | ||||
| import { AssetService } from './asset.service'; | ||||
| @ -34,6 +35,7 @@ import { Queue } from 'bull'; | ||||
| import { IAssetUploadedJob } from '@app/job/index'; | ||||
| import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant'; | ||||
| import { assetUploadedProcessorName } from '@app/job/constants/job-name.constant'; | ||||
| import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto'; | ||||
| 
 | ||||
| @UseGuards(JwtAuthGuard) | ||||
| @Controller('asset') | ||||
| @ -66,17 +68,16 @@ export class AssetController { | ||||
|       try { | ||||
|         const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype); | ||||
| 
 | ||||
|         if (!savedAsset) { | ||||
|           return; | ||||
|         if (savedAsset) { | ||||
|           await this.assetUploadedQueue.add( | ||||
|             assetUploadedProcessorName, | ||||
|             { asset: savedAsset, fileName: file.originalname, fileSize: file.size }, | ||||
|             { jobId: savedAsset.id }, | ||||
|           ); | ||||
|         } | ||||
| 
 | ||||
|         await this.assetUploadedQueue.add( | ||||
|           assetUploadedProcessorName, | ||||
|           { asset: savedAsset, fileName: file.originalname, fileSize: file.size }, | ||||
|           { jobId: savedAsset.id }, | ||||
|         ); | ||||
|       } catch (e) { | ||||
|         Logger.error(`Error receiving upload file ${e}`); | ||||
|         Logger.error(`Error uploading file ${e}`); | ||||
|         throw new BadRequestException(`Error uploading file`, `${e}`); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
| @ -172,9 +173,9 @@ export class AssetController { | ||||
|   @HttpCode(200) | ||||
|   async checkDuplicateAsset( | ||||
|     @GetAuthUser() authUser: AuthUserDto, | ||||
|     @Body(ValidationPipe) { deviceAssetId }: { deviceAssetId: string }, | ||||
|     @Body(ValidationPipe) checkDuplicateAssetDto: CheckDuplicateAssetDto, | ||||
|   ) { | ||||
|     const res = await this.assetService.checkDuplicatedAsset(authUser, deviceAssetId); | ||||
|     const res = await this.assetService.checkDuplicatedAsset(authUser, checkDuplicateAssetDto); | ||||
| 
 | ||||
|     return { | ||||
|       isExist: res, | ||||
|  | ||||
| @ -18,6 +18,7 @@ import { promisify } from 'util'; | ||||
| import { DeleteAssetDto } from './dto/delete-asset.dto'; | ||||
| import { SearchAssetDto } from './dto/search-asset.dto'; | ||||
| import fs from 'fs/promises'; | ||||
| import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto'; | ||||
| 
 | ||||
| const fileInfo = promisify(stat); | ||||
| 
 | ||||
| @ -58,15 +59,11 @@ export class AssetService { | ||||
|     asset.mimeType = mimeType; | ||||
|     asset.duration = assetInfo.duration || null; | ||||
| 
 | ||||
|     try { | ||||
|       const createdAsset = await this.assetRepository.save(asset); | ||||
|       if (!createdAsset) { | ||||
|         throw new Error('Asset not created'); | ||||
|       } | ||||
|       return createdAsset; | ||||
|     } catch (e) { | ||||
|       Logger.error(`Error Create New Asset ${e}`, 'createUserAsset'); | ||||
|     const createdAsset = await this.assetRepository.save(asset); | ||||
|     if (!createdAsset) { | ||||
|       throw new Error('Asset not created'); | ||||
|     } | ||||
|     return createdAsset; | ||||
|   } | ||||
| 
 | ||||
|   public async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) { | ||||
| @ -439,10 +436,11 @@ export class AssetService { | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   async checkDuplicatedAsset(authUser: AuthUserDto, deviceAssetId: string) { | ||||
|   async checkDuplicatedAsset(authUser: AuthUserDto, checkDuplicateAssetDto: CheckDuplicateAssetDto) { | ||||
|     const res = await this.assetRepository.findOne({ | ||||
|       where: { | ||||
|         deviceAssetId, | ||||
|         deviceAssetId: checkDuplicateAssetDto.deviceAssetId, | ||||
|         deviceId: checkDuplicateAssetDto.deviceId, | ||||
|         userId: authUser.id, | ||||
|       }, | ||||
|     }); | ||||
|  | ||||
| @ -0,0 +1,9 @@ | ||||
| import { IsNotEmpty } from 'class-validator'; | ||||
| 
 | ||||
| export class CheckDuplicateAssetDto { | ||||
|   @IsNotEmpty() | ||||
|   deviceAssetId!: string; | ||||
| 
 | ||||
|   @IsNotEmpty() | ||||
|   deviceId!: string; | ||||
| } | ||||
| @ -3,7 +3,7 @@ | ||||
| 
 | ||||
| export const serverVersion = { | ||||
|   major: 1, | ||||
|   minor: 16, | ||||
|   minor: 17, | ||||
|   patch: 0, | ||||
|   build: 23, | ||||
|   build: 25, | ||||
| }; | ||||
|  | ||||
| @ -53,7 +53,7 @@ export async function fileUploader(asset: File, accessToken: string) { | ||||
| 		// Check if asset upload on server before performing upload
 | ||||
| 		const res = await fetch(serverEndpoint + '/asset/check', { | ||||
| 			method: 'POST', | ||||
| 			body: JSON.stringify({ deviceAssetId }), | ||||
| 			body: JSON.stringify({ deviceAssetId, deviceId: 'WEB' }), | ||||
| 			headers: { | ||||
| 				Authorization: 'Bearer ' + accessToken, | ||||
| 				'Content-Type': 'application/json', | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user