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:
Fynn Petersen-Frey 2022-11-26 17:16:02 +01:00 committed by GitHub
parent efa7b3ba54
commit 47f5e4134e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 322 additions and 201 deletions

View file

@ -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 {