mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(mobile): use cached asset info if unchanged instead of downloading all assets (#1017)
* feat(mobile): use cached asset info if unchanged instead of downloading all assets This adds an HTTP ETag to the getAllAssets endpoint and client-side support in the app. If locally cache content is identical to the content on the server, the potentially large list of all assets does not need to be downloaded. * use ts import instead of require
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							efa7b3ba54
						
					
				
				
					commit
					47f5e4134e
				
			| @@ -4,6 +4,7 @@ const String accessTokenKey = "immichBoxAccessTokenKey"; // Key 1 | ||||
| const String deviceIdKey = 'immichBoxDeviceIdKey'; // Key 2 | ||||
| const String isLoggedInKey = 'immichIsLoggedInKey'; // Key 3 | ||||
| const String serverEndpointKey = 'immichBoxServerEndpoint'; // Key 4 | ||||
| const String assetEtagKey = 'immichAssetEtagKey'; // Key 5 | ||||
|  | ||||
| // Login Info | ||||
| const String hiveLoginInfoBox = "immichLoginInfoBox"; // Box | ||||
|   | ||||
| @@ -10,8 +10,9 @@ import 'package:immich_mobile/modules/backup/services/backup.service.dart'; | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
| import 'package:immich_mobile/shared/providers/api.provider.dart'; | ||||
| import 'package:immich_mobile/shared/services/api.service.dart'; | ||||
| import 'package:immich_mobile/utils/openapi_extensions.dart'; | ||||
| import 'package:immich_mobile/utils/tuple.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:photo_manager/photo_manager.dart'; | ||||
|  | ||||
| final assetServiceProvider = Provider( | ||||
|   (ref) => AssetService( | ||||
| @@ -28,39 +29,22 @@ class AssetService { | ||||
|  | ||||
|   AssetService(this._apiService, this._backupService, this._backgroundService); | ||||
|  | ||||
|   /// Returns all local, remote assets in that order | ||||
|   Future<List<Asset>> getAllAsset({bool urgent = false}) async { | ||||
|     final List<Asset> assets = []; | ||||
|     try { | ||||
|       // not using `await` here to fetch local & remote assets concurrently | ||||
|       final Future<List<AssetResponseDto>?> remoteTask = | ||||
|           _apiService.assetApi.getAllAssets(); | ||||
|       final Iterable<AssetEntity> newLocalAssets; | ||||
|       final List<AssetEntity> localAssets = await _getLocalAssets(urgent); | ||||
|       final List<AssetResponseDto> remoteAssets = await remoteTask ?? []; | ||||
|       if (remoteAssets.isNotEmpty && localAssets.isNotEmpty) { | ||||
|         final String deviceId = Hive.box(userInfoBox).get(deviceIdKey); | ||||
|         final Set<String> existingIds = remoteAssets | ||||
|             .where((e) => e.deviceId == deviceId) | ||||
|             .map((e) => e.deviceAssetId) | ||||
|             .toSet(); | ||||
|         newLocalAssets = localAssets.where((e) => !existingIds.contains(e.id)); | ||||
|       } else { | ||||
|         newLocalAssets = localAssets; | ||||
|       } | ||||
|  | ||||
|       assets.addAll(newLocalAssets.map((e) => Asset.local(e))); | ||||
|       // the order (first all local, then remote assets) is important! | ||||
|       assets.addAll(remoteAssets.map((e) => Asset.remote(e))); | ||||
|     } catch (e) { | ||||
|       debugPrint("Error [getAllAsset]  ${e.toString()}"); | ||||
|   /// Returns `null` if the server state did not change, else list of assets | ||||
|   Future<List<Asset>?> getRemoteAssets() async { | ||||
|     final Box box = Hive.box(userInfoBox); | ||||
|     final Pair<List<AssetResponseDto>, String?>? remote = await _apiService | ||||
|         .assetApi | ||||
|         .getAllAssetsWithETag(eTag: box.get(assetEtagKey)); | ||||
|     if (remote == null) { | ||||
|       return null; | ||||
|     } | ||||
|     return assets; | ||||
|     box.put(assetEtagKey, remote.second); | ||||
|     return remote.first.map(Asset.remote).toList(growable: false); | ||||
|   } | ||||
|  | ||||
|   /// if [urgent] is `true`, do not block by waiting on the background service | ||||
|   /// to finish running. Returns an empty list instead after a timeout. | ||||
|   Future<List<AssetEntity>> _getLocalAssets(bool urgent) async { | ||||
|   /// to finish running. Returns `null` instead after a timeout. | ||||
|   Future<List<Asset>?> getLocalAssets({bool urgent = false}) async { | ||||
|     try { | ||||
|       final Future<bool> hasAccess = urgent | ||||
|           ? _backgroundService.hasAccess | ||||
| @@ -71,15 +55,16 @@ class AssetService { | ||||
|       } | ||||
|       final box = await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox); | ||||
|       final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey); | ||||
|  | ||||
|       return backupAlbumInfo != null | ||||
|           ? await _backupService | ||||
|               .buildUploadCandidates(backupAlbumInfo.deepCopy()) | ||||
|           : []; | ||||
|       if (backupAlbumInfo != null) { | ||||
|         return (await _backupService | ||||
|                 .buildUploadCandidates(backupAlbumInfo.deepCopy())) | ||||
|             .map(Asset.local) | ||||
|             .toList(growable: false); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       debugPrint("Error [_getLocalAssets] ${e.toString()}"); | ||||
|       return []; | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   Future<Asset?> getAssetById(String assetId) async { | ||||
|   | ||||
| @@ -1,7 +1,9 @@ | ||||
| import 'dart:collection'; | ||||
|  | ||||
| import 'package:flutter/foundation.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/home/services/asset.service.dart'; | ||||
| import 'package:immich_mobile/modules/home/services/asset_cache.service.dart'; | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
| @@ -33,10 +35,11 @@ class AssetNotifier extends StateNotifier<List<Asset>> { | ||||
|     final stopwatch = Stopwatch(); | ||||
|     try { | ||||
|       _getAllAssetInProgress = true; | ||||
|  | ||||
|       final bool isCacheValid = await _assetCacheService.isValid(); | ||||
|       stopwatch.start(); | ||||
|       final localTask = _assetService.getLocalAssets(urgent: !isCacheValid); | ||||
|       final remoteTask = _assetService.getRemoteAssets(); | ||||
|       if (isCacheValid && state.isEmpty) { | ||||
|         stopwatch.start(); | ||||
|         state = await _assetCacheService.get(); | ||||
|         debugPrint( | ||||
|           "Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms", | ||||
| @@ -44,21 +47,49 @@ class AssetNotifier extends StateNotifier<List<Asset>> { | ||||
|         stopwatch.reset(); | ||||
|       } | ||||
|  | ||||
|       stopwatch.start(); | ||||
|       var allAssets = await _assetService.getAllAsset(urgent: !isCacheValid); | ||||
|       debugPrint("Query assets from API: ${stopwatch.elapsedMilliseconds}ms"); | ||||
|       int remoteBegin = state.indexWhere((a) => a.isRemote); | ||||
|       remoteBegin = remoteBegin == -1 ? state.length : remoteBegin; | ||||
|       final List<Asset> currentLocal = state.slice(0, remoteBegin); | ||||
|       List<Asset>? newRemote = await remoteTask; | ||||
|       List<Asset>? newLocal = await localTask; | ||||
|       debugPrint("Load assets: ${stopwatch.elapsedMilliseconds}ms"); | ||||
|       stopwatch.reset(); | ||||
|  | ||||
|       state = allAssets; | ||||
|       if (newRemote == null && | ||||
|           (newLocal == null || currentLocal.equals(newLocal))) { | ||||
|         debugPrint("state is already up-to-date"); | ||||
|         return; | ||||
|       } | ||||
|       newRemote ??= state.slice(remoteBegin); | ||||
|       newLocal ??= []; | ||||
|       state = _combineLocalAndRemoteAssets(local: newLocal, remote: newRemote); | ||||
|       debugPrint("Combining assets: ${stopwatch.elapsedMilliseconds}ms"); | ||||
|     } finally { | ||||
|       _getAllAssetInProgress = false; | ||||
|     } | ||||
|     debugPrint("[getAllAsset] setting new asset state"); | ||||
|  | ||||
|     stopwatch.start(); | ||||
|     stopwatch.reset(); | ||||
|     _cacheState(); | ||||
|     debugPrint("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms"); | ||||
|     stopwatch.reset(); | ||||
|   } | ||||
|  | ||||
|   List<Asset> _combineLocalAndRemoteAssets({ | ||||
|     required Iterable<Asset> local, | ||||
|     required List<Asset> remote, | ||||
|   }) { | ||||
|     final List<Asset> assets = []; | ||||
|     if (remote.isNotEmpty && local.isNotEmpty) { | ||||
|       final String deviceId = Hive.box(userInfoBox).get(deviceIdKey); | ||||
|       final Set<String> existingIds = remote | ||||
|           .where((e) => e.deviceId == deviceId) | ||||
|           .map((e) => e.deviceAssetId) | ||||
|           .toSet(); | ||||
|       local = local.where((e) => !existingIds.contains(e.id)); | ||||
|     } | ||||
|     assets.addAll(local); | ||||
|     // the order (first all local, then remote assets) is important! | ||||
|     assets.addAll(remote); | ||||
|     return assets; | ||||
|   } | ||||
|  | ||||
|   clearAllAsset() { | ||||
|   | ||||
							
								
								
									
										53
									
								
								mobile/lib/utils/openapi_extensions.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								mobile/lib/utils/openapi_extensions.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| import 'dart:convert'; | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| import 'tuple.dart'; | ||||
|  | ||||
| /// Extension methods to retrieve ETag together with the API call | ||||
| extension WithETag on AssetApi { | ||||
|   /// Get all AssetEntity belong to the user | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [String] eTag: | ||||
|   ///   ETag of data already cached on the client | ||||
|   Future<Pair<List<AssetResponseDto>, String?>?> getAllAssetsWithETag({ | ||||
|     String? eTag, | ||||
|   }) async { | ||||
|     final response = await getAllAssetsWithHttpInfo( | ||||
|       ifNoneMatch: eTag, | ||||
|     ); | ||||
|     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) { | ||||
|       final responseBody = await _decodeBodyBytes(response); | ||||
|       final etag = response.headers[HttpHeaders.etagHeader]; | ||||
|       final data = (await apiClient.deserializeAsync( | ||||
|               responseBody, 'List<AssetResponseDto>') as List) | ||||
|           .cast<AssetResponseDto>() | ||||
|           .toList(); | ||||
|       return Pair(data, etag); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// Returns the decoded body as UTF-8 if the given headers indicate an 'application/json' | ||||
| /// content type. Otherwise, returns the decoded body as decoded by dart:http package. | ||||
| Future<String> _decodeBodyBytes(Response response) async { | ||||
|   final contentType = response.headers['content-type']; | ||||
|   return contentType != null && | ||||
|           contentType.toLowerCase().startsWith('application/json') | ||||
|       ? response.bodyBytes.isEmpty | ||||
|           ? '' | ||||
|           : utf8.decode(response.bodyBytes) | ||||
|       : response.body; | ||||
| } | ||||
							
								
								
									
										8
									
								
								mobile/lib/utils/tuple.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								mobile/lib/utils/tuple.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| /// An immutable pair or 2-tuple | ||||
| /// TODO replace with Record once Dart 2.19 is available | ||||
| class Pair<T1, T2> { | ||||
|   final T1 first; | ||||
|   final T2 second; | ||||
|  | ||||
|   const Pair(this.first, this.second); | ||||
| } | ||||
| @@ -274,7 +274,7 @@ 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) | ||||
|  | ||||
| # **getAllAssets** | ||||
| > List<AssetResponseDto> getAllAssets() | ||||
| > List<AssetResponseDto> getAllAssets(ifNoneMatch) | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -291,9 +291,10 @@ import 'package:openapi/api.dart'; | ||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction); | ||||
|  | ||||
| final api_instance = AssetApi(); | ||||
| final ifNoneMatch = ifNoneMatch_example; // String | ETag of data already cached on the client | ||||
|  | ||||
| try { | ||||
|     final result = api_instance.getAllAssets(); | ||||
|     final result = api_instance.getAllAssets(ifNoneMatch); | ||||
|     print(result); | ||||
| } catch (e) { | ||||
|     print('Exception when calling AssetApi->getAllAssets: $e\n'); | ||||
| @@ -301,7 +302,10 @@ try { | ||||
| ``` | ||||
|  | ||||
| ### Parameters | ||||
| This endpoint does not need any parameter. | ||||
|  | ||||
| Name | Type | Description  | Notes | ||||
| ------------- | ------------- | ------------- | ------------- | ||||
|  **ifNoneMatch** | **String**| ETag of data already cached on the client | [optional]  | ||||
|  | ||||
| ### Return type | ||||
|  | ||||
|   | ||||
| @@ -21,10 +21,10 @@ Name | Type | Description | Notes | ||||
| **mimeType** | **String** |  |  | ||||
| **duration** | **String** |  |  | ||||
| **webpPath** | **String** |  |  | ||||
| **encodedVideoPath** | **String** |  |  | ||||
| **encodedVideoPath** | **String** |  | [optional]  | ||||
| **exifInfo** | [**ExifResponseDto**](ExifResponseDto.md) |  | [optional]  | ||||
| **smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) |  | [optional]  | ||||
| **livePhotoVideoId** | **String** |  |  | ||||
| **livePhotoVideoId** | **String** |  | [optional]  | ||||
|  | ||||
| [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | ||||
|  | ||||
|   | ||||
| @@ -16,7 +16,7 @@ Name | Type | Description | Notes | ||||
| **profileImagePath** | **String** |  |  | ||||
| **shouldChangePassword** | **bool** |  |  | ||||
| **isAdmin** | **bool** |  |  | ||||
| **deletedAt** | [**DateTime**](DateTime.md) |  |  | ||||
| **deletedAt** | [**DateTime**](DateTime.md) |  | [optional]  | ||||
|  | ||||
| [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | ||||
|  | ||||
|   | ||||
| @@ -297,7 +297,12 @@ class AssetApi { | ||||
|   /// Get all AssetEntity belong to the user | ||||
|   /// | ||||
|   /// Note: This method returns the HTTP [Response]. | ||||
|   Future<Response> getAllAssetsWithHttpInfo() async { | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [String] ifNoneMatch: | ||||
|   ///   ETag of data already cached on the client | ||||
|   Future<Response> getAllAssetsWithHttpInfo({ String? ifNoneMatch, }) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final path = r'/asset'; | ||||
|  | ||||
| @@ -308,6 +313,10 @@ class AssetApi { | ||||
|     final headerParams = <String, String>{}; | ||||
|     final formParams = <String, String>{}; | ||||
|  | ||||
|     if (ifNoneMatch != null) { | ||||
|       headerParams[r'if-none-match'] = parameterToString(ifNoneMatch); | ||||
|     } | ||||
|  | ||||
|     const contentTypes = <String>[]; | ||||
|  | ||||
|  | ||||
| @@ -325,8 +334,13 @@ class AssetApi { | ||||
|   ///  | ||||
|   /// | ||||
|   /// Get all AssetEntity belong to the user | ||||
|   Future<List<AssetResponseDto>?> getAllAssets() async { | ||||
|     final response = await getAllAssetsWithHttpInfo(); | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [String] ifNoneMatch: | ||||
|   ///   ETag of data already cached on the client | ||||
|   Future<List<AssetResponseDto>?> getAllAssets({ String? ifNoneMatch, }) async { | ||||
|     final response = await getAllAssetsWithHttpInfo( ifNoneMatch: ifNoneMatch, ); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
|   | ||||
| @@ -26,10 +26,10 @@ class AssetResponseDto { | ||||
|     required this.mimeType, | ||||
|     required this.duration, | ||||
|     required this.webpPath, | ||||
|     required this.encodedVideoPath, | ||||
|     this.encodedVideoPath, | ||||
|     this.exifInfo, | ||||
|     this.smartInfo, | ||||
|     required this.livePhotoVideoId, | ||||
|     this.livePhotoVideoId, | ||||
|   }); | ||||
|  | ||||
|   AssetTypeEnum type; | ||||
| @@ -79,74 +79,71 @@ class AssetResponseDto { | ||||
|   String? livePhotoVideoId; | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) => | ||||
|       identical(this, other) || | ||||
|       other is AssetResponseDto && | ||||
|           other.type == type && | ||||
|           other.id == id && | ||||
|           other.deviceAssetId == deviceAssetId && | ||||
|           other.ownerId == ownerId && | ||||
|           other.deviceId == deviceId && | ||||
|           other.originalPath == originalPath && | ||||
|           other.resizePath == resizePath && | ||||
|           other.createdAt == createdAt && | ||||
|           other.modifiedAt == modifiedAt && | ||||
|           other.isFavorite == isFavorite && | ||||
|           other.mimeType == mimeType && | ||||
|           other.duration == duration && | ||||
|           other.webpPath == webpPath && | ||||
|           other.encodedVideoPath == encodedVideoPath && | ||||
|           other.exifInfo == exifInfo && | ||||
|           other.smartInfo == smartInfo && | ||||
|           other.livePhotoVideoId == livePhotoVideoId; | ||||
|   bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto && | ||||
|      other.type == type && | ||||
|      other.id == id && | ||||
|      other.deviceAssetId == deviceAssetId && | ||||
|      other.ownerId == ownerId && | ||||
|      other.deviceId == deviceId && | ||||
|      other.originalPath == originalPath && | ||||
|      other.resizePath == resizePath && | ||||
|      other.createdAt == createdAt && | ||||
|      other.modifiedAt == modifiedAt && | ||||
|      other.isFavorite == isFavorite && | ||||
|      other.mimeType == mimeType && | ||||
|      other.duration == duration && | ||||
|      other.webpPath == webpPath && | ||||
|      other.encodedVideoPath == encodedVideoPath && | ||||
|      other.exifInfo == exifInfo && | ||||
|      other.smartInfo == smartInfo && | ||||
|      other.livePhotoVideoId == livePhotoVideoId; | ||||
|  | ||||
|   @override | ||||
|   int get hashCode => | ||||
|       // ignore: unnecessary_parenthesis | ||||
|       (type.hashCode) + | ||||
|       (id.hashCode) + | ||||
|       (deviceAssetId.hashCode) + | ||||
|       (ownerId.hashCode) + | ||||
|       (deviceId.hashCode) + | ||||
|       (originalPath.hashCode) + | ||||
|       (resizePath == null ? 0 : resizePath!.hashCode) + | ||||
|       (createdAt.hashCode) + | ||||
|       (modifiedAt.hashCode) + | ||||
|       (isFavorite.hashCode) + | ||||
|       (mimeType == null ? 0 : mimeType!.hashCode) + | ||||
|       (duration.hashCode) + | ||||
|       (webpPath == null ? 0 : webpPath!.hashCode) + | ||||
|       (encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) + | ||||
|       (exifInfo == null ? 0 : exifInfo!.hashCode) + | ||||
|       (smartInfo == null ? 0 : smartInfo!.hashCode) + | ||||
|       (livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode); | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (type.hashCode) + | ||||
|     (id.hashCode) + | ||||
|     (deviceAssetId.hashCode) + | ||||
|     (ownerId.hashCode) + | ||||
|     (deviceId.hashCode) + | ||||
|     (originalPath.hashCode) + | ||||
|     (resizePath == null ? 0 : resizePath!.hashCode) + | ||||
|     (createdAt.hashCode) + | ||||
|     (modifiedAt.hashCode) + | ||||
|     (isFavorite.hashCode) + | ||||
|     (mimeType == null ? 0 : mimeType!.hashCode) + | ||||
|     (duration.hashCode) + | ||||
|     (webpPath == null ? 0 : webpPath!.hashCode) + | ||||
|     (encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) + | ||||
|     (exifInfo == null ? 0 : exifInfo!.hashCode) + | ||||
|     (smartInfo == null ? 0 : smartInfo!.hashCode) + | ||||
|     (livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode); | ||||
|  | ||||
|   @override | ||||
|   String toString() => | ||||
|       'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId]'; | ||||
|   String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId]'; | ||||
|  | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final _json = <String, dynamic>{}; | ||||
|     _json[r'type'] = type; | ||||
|     _json[r'id'] = id; | ||||
|     _json[r'deviceAssetId'] = deviceAssetId; | ||||
|     _json[r'ownerId'] = ownerId; | ||||
|     _json[r'deviceId'] = deviceId; | ||||
|     _json[r'originalPath'] = originalPath; | ||||
|       _json[r'type'] = type; | ||||
|       _json[r'id'] = id; | ||||
|       _json[r'deviceAssetId'] = deviceAssetId; | ||||
|       _json[r'ownerId'] = ownerId; | ||||
|       _json[r'deviceId'] = deviceId; | ||||
|       _json[r'originalPath'] = originalPath; | ||||
|     if (resizePath != null) { | ||||
|       _json[r'resizePath'] = resizePath; | ||||
|     } else { | ||||
|       _json[r'resizePath'] = null; | ||||
|     } | ||||
|     _json[r'createdAt'] = createdAt; | ||||
|     _json[r'modifiedAt'] = modifiedAt; | ||||
|     _json[r'isFavorite'] = isFavorite; | ||||
|       _json[r'createdAt'] = createdAt; | ||||
|       _json[r'modifiedAt'] = modifiedAt; | ||||
|       _json[r'isFavorite'] = isFavorite; | ||||
|     if (mimeType != null) { | ||||
|       _json[r'mimeType'] = mimeType; | ||||
|     } else { | ||||
|       _json[r'mimeType'] = null; | ||||
|     } | ||||
|     _json[r'duration'] = duration; | ||||
|       _json[r'duration'] = duration; | ||||
|     if (webpPath != null) { | ||||
|       _json[r'webpPath'] = webpPath; | ||||
|     } else { | ||||
| @@ -185,13 +182,13 @@ class AssetResponseDto { | ||||
|       // 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 "AssetResponseDto[$key]" is missing from JSON.'); | ||||
|       //     assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.'); | ||||
|       //   }); | ||||
|       //   return true; | ||||
|       // }()); | ||||
|       assert(() { | ||||
|         requiredKeys.forEach((key) { | ||||
|           assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.'); | ||||
|           assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.'); | ||||
|         }); | ||||
|         return true; | ||||
|       }()); | ||||
|  | ||||
|       return AssetResponseDto( | ||||
|         type: AssetTypeEnum.fromJson(json[r'type'])!, | ||||
| @@ -216,10 +213,7 @@ class AssetResponseDto { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   static List<AssetResponseDto>? listFromJson( | ||||
|     dynamic json, { | ||||
|     bool growable = false, | ||||
|   }) { | ||||
|   static List<AssetResponseDto>? listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <AssetResponseDto>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
| @@ -247,18 +241,12 @@ class AssetResponseDto { | ||||
|   } | ||||
|  | ||||
|   // maps a json object with a list of AssetResponseDto-objects as value to a dart map | ||||
|   static Map<String, List<AssetResponseDto>> mapListFromJson( | ||||
|     dynamic json, { | ||||
|     bool growable = false, | ||||
|   }) { | ||||
|   static Map<String, List<AssetResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<AssetResponseDto>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = AssetResponseDto.listFromJson( | ||||
|           entry.value, | ||||
|           growable: growable, | ||||
|         ); | ||||
|         final value = AssetResponseDto.listFromJson(entry.value, growable: growable,); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
| @@ -282,7 +270,6 @@ class AssetResponseDto { | ||||
|     'mimeType', | ||||
|     'duration', | ||||
|     'webpPath', | ||||
|     'encodedVideoPath', | ||||
|     'livePhotoVideoId', | ||||
|   }; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -21,7 +21,7 @@ class UserResponseDto { | ||||
|     required this.profileImagePath, | ||||
|     required this.shouldChangePassword, | ||||
|     required this.isAdmin, | ||||
|     required this.deletedAt, | ||||
|     this.deletedAt, | ||||
|   }); | ||||
|  | ||||
|   String id; | ||||
| @@ -40,49 +40,52 @@ class UserResponseDto { | ||||
|  | ||||
|   bool isAdmin; | ||||
|  | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
|   /// source code must fall back to having a nullable type. | ||||
|   /// Consider adding a "default:" property in the specification file to hide this note. | ||||
|   /// | ||||
|   DateTime? deletedAt; | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) => | ||||
|       identical(this, other) || | ||||
|       other is UserResponseDto && | ||||
|           other.id == id && | ||||
|           other.email == email && | ||||
|           other.firstName == firstName && | ||||
|           other.lastName == lastName && | ||||
|           other.createdAt == createdAt && | ||||
|           other.profileImagePath == profileImagePath && | ||||
|           other.shouldChangePassword == shouldChangePassword && | ||||
|           other.isAdmin == isAdmin && | ||||
|           other.deletedAt == deletedAt; | ||||
|   bool operator ==(Object other) => identical(this, other) || other is UserResponseDto && | ||||
|      other.id == id && | ||||
|      other.email == email && | ||||
|      other.firstName == firstName && | ||||
|      other.lastName == lastName && | ||||
|      other.createdAt == createdAt && | ||||
|      other.profileImagePath == profileImagePath && | ||||
|      other.shouldChangePassword == shouldChangePassword && | ||||
|      other.isAdmin == isAdmin && | ||||
|      other.deletedAt == deletedAt; | ||||
|  | ||||
|   @override | ||||
|   int get hashCode => | ||||
|       // ignore: unnecessary_parenthesis | ||||
|       (id.hashCode) + | ||||
|       (email.hashCode) + | ||||
|       (firstName.hashCode) + | ||||
|       (lastName.hashCode) + | ||||
|       (createdAt.hashCode) + | ||||
|       (profileImagePath.hashCode) + | ||||
|       (shouldChangePassword.hashCode) + | ||||
|       (isAdmin.hashCode) + | ||||
|       (deletedAt == null ? 0 : deletedAt!.hashCode); | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (id.hashCode) + | ||||
|     (email.hashCode) + | ||||
|     (firstName.hashCode) + | ||||
|     (lastName.hashCode) + | ||||
|     (createdAt.hashCode) + | ||||
|     (profileImagePath.hashCode) + | ||||
|     (shouldChangePassword.hashCode) + | ||||
|     (isAdmin.hashCode) + | ||||
|     (deletedAt == null ? 0 : deletedAt!.hashCode); | ||||
|  | ||||
|   @override | ||||
|   String toString() => | ||||
|       'UserResponseDto[id=$id, email=$email, firstName=$firstName, lastName=$lastName, createdAt=$createdAt, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, isAdmin=$isAdmin, deletedAt=$deletedAt]'; | ||||
|   String toString() => 'UserResponseDto[id=$id, email=$email, firstName=$firstName, lastName=$lastName, createdAt=$createdAt, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, isAdmin=$isAdmin, deletedAt=$deletedAt]'; | ||||
|  | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final _json = <String, dynamic>{}; | ||||
|     _json[r'id'] = id; | ||||
|     _json[r'email'] = email; | ||||
|     _json[r'firstName'] = firstName; | ||||
|     _json[r'lastName'] = lastName; | ||||
|     _json[r'createdAt'] = createdAt; | ||||
|     _json[r'profileImagePath'] = profileImagePath; | ||||
|     _json[r'shouldChangePassword'] = shouldChangePassword; | ||||
|     _json[r'isAdmin'] = isAdmin; | ||||
|       _json[r'id'] = id; | ||||
|       _json[r'email'] = email; | ||||
|       _json[r'firstName'] = firstName; | ||||
|       _json[r'lastName'] = lastName; | ||||
|       _json[r'createdAt'] = createdAt; | ||||
|       _json[r'profileImagePath'] = profileImagePath; | ||||
|       _json[r'shouldChangePassword'] = shouldChangePassword; | ||||
|       _json[r'isAdmin'] = isAdmin; | ||||
|     if (deletedAt != null) { | ||||
|       _json[r'deletedAt'] = deletedAt!.toUtc().toIso8601String(); | ||||
|     } else { | ||||
| @@ -101,13 +104,13 @@ class UserResponseDto { | ||||
|       // 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 "UserResponseDto[$key]" is missing from JSON.'); | ||||
|       //     assert(json[key] != null, 'Required key "UserResponseDto[$key]" has a null value in JSON.'); | ||||
|       //   }); | ||||
|       //   return true; | ||||
|       // }()); | ||||
|       assert(() { | ||||
|         requiredKeys.forEach((key) { | ||||
|           assert(json.containsKey(key), 'Required key "UserResponseDto[$key]" is missing from JSON.'); | ||||
|           assert(json[key] != null, 'Required key "UserResponseDto[$key]" has a null value in JSON.'); | ||||
|         }); | ||||
|         return true; | ||||
|       }()); | ||||
|  | ||||
|       return UserResponseDto( | ||||
|         id: mapValueOfType<String>(json, r'id')!, | ||||
| @@ -116,8 +119,7 @@ class UserResponseDto { | ||||
|         lastName: mapValueOfType<String>(json, r'lastName')!, | ||||
|         createdAt: mapValueOfType<String>(json, r'createdAt')!, | ||||
|         profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!, | ||||
|         shouldChangePassword: | ||||
|             mapValueOfType<bool>(json, r'shouldChangePassword')!, | ||||
|         shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!, | ||||
|         isAdmin: mapValueOfType<bool>(json, r'isAdmin')!, | ||||
|         deletedAt: mapDateTime(json, r'deletedAt', ''), | ||||
|       ); | ||||
| @@ -125,10 +127,7 @@ class UserResponseDto { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   static List<UserResponseDto>? listFromJson( | ||||
|     dynamic json, { | ||||
|     bool growable = false, | ||||
|   }) { | ||||
|   static List<UserResponseDto>? listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <UserResponseDto>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
| @@ -156,18 +155,12 @@ class UserResponseDto { | ||||
|   } | ||||
|  | ||||
|   // maps a json object with a list of UserResponseDto-objects as value to a dart map | ||||
|   static Map<String, List<UserResponseDto>> mapListFromJson( | ||||
|     dynamic json, { | ||||
|     bool growable = false, | ||||
|   }) { | ||||
|   static Map<String, List<UserResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<UserResponseDto>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = UserResponseDto.listFromJson( | ||||
|           entry.value, | ||||
|           growable: growable, | ||||
|         ); | ||||
|         final value = UserResponseDto.listFromJson(entry.value, growable: growable,); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
| @@ -186,6 +179,6 @@ class UserResponseDto { | ||||
|     'profileImagePath', | ||||
|     'shouldChangePassword', | ||||
|     'isAdmin', | ||||
|     'deletedAt', | ||||
|   }; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import { | ||||
|   Header, | ||||
|   Put, | ||||
|   UploadedFiles, | ||||
|   Request, | ||||
| } from '@nestjs/common'; | ||||
| import { Authenticated } from '../../decorators/authenticated.decorator'; | ||||
| import { AssetService } from './asset.service'; | ||||
| @@ -21,12 +22,12 @@ import { FileFieldsInterceptor } from '@nestjs/platform-express'; | ||||
| import { assetUploadOption } from '../../config/asset-upload.config'; | ||||
| import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator'; | ||||
| import { ServeFileDto } from './dto/serve-file.dto'; | ||||
| import { Response as Res } from 'express'; | ||||
| import { Response as Res, Request as Req } from 'express'; | ||||
| import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; | ||||
| import { DeleteAssetDto } from './dto/delete-asset.dto'; | ||||
| import { SearchAssetDto } from './dto/search-asset.dto'; | ||||
| import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto'; | ||||
| import { ApiBearerAuth, ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; | ||||
| import { ApiBearerAuth, ApiBody, ApiConsumes, ApiHeader, ApiResponse, ApiTags } from '@nestjs/swagger'; | ||||
| import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto'; | ||||
| import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto'; | ||||
| import { AssetResponseDto } from './response-dto/asset-response.dto'; | ||||
| @@ -49,6 +50,7 @@ import { | ||||
|   IMMICH_ARCHIVE_FILE_COUNT, | ||||
|   IMMICH_CONTENT_LENGTH_HINT, | ||||
| } from '../../constants/download.constant'; | ||||
| import { etag } from '../../utils/etag'; | ||||
|  | ||||
| @Authenticated() | ||||
| @ApiBearerAuth() | ||||
| @@ -168,8 +170,28 @@ export class AssetController { | ||||
|    * Get all AssetEntity belong to the user | ||||
|    */ | ||||
|   @Get('/') | ||||
|   async getAllAssets(@GetAuthUser() authUser: AuthUserDto): Promise<AssetResponseDto[]> { | ||||
|     return await this.assetService.getAllAssets(authUser); | ||||
|   @ApiHeader({ | ||||
|     name: 'if-none-match', | ||||
|     description: 'ETag of data already cached on the client', | ||||
|     required: false, | ||||
|     schema: { type: 'string' }, | ||||
|   }) | ||||
|   @ApiResponse({ | ||||
|     status: 200, | ||||
|     headers: { ETag: { required: true, schema: { type: 'string' } } }, | ||||
|     type: [AssetResponseDto], | ||||
|   }) | ||||
|   async getAllAssets(@GetAuthUser() authUser: AuthUserDto, @Response() response: Res, @Request() request: Req) { | ||||
|     const assets = await this.assetService.getAllAssets(authUser); | ||||
|     const clientEtag = request.headers['if-none-match']; | ||||
|     const json = JSON.stringify(assets); | ||||
|     const serverEtag = await etag(json); | ||||
|     response.setHeader('ETag', serverEtag); | ||||
|     if (clientEtag === serverEtag) { | ||||
|       response.status(304).end(); | ||||
|     } else { | ||||
|       response.contentType('application/json').status(200).send(json); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Post('/time-bucket') | ||||
|   | ||||
| @@ -19,10 +19,10 @@ export class AssetResponseDto { | ||||
|   mimeType!: string | null; | ||||
|   duration!: string; | ||||
|   webpPath!: string | null; | ||||
|   encodedVideoPath!: string | null; | ||||
|   encodedVideoPath?: string | null; | ||||
|   exifInfo?: ExifResponseDto; | ||||
|   smartInfo?: SmartInfoResponseDto; | ||||
|   livePhotoVideoId!: string | null; | ||||
|   livePhotoVideoId?: string | null; | ||||
| } | ||||
|  | ||||
| export function mapAsset(entity: AssetEntity): AssetResponseDto { | ||||
|   | ||||
| @@ -9,7 +9,7 @@ export class UserResponseDto { | ||||
|   profileImagePath!: string; | ||||
|   shouldChangePassword!: boolean; | ||||
|   isAdmin!: boolean; | ||||
|   deletedAt!: Date | null; | ||||
|   deletedAt?: Date; | ||||
| } | ||||
|  | ||||
| export function mapUser(entity: UserEntity): UserResponseDto { | ||||
| @@ -22,6 +22,6 @@ export function mapUser(entity: UserEntity): UserResponseDto { | ||||
|     profileImagePath: entity.profileImagePath, | ||||
|     shouldChangePassword: entity.shouldChangePassword, | ||||
|     isAdmin: entity.isAdmin, | ||||
|     deletedAt: entity.deletedAt || null, | ||||
|     deletedAt: entity.deletedAt, | ||||
|   }; | ||||
| } | ||||
|   | ||||
							
								
								
									
										5
									
								
								server/apps/immich/src/types/index.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								server/apps/immich/src/types/index.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| declare module 'crypto' { | ||||
|   namespace webcrypto { | ||||
|     const subtle: SubtleCrypto; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										10
									
								
								server/apps/immich/src/utils/etag.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								server/apps/immich/src/utils/etag.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import { webcrypto } from 'node:crypto'; | ||||
| const { subtle } = webcrypto; | ||||
|  | ||||
| export async function etag(text: string): Promise<string> { | ||||
|     const encoder = new TextEncoder(); | ||||
|     const data = encoder.encode(text); | ||||
|     const buffer = await subtle.digest('SHA-1', data); | ||||
|     const hash = Buffer.from(buffer).toString('base64').slice(0, 27); | ||||
|     return `"${data.length}-${hash}"`; | ||||
| } | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -427,7 +427,7 @@ export interface AssetResponseDto { | ||||
|      * @type {string} | ||||
|      * @memberof AssetResponseDto | ||||
|      */ | ||||
|     'encodedVideoPath': string | null; | ||||
|     'encodedVideoPath'?: string | null; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {ExifResponseDto} | ||||
| @@ -445,7 +445,7 @@ export interface AssetResponseDto { | ||||
|      * @type {string} | ||||
|      * @memberof AssetResponseDto | ||||
|      */ | ||||
|     'livePhotoVideoId': string | null; | ||||
|     'livePhotoVideoId'?: string | null; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
| @@ -1729,7 +1729,7 @@ export interface UserResponseDto { | ||||
|      * @type {string} | ||||
|      * @memberof UserResponseDto | ||||
|      */ | ||||
|     'deletedAt': string | null; | ||||
|     'deletedAt'?: string; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
| @@ -2788,10 +2788,11 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | ||||
|         /** | ||||
|          * Get all AssetEntity belong to the user | ||||
|          * @summary  | ||||
|          * @param {string} [ifNoneMatch] ETag of data already cached on the client | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         getAllAssets: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|         getAllAssets: async (ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             const localVarPath = `/asset`; | ||||
|             // use dummy base URL string because the URL constructor only accepts absolute URLs. | ||||
|             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); | ||||
| @@ -2808,6 +2809,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | ||||
|             // http bearer authentication required | ||||
|             await setBearerAuthToObject(localVarHeaderParameter, configuration) | ||||
|  | ||||
|             if (ifNoneMatch !== undefined && ifNoneMatch !== null) { | ||||
|                 localVarHeaderParameter['if-none-match'] = String(ifNoneMatch); | ||||
|             } | ||||
|  | ||||
|  | ||||
|      | ||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); | ||||
| @@ -3388,11 +3393,12 @@ export const AssetApiFp = function(configuration?: Configuration) { | ||||
|         /** | ||||
|          * Get all AssetEntity belong to the user | ||||
|          * @summary  | ||||
|          * @param {string} [ifNoneMatch] ETag of data already cached on the client | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async getAllAssets(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(options); | ||||
|         async getAllAssets(ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(ifNoneMatch, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
| @@ -3590,11 +3596,12 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath | ||||
|         /** | ||||
|          * Get all AssetEntity belong to the user | ||||
|          * @summary  | ||||
|          * @param {string} [ifNoneMatch] ETag of data already cached on the client | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         getAllAssets(options?: any): AxiosPromise<Array<AssetResponseDto>> { | ||||
|             return localVarFp.getAllAssets(options).then((request) => request(axios, basePath)); | ||||
|         getAllAssets(ifNoneMatch?: string, options?: any): AxiosPromise<Array<AssetResponseDto>> { | ||||
|             return localVarFp.getAllAssets(ifNoneMatch, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          * Get a single asset\'s information | ||||
| @@ -3788,12 +3795,13 @@ export class AssetApi extends BaseAPI { | ||||
|     /** | ||||
|      * Get all AssetEntity belong to the user | ||||
|      * @summary  | ||||
|      * @param {string} [ifNoneMatch] ETag of data already cached on the client | ||||
|      * @param {*} [options] Override http request option. | ||||
|      * @throws {RequiredError} | ||||
|      * @memberof AssetApi | ||||
|      */ | ||||
|     public getAllAssets(options?: AxiosRequestConfig) { | ||||
|         return AssetApiFp(this.configuration).getAllAssets(options).then((request) => request(this.axios, this.basePath)); | ||||
|     public getAllAssets(ifNoneMatch?: string, options?: AxiosRequestConfig) { | ||||
|         return AssetApiFp(this.configuration).getAllAssets(ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
		Reference in New Issue
	
	Block a user