feat: full local assets / album sync

This commit is contained in:
shenlong-tanwen
2024-10-17 23:33:00 +05:30
parent a09710ec7b
commit c91a2878dc
87 changed files with 2417 additions and 366 deletions
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:collection/collection.dart';
import 'package:immich_mobile/domain/interfaces/asset.interface.dart';
import 'package:immich_mobile/domain/interfaces/database.interface.dart';
import 'package:immich_mobile/domain/models/asset.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/service_locator.dart';
@@ -9,76 +10,87 @@ import 'package:immich_mobile/utils/collection_util.dart';
import 'package:immich_mobile/utils/constants/globals.dart';
import 'package:immich_mobile/utils/immich_api_client.dart';
import 'package:immich_mobile/utils/isolate_helper.dart';
import 'package:immich_mobile/utils/log_manager.dart';
import 'package:immich_mobile/utils/mixins/log.mixin.dart';
import 'package:openapi/api.dart';
class AssetSyncService with LogMixin {
const AssetSyncService();
Future<bool> performFullRemoteSyncForUser(
Future<bool> performFullRemoteSyncIsolate(
User user, {
DateTime? updatedUtil,
int? limit,
}) async {
return await IsolateHelper.run(() async {
try {
final logger = LogManager.I.get("SyncService <Isolate>");
final syncClient = di<ImmichApiClient>().getSyncApi();
final chunkSize = limit ?? kFullSyncChunkSize;
final updatedTill = updatedUtil ?? DateTime.now().toUtc();
updatedUtil ??= DateTime.now().toUtc();
String? lastAssetId;
while (true) {
logger.d(
"Requesting more chunks from lastId - ${lastAssetId ?? "<initial_fetch>"}",
);
final assets = await syncClient.getFullSyncForUser(AssetFullSyncDto(
limit: chunkSize,
updatedUntil: updatedTill,
lastId: lastAssetId,
userId: user.id,
));
if (assets == null) {
break;
}
final assetsFromServer =
assets.map(Asset.remote).sorted(Asset.compareByRemoteId);
final assetsInDb = await di<IAssetRepository>().getForRemoteIds(
assetsFromServer.map((a) => a.remoteId!).toList(),
);
await _syncAssetsToDb(
assetsFromServer,
assetsInDb,
Asset.compareByRemoteId,
isRemoteSync: true,
);
lastAssetId = assets.lastOrNull?.id;
if (assets.length != chunkSize) break;
}
return true;
} catch (e, s) {
log.e("Error performing full sync for user - ${user.name}", e, s);
}
return false;
return await performFullRemoteSync(
user,
updatedUtil: updatedUtil,
limit: limit,
);
});
}
Future<void> _syncAssetsToDb(
List<Asset> newAssets,
List<Asset> existingAssets,
Comparator<Asset> compare, {
bool? isRemoteSync,
Future<bool> performFullRemoteSync(
User user, {
DateTime? updatedUtil,
int? limit,
}) async {
final (toAdd, toUpdate, assetsToRemove) = _diffAssets(
try {
final syncClient = di<ImApiClient>().getSyncApi();
final db = di<IDatabaseRepository>();
final assetRepo = di<IAssetRepository>();
final chunkSize = limit ?? kFullSyncChunkSize;
final updatedTill = updatedUtil ?? DateTime.now().toUtc();
updatedUtil ??= DateTime.now().toUtc();
String? lastAssetId;
while (true) {
log.d(
"Requesting more chunks from lastId - ${lastAssetId ?? "<initial_fetch>"}",
);
final assets = await syncClient.getFullSyncForUser(AssetFullSyncDto(
limit: chunkSize,
updatedUntil: updatedTill,
lastId: lastAssetId,
userId: user.id,
));
if (assets == null) {
break;
}
final assetsFromServer = assets.map(Asset.remote).toList();
await db.txn(() async {
final assetsInDb =
await assetRepo.getForHashes(assetsFromServer.map((a) => a.hash));
await upsertAssetsToDb(
assetsFromServer,
assetsInDb,
isRemoteSync: true,
);
});
lastAssetId = assets.lastOrNull?.id;
if (assets.length != chunkSize) break;
}
return true;
} catch (e, s) {
log.e("Error performing full remote sync for user - ${user.name}", e, s);
}
return false;
}
Future<void> upsertAssetsToDb(
List<Asset> newAssets,
List<Asset> existingAssets, {
bool? isRemoteSync,
Comparator<Asset> compare = Asset.compareByHash,
}) async {
final (toAdd, toUpdate, toRemove) = await _diffAssets(
newAssets,
existingAssets,
compare: compare,
@@ -88,37 +100,36 @@ class AssetSyncService with LogMixin {
final assetsToAdd = toAdd.followedBy(toUpdate);
await di<IAssetRepository>().upsertAll(assetsToAdd);
await di<IAssetRepository>()
.deleteIds(assetsToRemove.map((a) => a.id).toList());
await di<IAssetRepository>().deleteIds(toRemove.map((a) => a.id!).toList());
}
/// Returns a triple (toAdd, toUpdate, toRemove)
(List<Asset>, List<Asset>, List<Asset>) _diffAssets(
FutureOr<(List<Asset>, List<Asset>, List<Asset>)> _diffAssets(
List<Asset> newAssets,
List<Asset> inDb, {
bool? isRemoteSync,
required Comparator<Asset> compare,
}) {
Comparator<Asset> compare = Asset.compareByHash,
}) async {
// fast paths for trivial cases: reduces memory usage during initial sync etc.
if (newAssets.isEmpty && inDb.isEmpty) {
return const ([], [], []);
return const (<Asset>[], <Asset>[], <Asset>[]);
} else if (newAssets.isEmpty && isRemoteSync == null) {
// remove all from database
return (const [], const [], inDb);
return (const <Asset>[], const <Asset>[], inDb);
} else if (inDb.isEmpty) {
// add all assets
return (newAssets, const [], const []);
return (newAssets, const <Asset>[], const <Asset>[]);
}
final List<Asset> toAdd = [];
final List<Asset> toUpdate = [];
final List<Asset> toRemove = [];
CollectionUtil.diffSortedLists(
await CollectionUtil.diffLists(
inDb,
newAssets,
compare: compare,
both: (Asset a, Asset b) {
if (a == b) {
if (a != b) {
toUpdate.add(a.merge(b));
return true;
}
@@ -140,7 +151,7 @@ class AssetSyncService with LogMixin {
toRemove.add(a);
}
},
// Only in remote (new asset)
// Only in new assets
onlySecond: (Asset b) => toAdd.add(b),
);
return (toAdd, toUpdate, toRemove);