Przeglądaj źródła

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
Fynn Petersen-Frey 2 lat temu
rodzic
commit
47f5e4134e

+ 1 - 0
mobile/lib/constants/hive_box.dart

@@ -4,6 +4,7 @@ const String accessTokenKey = "immichBoxAccessTokenKey"; // Key 1
 const String deviceIdKey = 'immichBoxDeviceIdKey'; // Key 2
 const String deviceIdKey = 'immichBoxDeviceIdKey'; // Key 2
 const String isLoggedInKey = 'immichIsLoggedInKey'; // Key 3
 const String isLoggedInKey = 'immichIsLoggedInKey'; // Key 3
 const String serverEndpointKey = 'immichBoxServerEndpoint'; // Key 4
 const String serverEndpointKey = 'immichBoxServerEndpoint'; // Key 4
+const String assetEtagKey = 'immichAssetEtagKey'; // Key 5
 
 
 // Login Info
 // Login Info
 const String hiveLoginInfoBox = "immichLoginInfoBox"; // Box
 const String hiveLoginInfoBox = "immichLoginInfoBox"; // Box

+ 21 - 36
mobile/lib/modules/home/services/asset.service.dart

@@ -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/models/asset.dart';
 import 'package:immich_mobile/shared/providers/api.provider.dart';
 import 'package:immich_mobile/shared/providers/api.provider.dart';
 import 'package:immich_mobile/shared/services/api.service.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:openapi/api.dart';
