From 24e5dabb516ad3373949997a3245125e12dbc5ae Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:19:27 +0530 Subject: [PATCH] fix: use proper updatedAt value in local assets (#24137) * fix: incorrect updatedAt value in local assets * add test --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../domain/services/local_sync.service.dart | 4 +- mobile/lib/utils/migration.dart | 36 +++++++++++++++- .../services/local_sync_service_test.dart | 41 ++++++++++++++----- 3 files changed, 68 insertions(+), 13 deletions(-) diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart index 5cbae9c5a1..04eaf04694 100644 --- a/mobile/lib/domain/services/local_sync.service.dart +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -363,14 +363,14 @@ extension on Iterable { } } -extension on PlatformAsset { +extension PlatformToLocalAsset on PlatformAsset { LocalAsset toLocalAsset() => LocalAsset( id: id, name: name, checksum: null, type: AssetType.values.elementAtOrNull(type) ?? AssetType.other, createdAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(), - updatedAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(), + updatedAt: tryFromSecondsSinceEpoch(updatedAt, isUtc: true) ?? DateTime.timestamp(), width: width, height: height, durationInSeconds: durationInSeconds, diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index b0d7ea6013..552c9e356a 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -22,14 +22,16 @@ import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; +import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/datetime_helpers.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:isar/isar.dart'; // ignore: import_rule_photo_manager import 'package:photo_manager/photo_manager.dart'; -const int targetVersion = 18; +const int targetVersion = 19; Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { final hasVersion = Store.tryGet(StoreKey.version) != null; @@ -78,6 +80,12 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { await Store.put(StoreKey.shouldResetSync, true); } + if (version < 19 && Store.isBetaTimelineEnabled) { + if (!await _populateUpdatedAtTime(drift)) { + return; + } + } + if (targetVersion >= 12) { await Store.put(StoreKey.version, targetVersion); return; @@ -221,6 +229,32 @@ Future _migrateDeviceAsset(Isar db) async { }); } +Future _populateUpdatedAtTime(Drift db) async { + try { + final nativeApi = NativeSyncApi(); + final albums = await nativeApi.getAlbums(); + for (final album in albums) { + final assets = await nativeApi.getAssetsForAlbum(album.id); + await db.batch((batch) async { + for (final asset in assets) { + batch.update( + db.localAssetEntity, + LocalAssetEntityCompanion( + updatedAt: Value(tryFromSecondsSinceEpoch(asset.updatedAt, isUtc: true) ?? DateTime.timestamp()), + ), + where: (t) => t.id.equals(asset.id), + ); + } + }); + } + + return true; + } catch (error) { + dPrint(() => "[MIGRATION] Error while populating updatedAt time: $error"); + return false; + } +} + Future migrateDeviceAssetToSqlite(Isar db, Drift drift) async { try { final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll(); diff --git a/mobile/test/domain/services/local_sync_service_test.dart b/mobile/test/domain/services/local_sync_service_test.dart index 2f236971e0..92ab01c7e0 100644 --- a/mobile/test/domain/services/local_sync_service_test.dart +++ b/mobile/test/domain/services/local_sync_service_test.dart @@ -54,12 +54,7 @@ void main() { when(() => mockNativeSyncApi.shouldFullSync()).thenAnswer((_) async => false); when(() => mockNativeSyncApi.getMediaChanges()).thenAnswer( - (_) async => SyncDelta( - hasChanges: false, - updates: const [], - deletes: const [], - assetAlbums: const {}, - ), + (_) async => SyncDelta(hasChanges: false, updates: const [], deletes: const [], assetAlbums: const {}), ); when(() => mockNativeSyncApi.getTrashedAssets()).thenAnswer((_) async => {}); when(() => mockTrashedLocalAssetRepository.processTrashSnapshot(any())).thenAnswer((_) async {}); @@ -144,13 +139,19 @@ void main() { }); final localAssetToTrash = LocalAssetStub.image2.copyWith(id: 'local-trash', checksum: 'checksum-trash'); - when(() => mockTrashedLocalAssetRepository.getToTrash()).thenAnswer((_) async => {'album-a': [localAssetToTrash]}); + when(() => mockTrashedLocalAssetRepository.getToTrash()).thenAnswer( + (_) async => { + 'album-a': [localAssetToTrash], + }, + ); final assetEntity = MockAssetEntity(); when(() => assetEntity.getMediaUrl()).thenAnswer((_) async => 'content://local-trash'); when(() => mockStorageRepository.getAssetEntityForAsset(localAssetToTrash)).thenAnswer((_) async => assetEntity); - await sut.processTrashedAssets({'album-a': [platformAsset]}); + await sut.processTrashedAssets({ + 'album-a': [platformAsset], + }); verify(() => mockTrashedLocalAssetRepository.processTrashSnapshot(any())).called(1); verify(() => mockTrashedLocalAssetRepository.getToTrash()).called(1); @@ -159,8 +160,7 @@ void main() { verify(() => mockTrashedLocalAssetRepository.applyRestoredAssets(restoredIds)).called(1); verify(() => mockStorageRepository.getAssetEntityForAsset(localAssetToTrash)).called(1); - final moveArgs = - verify(() => mockLocalFilesManager.moveToTrash(captureAny())).captured.single as List; + final moveArgs = verify(() => mockLocalFilesManager.moveToTrash(captureAny())).captured.single as List; expect(moveArgs, ['content://local-trash']); final trashArgs = verify(() => mockTrashedLocalAssetRepository.trashLocalAsset(captureAny())).captured.single @@ -187,4 +187,25 @@ void main() { verifyNever(() => mockTrashedLocalAssetRepository.trashLocalAsset(any())); }); }); + + group('LocalSyncService - PlatformAsset conversion', () { + test('toLocalAsset uses correct updatedAt timestamp', () { + final platformAsset = PlatformAsset( + id: 'test-id', + name: 'test.jpg', + type: AssetType.image.index, + durationInSeconds: 0, + orientation: 0, + isFavorite: false, + createdAt: 1700000000, + updatedAt: 1732000000, + ); + + final localAsset = platformAsset.toLocalAsset(); + + expect(localAsset.createdAt.millisecondsSinceEpoch ~/ 1000, 1700000000); + expect(localAsset.updatedAt.millisecondsSinceEpoch ~/ 1000, 1732000000); + expect(localAsset.updatedAt, isNot(localAsset.createdAt)); + }); + }); }