feat: add cloud id during native sync (#20418)

* use adjustment time in iOS for hash reset

# Conflicts:
#	mobile/lib/infrastructure/repositories/local_album.repository.dart
#	mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart

* migration

* feat: sync cloudId and eTag on sync

* fixes fixes

* more fixes

* re-sync updated eTags

* add server version check & auto sync cloud ids on compatible servers

* fix test

* remove button from sync status page

* chore: modify for testing

* more changes

* chore: add commas in toString

* use cached provider in splash screen

* read upload service provider to prevent reset

* log errors from fetching cloud id mapping

* WIP: migrate cloud id - debug log

* ignore locked asset update

* bulk update metadata

* change log text

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
shenlong
2026-01-15 00:04:11 +05:30
committed by GitHub
parent ed9448a6ee
commit 9fa8de7baa
46 changed files with 10913 additions and 121 deletions

View File

@@ -8,6 +8,7 @@ import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
@@ -18,6 +19,7 @@ import 'package:logging/logging.dart';
class LocalSyncService {
final DriftLocalAlbumRepository _localAlbumRepository;
final DriftLocalAssetRepository _localAssetRepository;
final NativeSyncApi _nativeSyncApi;
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
final LocalFilesManagerRepository _localFilesManager;
@@ -26,11 +28,13 @@ class LocalSyncService {
LocalSyncService({
required DriftLocalAlbumRepository localAlbumRepository,
required DriftLocalAssetRepository localAssetRepository,
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
required LocalFilesManagerRepository localFilesManager,
required StorageRepository storageRepository,
required NativeSyncApi nativeSyncApi,
}) : _localAlbumRepository = localAlbumRepository,
_localAssetRepository = localAssetRepository,
_trashedLocalAssetRepository = trashedLocalAssetRepository,
_localFilesManager = localFilesManager,
_storageRepository = storageRepository,
@@ -47,6 +51,12 @@ class LocalSyncService {
_log.warning("syncTrashedAssets cannot proceed because MANAGE_MEDIA permission is missing");
}
}
if (CurrentPlatform.isIOS) {
final assets = await _localAssetRepository.getEmptyCloudIdAssets();
await _mapIosCloudIds(assets);
}
if (full || await _nativeSyncApi.shouldFullSync()) {
_log.fine("Full sync request from ${full ? "user" : "native"}");
return await fullSync();
@@ -63,8 +73,9 @@ class LocalSyncService {
final deviceAlbums = await _nativeSyncApi.getAlbums();
await _localAlbumRepository.updateAll(deviceAlbums.toLocalAlbums());
final newAssets = delta.updates.toLocalAssets();
await _localAlbumRepository.processDelta(
updates: delta.updates.toLocalAssets(),
updates: newAssets,
deletes: delta.deletes,
assetAlbums: delta.assetAlbums,
);
@@ -92,6 +103,8 @@ class LocalSyncService {
}
await updateAlbum(dbAlbum, album);
}
await _mapIosCloudIds(newAssets);
}
await _nativeSyncApi.checkpointSync();
} catch (e, s) {
@@ -130,9 +143,12 @@ class LocalSyncService {
try {
_log.fine("Adding device album ${album.name}");
final assets = album.assetCount > 0 ? await _nativeSyncApi.getAssetsForAlbum(album.id) : <PlatformAsset>[];
final assets = album.assetCount > 0
? await _nativeSyncApi.getAssetsForAlbum(album.id).then((a) => a.toLocalAssets())
: <LocalAsset>[];
await _localAlbumRepository.upsert(album, toUpsert: assets.toLocalAssets());
await _localAlbumRepository.upsert(album, toUpsert: assets);
await _mapIosCloudIds(assets);
_log.fine("Successfully added device album ${album.name}");
} catch (e, s) {
_log.warning("Error while adding device album", e, s);
@@ -202,13 +218,16 @@ class LocalSyncService {
return false;
}
final newAssets = await _nativeSyncApi.getAssetsForAlbum(deviceAlbum.id, updatedTimeCond: updatedTime);
final newAssets = await _nativeSyncApi
.getAssetsForAlbum(deviceAlbum.id, updatedTimeCond: updatedTime)
.then((a) => a.toLocalAssets());
await _localAlbumRepository.upsert(
deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection),
toUpsert: newAssets.toLocalAssets(),
toUpsert: newAssets,
);
await _mapIosCloudIds(newAssets);
return true;
} catch (e, s) {
_log.warning("Error on fast syncing local album: ${dbAlbum.name}", e, s);
@@ -240,6 +259,7 @@ class LocalSyncService {
if (dbAlbum.assetCount == 0) {
_log.fine("Device album ${deviceAlbum.name} is empty. Adding assets to DB.");
await _localAlbumRepository.upsert(updatedDeviceAlbum, toUpsert: assetsInDevice);
await _mapIosCloudIds(assetsInDevice);
return true;
}
@@ -277,6 +297,7 @@ class LocalSyncService {
}
await _localAlbumRepository.upsert(updatedDeviceAlbum, toUpsert: assetsToUpsert, toDelete: assetsToDelete);
await _mapIosCloudIds(assetsToUpsert);
return true;
} catch (e, s) {
@@ -285,6 +306,29 @@ class LocalSyncService {
return true;
}
Future<void> _mapIosCloudIds(List<LocalAsset> assets) async {
if (!CurrentPlatform.isIOS || assets.isEmpty) {
return;
}
final assetIds = assets.map((a) => a.id).toList();
final cloudMapping = <String, String>{};
final cloudIds = await _nativeSyncApi.getCloudIdForAssetIds(assetIds);
for (int i = 0; i < cloudIds.length; i++) {
final cloudIdResult = cloudIds[i];
if (cloudIdResult.cloudId != null) {
cloudMapping[cloudIdResult.assetId] = cloudIdResult.cloudId!;
} else {
final asset = assets.firstWhereOrNull((a) => a.id == cloudIdResult.assetId);
_log.fine(
"Cannot fetch cloudId for asset with id: ${cloudIdResult.assetId}, name: ${asset?.name}, createdAt: ${asset?.createdAt}. Error: ${cloudIdResult.error ?? "unknown"}",
);
}
}
await _localAlbumRepository.updateCloudMapping(cloudMapping);
}
bool _assetsEqual(LocalAsset a, LocalAsset b) {
if (CurrentPlatform.isAndroid) {
return a.updatedAt.isAtSameMomentAs(b.updatedAt) &&