-import 'package:photo_manager/photo_manager.dart';
 
 
 final assetServiceProvider = Provider(
 final assetServiceProvider = Provider(
   (ref) => AssetService(
   (ref) => AssetService(
@@ -28,39 +29,22 @@ class AssetService {
 
 
   AssetService(this._apiService, this._backupService, this._backgroundService);
   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
   /// 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 {
     try {
       final Future<bool> hasAccess = urgent
       final Future<bool> hasAccess = urgent
           ? _backgroundService.hasAccess
           ? _backgroundService.hasAccess
@@ -71,15 +55,16 @@ class AssetService {
       }
       }
       final box = await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
       final box = await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
       final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey);
       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) {
     } catch (e) {
       debugPrint("Error [_getLocalAssets] ${e.toString()}");
       debugPrint("Error [_getLocalAssets] ${e.toString()}");
-      return [];
     }
     }
+    return null;
   }
   }
 
 
   Future<Asset?> getAssetById(String assetId) async {
   Future<Asset?> getAssetById(String assetId) async {

+ 40 - 9
mobile/lib/shared/providers/asset.provider.dart

@@ -1,7 +1,9 @@
 import 'dart:collection';
 import 'dart:collection';
 
 
 import 'package:flutter/foundation.dart';
 import 'package:flutter/foundation.dart';
+import 'package:hive/hive.dart';
 import 'package:hooks_riverpod/hooks_riverpod.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.service.dart';
 import 'package:immich_mobile/modules/home/services/asset_cache.service.dart';
 import 'package:immich_mobile/modules/home/services/asset_cache.service.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
@@ -33,10 +35,11 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
     final stopwatch = Stopwatch();
     final stopwatch = Stopwatch();
     try {
     try {
       _getAllAssetInProgress = true;
       _getAllAssetInProgress = true;
-
       final bool isCacheValid = await _assetCacheService.isValid();
       final bool isCacheValid = await _assetCacheService.isValid();
+      stopwatch.start();
+      final localTask = _assetService.getLocalAssets(urgent: !isCacheValid);
+      final remoteTask = _assetService.getRemoteAssets();
       if (isCacheValid && state.isEmpty) {
       if (isCacheValid && state.isEmpty) {
-        stopwatch.start();
         state = await _assetCacheService.get();
         state = await _assetCacheService.get();
         debugPrint(
         debugPrint(
           "Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms",
           "Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms",
@@ -44,21 +47,49 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
         stopwatch.reset();
         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();
       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 {
     } finally {
       _getAllAssetInProgress = false;
       _getAllAssetInProgress = false;
     }
     }
     debugPrint("[getAllAsset] setting new asset state");
     debugPrint("[getAllAsset] setting new asset state");
 
 
-    stopwatch.start();
+    stopwatch.reset();
     _cacheState();
     _cacheState();
     debugPrint("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms");
     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() {
   clearAllAsset() {

+ 53 - 0
mobile/lib/utils/openapi_extensions.dart

@@ -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 - 0
mobile/lib/utils/tuple.dart

@@ -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);
+}

+ 7 - 3
mobile/openapi/doc/AssetApi.md

@@ -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)
 [[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**
 # **getAllAssets**
-> List<AssetResponseDto> getAllAssets()
+> List<AssetResponseDto> getAllAssets(ifNoneMatch)
 
 
 
 
 
 
@@ -291,9 +291,10 @@ import 'package:openapi/api.dart';
 //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 
 
 final api_instance = AssetApi();
 final api_instance = AssetApi();
+final ifNoneMatch = ifNoneMatch_example; // String | ETag of data already cached on the client
 
 
 try {
 try {
-    final result = api_instance.getAllAssets();
+    final result = api_instance.getAllAssets(ifNoneMatch);
     print(result);
     print(result);
 } catch (e) {
 } catch (e) {
     print('Exception when calling AssetApi->getAllAssets: $e\n');
     print('Exception when calling AssetApi->getAllAssets: $e\n');
@@ -301,7 +302,10 @@ try {
 ```
 ```
 
 
 ### Parameters
 ### 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
 ### Return type
 
 

+ 2 - 2
mobile/openapi/doc/AssetResponseDto.md

@@ -21,10 +21,10 @@ Name | Type | Description | Notes
 **mimeType** | **String** |  | 
 **mimeType** | **String** |  | 
 **duration** | **String** |  | 
 **duration** | **String** |  | 
 **webpPath** | **String** |  | 
 **webpPath** | **String** |  | 
-**encodedVideoPath** | **String** |  | 
+**encodedVideoPath** | **String** |  | [optional] 
 **exifInfo** | [**ExifResponseDto**](ExifResponseDto.md) |  | [optional] 
 **exifInfo** | [**ExifResponseDto**](ExifResponseDto.md) |  | [optional] 
 **smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.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)
 [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 
 

+ 1 - 1
mobile/openapi/doc/UserResponseDto.md

@@ -16,7 +16,7 @@ Name | Type | Description | Notes
 **profileImagePath** | **String** |  | 
 **profileImagePath** | **String** |  | 
 **shouldChangePassword** | **bool** |  | 
 **shouldChangePassword** | **bool** |  | 
 **isAdmin** | **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)
 [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 
 

+ 17 - 3
mobile/openapi/lib/api/asset_api.dart

@@ -297,7 +297,12 @@ class AssetApi {
   /// Get all AssetEntity belong to the user
   /// Get all AssetEntity belong to the user
   ///
   ///
   /// Note: This method returns the HTTP [Response].
   /// 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
     // ignore: prefer_const_declarations
     final path = r'/asset';
     final path = r'/asset';
 
 
@@ -308,6 +313,10 @@ class AssetApi {
     final headerParams = <String, String>{};
     final headerParams = <String, String>{};
     final formParams = <String, String>{};
     final formParams = <String, String>{};
 
 
+    if (ifNoneMatch != null) {
+      headerParams[r'if-none-match'] = parameterToString(ifNoneMatch);
+    }
+
     const contentTypes = <String>[];
     const contentTypes = <String>[];
 
 
 
 
@@ -325,8 +334,13 @@ class AssetApi {
   /// 
   /// 
   ///
   ///
   /// Get all AssetEntity belong to the user
   /// 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) {
     if (response.statusCode >= HttpStatus.badRequest) {
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
     }
     }

+ 60 - 73
mobile/openapi/lib/model/asset_response_dto.dart

@@ -26,10 +26,10 @@ class AssetResponseDto {
     required this.mimeType,
     required this.mimeType,
     required this.duration,
     required this.duration,
     required this.webpPath,
     required this.webpPath,
-    required this.encodedVideoPath,
+    this.encodedVideoPath,
     this.exifInfo,
     this.exifInfo,
     this.smartInfo,
     this.smartInfo,
-    required this.livePhotoVideoId,
+    this.livePhotoVideoId,
   });
   });
 
 
   AssetTypeEnum type;
   AssetTypeEnum type;
@@ -79,74 +79,71 @@ class AssetResponseDto {
   String? livePhotoVideoId;
   String? livePhotoVideoId;
 
 
   @override
   @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
   @override
   int get hashCode =>
   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
   @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() {
   Map<String, dynamic> toJson() {
     final _json = <String, dynamic>{};
     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) {
     if (resizePath != null) {
       _json[r'resizePath'] = resizePath;
       _json[r'resizePath'] = resizePath;
     } else {
     } else {
       _json[r'resizePath'] = null;
       _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) {
     if (mimeType != null) {
       _json[r'mimeType'] = mimeType;
       _json[r'mimeType'] = mimeType;
     } else {
     } else {
       _json[r'mimeType'] = null;
       _json[r'mimeType'] = null;
     }
     }
-    _json[r'duration'] = duration;
+      _json[r'duration'] = duration;
     if (webpPath != null) {
     if (webpPath != null) {
       _json[r'webpPath'] = webpPath;
       _json[r'webpPath'] = webpPath;
     } else {
     } else {
@@ -185,13 +182,13 @@ class AssetResponseDto {
       // Ensure that the map contains the required keys.
       // Ensure that the map contains the required keys.
       // Note 1: the values aren't checked for validity beyond being non-null.
       // Note 1: the values aren't checked for validity beyond being non-null.
       // Note 2: this code is stripped in release mode!
       // 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(
       return AssetResponseDto(
         type: AssetTypeEnum.fromJson(json[r'type'])!,
         type: AssetTypeEnum.fromJson(json[r'type'])!,
@@ -216,10 +213,7 @@ class AssetResponseDto {
     return null;
     return null;
   }
   }
 
 
-  static List<AssetResponseDto>? listFromJson(
-    dynamic json, {
-    bool growable = false,
-  }) {
+  static List<AssetResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
     final result = <AssetResponseDto>[];
     final result = <AssetResponseDto>[];
     if (json is List && json.isNotEmpty) {
     if (json is List && json.isNotEmpty) {
       for (final row in json) {
       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
   // 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>>{};
     final map = <String, List<AssetResponseDto>>{};
     if (json is Map && json.isNotEmpty) {
     if (json is Map && json.isNotEmpty) {
       json = json.cast<String, dynamic>(); // ignore: parameter_assignments
       json = json.cast<String, dynamic>(); // ignore: parameter_assignments
       for (final entry in json.entries) {
       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) {
         if (value != null) {
           map[entry.key] = value;
           map[entry.key] = value;
         }
         }
@@ -282,7 +270,6 @@ class AssetResponseDto {
     'mimeType',
     'mimeType',
     'duration',
     'duration',
     'webpPath',
     'webpPath',
-    'encodedVideoPath',
-    'livePhotoVideoId',
   };
   };
 }
 }
+

+ 48 - 55
mobile/openapi/lib/model/user_response_dto.dart

@@ -21,7 +21,7 @@ class UserResponseDto {
     required this.profileImagePath,
     required this.profileImagePath,
     required this.shouldChangePassword,
     required this.shouldChangePassword,
     required this.isAdmin,
     required this.isAdmin,
-    required this.deletedAt,
+    this.deletedAt,
   });
   });
 
 
   String id;
   String id;
@@ -40,49 +40,52 @@ class UserResponseDto {
 
 
   bool isAdmin;
   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;
   DateTime? deletedAt;
 
 
   @override
   @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
   @override
   int get hashCode =>
   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
   @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() {
   Map<String, dynamic> toJson() {
     final _json = <String, dynamic>{};
     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) {
     if (deletedAt != null) {
       _json[r'deletedAt'] = deletedAt!.toUtc().toIso8601String();
       _json[r'deletedAt'] = deletedAt!.toUtc().toIso8601String();
     } else {
     } else {
@@ -101,13 +104,13 @@ class UserResponseDto {
       // Ensure that the map contains the required keys.
       // Ensure that the map contains the required keys.
       // Note 1: the values aren't checked for validity beyond being non-null.
       // Note 1: the values aren't checked for validity beyond being non-null.
       // Note 2: this code is stripped in release mode!
       // 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(
       return UserResponseDto(
         id: mapValueOfType<String>(json, r'id')!,
         id: mapValueOfType<String>(json, r'id')!,
@@ -116,8 +119,7 @@ class UserResponseDto {
         lastName: mapValueOfType<String>(json, r'lastName')!,
         lastName: mapValueOfType<String>(json, r'lastName')!,
         createdAt: mapValueOfType<String>(json, r'createdAt')!,
         createdAt: mapValueOfType<String>(json, r'createdAt')!,
         profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
         profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
-        shouldChangePassword:
-            mapValueOfType<bool>(json, r'shouldChangePassword')!,
+        shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
         isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
         isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
         deletedAt: mapDateTime(json, r'deletedAt', ''),
         deletedAt: mapDateTime(json, r'deletedAt', ''),
       );
       );
@@ -125,10 +127,7 @@ class UserResponseDto {
     return null;
     return null;
   }
   }
 
 
-  static List<UserResponseDto>? listFromJson(
-    dynamic json, {
-    bool growable = false,
-  }) {
+  static List<UserResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
     final result = <UserResponseDto>[];
     final result = <UserResponseDto>[];
     if (json is List && json.isNotEmpty) {
     if (json is List && json.isNotEmpty) {
       for (final row in json) {
       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
   // 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>>{};
     final map = <String, List<UserResponseDto>>{};
     if (json is Map && json.isNotEmpty) {
     if (json is Map && json.isNotEmpty) {
       json = json.cast<String, dynamic>(); // ignore: parameter_assignments
       json = json.cast<String, dynamic>(); // ignore: parameter_assignments
       for (final entry in json.entries) {
       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) {
         if (value != null) {
           map[entry.key] = value;
           map[entry.key] = value;
         }
         }
@@ -186,6 +179,6 @@ class UserResponseDto {
     'profileImagePath',
     'profileImagePath',
     'shouldChangePassword',
     'shouldChangePassword',
     'isAdmin',
     'isAdmin',
-    'deletedAt',
   };
   };
 }
 }
+

+ 26 - 4
server/apps/immich/src/api-v1/asset/asset.controller.ts

@@ -14,6 +14,7 @@ import {
   Header,
   Header,
   Put,
   Put,
   UploadedFiles,
   UploadedFiles,
+  Request,
 } from '@nestjs/common';
 } from '@nestjs/common';
 import { Authenticated } from '../../decorators/authenticated.decorator';
 import { Authenticated } from '../../decorators/authenticated.decorator';
 import { AssetService } from './asset.service';
 import { AssetService } from './asset.service';
@@ -21,12 +22,12 @@ import { FileFieldsInterceptor } from '@nestjs/platform-express';
 import { assetUploadOption } from '../../config/asset-upload.config';
 import { assetUploadOption } from '../../config/asset-upload.config';
 import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
 import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
 import { ServeFileDto } from './dto/serve-file.dto';
 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 { BackgroundTaskService } from '../../modules/background-task/background-task.service';
 import { DeleteAssetDto } from './dto/delete-asset.dto';
 import { DeleteAssetDto } from './dto/delete-asset.dto';
 import { SearchAssetDto } from './dto/search-asset.dto';
 import { SearchAssetDto } from './dto/search-asset.dto';
 import { CheckDuplicateAssetDto } from './dto/check-duplicate-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 { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
 import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
 import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
 import { AssetResponseDto } from './response-dto/asset-response.dto';
 import { AssetResponseDto } from './response-dto/asset-response.dto';
@@ -49,6 +50,7 @@ import {
   IMMICH_ARCHIVE_FILE_COUNT,
   IMMICH_ARCHIVE_FILE_COUNT,
   IMMICH_CONTENT_LENGTH_HINT,
   IMMICH_CONTENT_LENGTH_HINT,
 } from '../../constants/download.constant';
 } from '../../constants/download.constant';
+import { etag } from '../../utils/etag';
 
 
 @Authenticated()
 @Authenticated()
 @ApiBearerAuth()
 @ApiBearerAuth()
@@ -168,8 +170,28 @@ export class AssetController {
    * Get all AssetEntity belong to the user
    * Get all AssetEntity belong to the user
    */
    */
   @Get('/')
   @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')
   @Post('/time-bucket')

+ 2 - 2
server/apps/immich/src/api-v1/asset/response-dto/asset-response.dto.ts

@@ -19,10 +19,10 @@ export class AssetResponseDto {
   mimeType!: string | null;
   mimeType!: string | null;
   duration!: string;
   duration!: string;
   webpPath!: string | null;
   webpPath!: string | null;
-  encodedVideoPath!: string | null;
+  encodedVideoPath?: string | null;
   exifInfo?: ExifResponseDto;
   exifInfo?: ExifResponseDto;
   smartInfo?: SmartInfoResponseDto;
   smartInfo?: SmartInfoResponseDto;
-  livePhotoVideoId!: string | null;
+  livePhotoVideoId?: string | null;
 }
 }
 
 
 export function mapAsset(entity: AssetEntity): AssetResponseDto {
 export function mapAsset(entity: AssetEntity): AssetResponseDto {

+ 2 - 2
server/apps/immich/src/api-v1/user/response-dto/user-response.dto.ts

@@ -9,7 +9,7 @@ export class UserResponseDto {
   profileImagePath!: string;
   profileImagePath!: string;
   shouldChangePassword!: boolean;
   shouldChangePassword!: boolean;
   isAdmin!: boolean;
   isAdmin!: boolean;
-  deletedAt!: Date | null;
+  deletedAt?: Date;
 }
 }
 
 
 export function mapUser(entity: UserEntity): UserResponseDto {
 export function mapUser(entity: UserEntity): UserResponseDto {
@@ -22,6 +22,6 @@ export function mapUser(entity: UserEntity): UserResponseDto {
     profileImagePath: entity.profileImagePath,
     profileImagePath: entity.profileImagePath,
     shouldChangePassword: entity.shouldChangePassword,
     shouldChangePassword: entity.shouldChangePassword,
     isAdmin: entity.isAdmin,
     isAdmin: entity.isAdmin,
-    deletedAt: entity.deletedAt || null,
+    deletedAt: entity.deletedAt,
   };
   };
 }
 }

+ 5 - 0
server/apps/immich/src/types/index.d.ts

@@ -0,0 +1,5 @@
+declare module 'crypto' {
+  namespace webcrypto {
+    const subtle: SubtleCrypto;
+  }
+}

+ 10 - 0
server/apps/immich/src/utils/etag.ts

@@ -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}"`;
+}

Plik diff jest za duży
+ 0 - 0
server/immich-openapi-specs.json


+ 18 - 10
web/src/api/open-api/api.ts

@@ -427,7 +427,7 @@ export interface AssetResponseDto {
      * @type {string}
      * @type {string}
      * @memberof AssetResponseDto
      * @memberof AssetResponseDto
      */
      */
-    'encodedVideoPath': string | null;
+    'encodedVideoPath'?: string | null;
     /**
     /**
      * 
      * 
      * @type {ExifResponseDto}
      * @type {ExifResponseDto}
@@ -445,7 +445,7 @@ export interface AssetResponseDto {
      * @type {string}
      * @type {string}
      * @memberof AssetResponseDto
      * @memberof AssetResponseDto
      */
      */
-    'livePhotoVideoId': string | null;
+    'livePhotoVideoId'?: string | null;
 }
 }
 /**
 /**
  * 
  * 
@@ -1729,7 +1729,7 @@ export interface UserResponseDto {
      * @type {string}
      * @type {string}
      * @memberof UserResponseDto
      * @memberof UserResponseDto
      */
      */
-    'deletedAt': string | null;
+    'deletedAt'?: string;
 }
 }
 /**
 /**
  * 
  * 
@@ -2788,10 +2788,11 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
         /**
         /**
          * Get all AssetEntity belong to the user
          * Get all AssetEntity belong to the user
          * @summary 
          * @summary 
+         * @param {string} [ifNoneMatch] ETag of data already cached on the client
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          * @throws {RequiredError}
          */
          */
-        getAllAssets: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+        getAllAssets: async (ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
             const localVarPath = `/asset`;
             const localVarPath = `/asset`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -2808,6 +2809,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
             // http bearer authentication required
             // http bearer authentication required
             await setBearerAuthToObject(localVarHeaderParameter, configuration)
             await setBearerAuthToObject(localVarHeaderParameter, configuration)
 
 
+            if (ifNoneMatch !== undefined && ifNoneMatch !== null) {
+                localVarHeaderParameter['if-none-match'] = String(ifNoneMatch);
+            }
+
 
 
     
     
             setSearchParams(localVarUrlObj, localVarQueryParameter);
             setSearchParams(localVarUrlObj, localVarQueryParameter);
@@ -3388,11 +3393,12 @@ export const AssetApiFp = function(configuration?: Configuration) {
         /**
         /**
          * Get all AssetEntity belong to the user
          * Get all AssetEntity belong to the user
          * @summary 
          * @summary 
+         * @param {string} [ifNoneMatch] ETag of data already cached on the client
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          * @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);
             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
          * Get all AssetEntity belong to the user
          * @summary 
          * @summary 
+         * @param {string} [ifNoneMatch] ETag of data already cached on the client
          * @param {*} [options] Override http request option.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          * @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
          * Get a single asset\'s information
@@ -3788,12 +3795,13 @@ export class AssetApi extends BaseAPI {
     /**
     /**
      * Get all AssetEntity belong to the user
      * Get all AssetEntity belong to the user
      * @summary 
      * @summary 
+     * @param {string} [ifNoneMatch] ETag of data already cached on the client
      * @param {*} [options] Override http request option.
      * @param {*} [options] Override http request option.
      * @throws {RequiredError}
      * @throws {RequiredError}
      * @memberof AssetApi
      * @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));
     }
     }
 
 
     /**
     /**

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików