mirror of
https://github.com/immich-app/immich.git
synced 2025-12-18 17:23:16 +03:00
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>
This commit is contained in:
@@ -363,14 +363,14 @@ extension on Iterable<PlatformAsset> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on PlatformAsset {
|
extension PlatformToLocalAsset on PlatformAsset {
|
||||||
LocalAsset toLocalAsset() => LocalAsset(
|
LocalAsset toLocalAsset() => LocalAsset(
|
||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
checksum: null,
|
checksum: null,
|
||||||
type: AssetType.values.elementAtOrNull(type) ?? AssetType.other,
|
type: AssetType.values.elementAtOrNull(type) ?? AssetType.other,
|
||||||
createdAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(),
|
createdAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(),
|
||||||
updatedAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(),
|
updatedAt: tryFromSecondsSinceEpoch(updatedAt, isUtc: true) ?? DateTime.timestamp(),
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
durationInSeconds: durationInSeconds,
|
durationInSeconds: durationInSeconds,
|
||||||
|
|||||||
@@ -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/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/sync_stream.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/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/debug_print.dart';
|
||||||
import 'package:immich_mobile/utils/diff.dart';
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
// ignore: import_rule_photo_manager
|
// ignore: import_rule_photo_manager
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
const int targetVersion = 18;
|
const int targetVersion = 19;
|
||||||
|
|
||||||
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
||||||
final hasVersion = Store.tryGet(StoreKey.version) != null;
|
final hasVersion = Store.tryGet(StoreKey.version) != null;
|
||||||
@@ -78,6 +80,12 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
|||||||
await Store.put(StoreKey.shouldResetSync, true);
|
await Store.put(StoreKey.shouldResetSync, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version < 19 && Store.isBetaTimelineEnabled) {
|
||||||
|
if (!await _populateUpdatedAtTime(drift)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (targetVersion >= 12) {
|
if (targetVersion >= 12) {
|
||||||
await Store.put(StoreKey.version, targetVersion);
|
await Store.put(StoreKey.version, targetVersion);
|
||||||
return;
|
return;
|
||||||
@@ -221,6 +229,32 @@ Future<void> _migrateDeviceAsset(Isar db) async {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> _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<void> migrateDeviceAssetToSqlite(Isar db, Drift drift) async {
|
Future<void> migrateDeviceAssetToSqlite(Isar db, Drift drift) async {
|
||||||
try {
|
try {
|
||||||
final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll();
|
final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll();
|
||||||
|
|||||||
@@ -54,12 +54,7 @@ void main() {
|
|||||||
|
|
||||||
when(() => mockNativeSyncApi.shouldFullSync()).thenAnswer((_) async => false);
|
when(() => mockNativeSyncApi.shouldFullSync()).thenAnswer((_) async => false);
|
||||||
when(() => mockNativeSyncApi.getMediaChanges()).thenAnswer(
|
when(() => mockNativeSyncApi.getMediaChanges()).thenAnswer(
|
||||||
(_) async => SyncDelta(
|
(_) async => SyncDelta(hasChanges: false, updates: const [], deletes: const [], assetAlbums: const {}),
|
||||||
hasChanges: false,
|
|
||||||
updates: const [],
|
|
||||||
deletes: const [],
|
|
||||||
assetAlbums: const {},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
when(() => mockNativeSyncApi.getTrashedAssets()).thenAnswer((_) async => {});
|
when(() => mockNativeSyncApi.getTrashedAssets()).thenAnswer((_) async => {});
|
||||||
when(() => mockTrashedLocalAssetRepository.processTrashSnapshot(any())).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');
|
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();
|
final assetEntity = MockAssetEntity();
|
||||||
when(() => assetEntity.getMediaUrl()).thenAnswer((_) async => 'content://local-trash');
|
when(() => assetEntity.getMediaUrl()).thenAnswer((_) async => 'content://local-trash');
|
||||||
when(() => mockStorageRepository.getAssetEntityForAsset(localAssetToTrash)).thenAnswer((_) async => assetEntity);
|
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.processTrashSnapshot(any())).called(1);
|
||||||
verify(() => mockTrashedLocalAssetRepository.getToTrash()).called(1);
|
verify(() => mockTrashedLocalAssetRepository.getToTrash()).called(1);
|
||||||
@@ -159,8 +160,7 @@ void main() {
|
|||||||
verify(() => mockTrashedLocalAssetRepository.applyRestoredAssets(restoredIds)).called(1);
|
verify(() => mockTrashedLocalAssetRepository.applyRestoredAssets(restoredIds)).called(1);
|
||||||
|
|
||||||
verify(() => mockStorageRepository.getAssetEntityForAsset(localAssetToTrash)).called(1);
|
verify(() => mockStorageRepository.getAssetEntityForAsset(localAssetToTrash)).called(1);
|
||||||
final moveArgs =
|
final moveArgs = verify(() => mockLocalFilesManager.moveToTrash(captureAny())).captured.single as List<String>;
|
||||||
verify(() => mockLocalFilesManager.moveToTrash(captureAny())).captured.single as List<String>;
|
|
||||||
expect(moveArgs, ['content://local-trash']);
|
expect(moveArgs, ['content://local-trash']);
|
||||||
final trashArgs =
|
final trashArgs =
|
||||||
verify(() => mockTrashedLocalAssetRepository.trashLocalAsset(captureAny())).captured.single
|
verify(() => mockTrashedLocalAssetRepository.trashLocalAsset(captureAny())).captured.single
|
||||||
@@ -187,4 +187,25 @@ void main() {
|
|||||||
verifyNever(() => mockTrashedLocalAssetRepository.trashLocalAsset(any()));
|
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));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user