mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
fix: width and height migration issue (#25643)
* fix: width and height migration issue * chore: sync stream migration tests * lint and test --------- Co-authored-by: bwees <brandonwees@gmail.com>
This commit is contained in:
parent
e07a91f9c2
commit
0beb1f9e7a
9 changed files with 219 additions and 23 deletions
|
|
@ -89,7 +89,9 @@ enum StoreKey<T> {
|
||||||
cleanupKeepMediaType<int>._(1009),
|
cleanupKeepMediaType<int>._(1009),
|
||||||
cleanupKeepAlbumIds<String>._(1010),
|
cleanupKeepAlbumIds<String>._(1010),
|
||||||
cleanupCutoffDaysAgo<int>._(1011),
|
cleanupCutoffDaysAgo<int>._(1011),
|
||||||
cleanupDefaultsInitialized<bool>._(1012);
|
cleanupDefaultsInitialized<bool>._(1012),
|
||||||
|
|
||||||
|
syncMigrationStatus<String>._(1013);
|
||||||
|
|
||||||
const StoreKey._(this.id);
|
const StoreKey._(this.id);
|
||||||
final int id;
|
final int id;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
||||||
|
|
@ -7,12 +10,21 @@ import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.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/storage.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/sync_migration.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/infrastructure/repositories/trashed_local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||||
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/semver.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
enum SyncMigrationTask {
|
||||||
|
v20260128_ResetExifV1, // EXIF table has incorrect width and height information.
|
||||||
|
v20260128_CopyExifWidthHeightToAsset, // Asset table has incorrect width and height for video ratio calculations.
|
||||||
|
v20260128_ResetAssetV1, // Asset v2.5.0 has width and height information that were edited assets.
|
||||||
|
}
|
||||||
|
|
||||||
class SyncStreamService {
|
class SyncStreamService {
|
||||||
final Logger _logger = Logger('SyncStreamService');
|
final Logger _logger = Logger('SyncStreamService');
|
||||||
|
|
||||||
|
|
@ -22,6 +34,8 @@ class SyncStreamService {
|
||||||
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||||
final LocalFilesManagerRepository _localFilesManager;
|
final LocalFilesManagerRepository _localFilesManager;
|
||||||
final StorageRepository _storageRepository;
|
final StorageRepository _storageRepository;
|
||||||
|
final SyncMigrationRepository _syncMigrationRepository;
|
||||||
|
final ApiService _api;
|
||||||
final bool Function()? _cancelChecker;
|
final bool Function()? _cancelChecker;
|
||||||
|
|
||||||
SyncStreamService({
|
SyncStreamService({
|
||||||
|
|
@ -31,6 +45,8 @@ class SyncStreamService {
|
||||||
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
||||||
required LocalFilesManagerRepository localFilesManager,
|
required LocalFilesManagerRepository localFilesManager,
|
||||||
required StorageRepository storageRepository,
|
required StorageRepository storageRepository,
|
||||||
|
required SyncMigrationRepository syncMigrationRepository,
|
||||||
|
required ApiService api,
|
||||||
bool Function()? cancelChecker,
|
bool Function()? cancelChecker,
|
||||||
}) : _syncApiRepository = syncApiRepository,
|
}) : _syncApiRepository = syncApiRepository,
|
||||||
_syncStreamRepository = syncStreamRepository,
|
_syncStreamRepository = syncStreamRepository,
|
||||||
|
|
@ -38,12 +54,32 @@ class SyncStreamService {
|
||||||
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
||||||
_localFilesManager = localFilesManager,
|
_localFilesManager = localFilesManager,
|
||||||
_storageRepository = storageRepository,
|
_storageRepository = storageRepository,
|
||||||
|
_syncMigrationRepository = syncMigrationRepository,
|
||||||
|
_api = api,
|
||||||
_cancelChecker = cancelChecker;
|
_cancelChecker = cancelChecker;
|
||||||
|
|
||||||
bool get isCancelled => _cancelChecker?.call() ?? false;
|
bool get isCancelled => _cancelChecker?.call() ?? false;
|
||||||
|
|
||||||
Future<bool> sync() async {
|
Future<bool> sync() async {
|
||||||
_logger.info("Remote sync request for user");
|
_logger.info("Remote sync request for user");
|
||||||
|
final serverVersion = await _api.serverInfoApi.getServerVersion();
|
||||||
|
if (serverVersion == null) {
|
||||||
|
_logger.severe("Cannot perform sync: unable to determine server version");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final semVer = SemVer(major: serverVersion.major, minor: serverVersion.minor, patch: serverVersion.patch_);
|
||||||
|
|
||||||
|
final value = Store.get(StoreKey.syncMigrationStatus, "[]");
|
||||||
|
final migrations = (jsonDecode(value) as List).cast<String>();
|
||||||
|
int previousLength = migrations.length;
|
||||||
|
await _runPreSyncTasks(migrations, semVer);
|
||||||
|
|
||||||
|
if (migrations.length != previousLength) {
|
||||||
|
_logger.info("Updated pre-sync migration status: $migrations");
|
||||||
|
await Store.put(StoreKey.syncMigrationStatus, jsonEncode(migrations));
|
||||||
|
}
|
||||||
|
|
||||||
// Start the sync stream and handle events
|
// Start the sync stream and handle events
|
||||||
bool shouldReset = false;
|
bool shouldReset = false;
|
||||||
await _syncApiRepository.streamChanges(_handleEvents, onReset: () => shouldReset = true);
|
await _syncApiRepository.streamChanges(_handleEvents, onReset: () => shouldReset = true);
|
||||||
|
|
@ -51,9 +87,56 @@ class SyncStreamService {
|
||||||
_logger.info("Resetting sync state as requested by server");
|
_logger.info("Resetting sync state as requested by server");
|
||||||
await _syncApiRepository.streamChanges(_handleEvents);
|
await _syncApiRepository.streamChanges(_handleEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
previousLength = migrations.length;
|
||||||
|
await _runPostSyncTasks(migrations);
|
||||||
|
|
||||||
|
if (migrations.length != previousLength) {
|
||||||
|
_logger.info("Updated pre-sync migration status: $migrations");
|
||||||
|
await Store.put(StoreKey.syncMigrationStatus, jsonEncode(migrations));
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _runPreSyncTasks(List<String> migrations, SemVer semVer) async {
|
||||||
|
if (!migrations.contains(SyncMigrationTask.v20260128_ResetExifV1.name)) {
|
||||||
|
_logger.info("Running pre-sync task: v20260128_ResetExifV1");
|
||||||
|
await _syncApiRepository.deleteSyncAck([
|
||||||
|
SyncEntityType.assetExifV1,
|
||||||
|
SyncEntityType.partnerAssetExifV1,
|
||||||
|
SyncEntityType.albumAssetExifCreateV1,
|
||||||
|
SyncEntityType.albumAssetExifUpdateV1,
|
||||||
|
]);
|
||||||
|
migrations.add(SyncMigrationTask.v20260128_ResetExifV1.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!migrations.contains(SyncMigrationTask.v20260128_ResetAssetV1.name) &&
|
||||||
|
semVer >= const SemVer(major: 2, minor: 5, patch: 0)) {
|
||||||
|
_logger.info("Running pre-sync task: v20260128_ResetAssetV1");
|
||||||
|
await _syncApiRepository.deleteSyncAck([
|
||||||
|
SyncEntityType.assetV1,
|
||||||
|
SyncEntityType.partnerAssetV1,
|
||||||
|
SyncEntityType.albumAssetCreateV1,
|
||||||
|
SyncEntityType.albumAssetUpdateV1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
migrations.add(SyncMigrationTask.v20260128_ResetAssetV1.name);
|
||||||
|
|
||||||
|
if (!migrations.contains(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name)) {
|
||||||
|
migrations.add(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _runPostSyncTasks(List<String> migrations) async {
|
||||||
|
if (!migrations.contains(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name)) {
|
||||||
|
_logger.info("Running post-sync task: v20260128_CopyExifWidthHeightToAsset");
|
||||||
|
await _syncMigrationRepository.v20260128CopyExifWidthHeightToAsset();
|
||||||
|
migrations.add(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _handleEvents(List<SyncEvent> events, Function() abort, Function() reset) async {
|
Future<void> _handleEvents(List<SyncEvent> events, Function() abort, Function() reset) async {
|
||||||
List<SyncEvent> items = [];
|
List<SyncEvent> items = [];
|
||||||
for (final event in events) {
|
for (final event in events) {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ class SyncApiRepository {
|
||||||
return _api.syncApi.sendSyncAck(SyncAckSetDto(acks: data));
|
return _api.syncApi.sendSyncAck(SyncAckSetDto(acks: data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> deleteSyncAck(List<SyncEntityType> types) {
|
||||||
|
return _api.syncApi.deleteSyncAck(SyncAckDeleteDto(types: types));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> streamChanges(
|
Future<void> streamChanges(
|
||||||
Future<void> Function(List<SyncEvent>, Function() abort, Function() reset) onData, {
|
Future<void> Function(List<SyncEvent>, Function() abort, Function() reset) onData, {
|
||||||
Function()? onReset,
|
Function()? onReset,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
||||||
|
class SyncMigrationRepository extends DriftDatabaseRepository {
|
||||||
|
final Drift _db;
|
||||||
|
|
||||||
|
const SyncMigrationRepository(super.db) : _db = db;
|
||||||
|
|
||||||
|
Future<void> v20260128CopyExifWidthHeightToAsset() async {
|
||||||
|
await _db.customStatement('''
|
||||||
|
UPDATE remote_asset_entity
|
||||||
|
SET width = CASE
|
||||||
|
WHEN exif.orientation IN ('5', '6', '7', '8', '-90', '90') THEN exif.height
|
||||||
|
ELSE exif.width
|
||||||
|
END,
|
||||||
|
height = CASE
|
||||||
|
WHEN exif.orientation IN ('5', '6', '7', '8', '-90', '90') THEN exif.width
|
||||||
|
ELSE exif.height
|
||||||
|
END
|
||||||
|
FROM remote_exif_entity exif
|
||||||
|
WHERE exif.asset_id = remote_asset_entity.id
|
||||||
|
AND (exif.width IS NOT NULL OR exif.height IS NOT NULL);
|
||||||
|
''');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import 'package:immich_mobile/domain/services/hash.service.dart';
|
||||||
import 'package:immich_mobile/domain/services/local_sync.service.dart';
|
import 'package:immich_mobile/domain/services/local_sync.service.dart';
|
||||||
import 'package:immich_mobile/domain/services/sync_stream.service.dart';
|
import 'package:immich_mobile/domain/services/sync_stream.service.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/sync_migration.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/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
|
|
@ -13,6 +14,8 @@ import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||||
|
|
||||||
|
final syncMigrationRepositoryProvider = Provider((ref) => SyncMigrationRepository(ref.watch(driftProvider)));
|
||||||
|
|
||||||
final syncStreamServiceProvider = Provider(
|
final syncStreamServiceProvider = Provider(
|
||||||
(ref) => SyncStreamService(
|
(ref) => SyncStreamService(
|
||||||
syncApiRepository: ref.watch(syncApiRepositoryProvider),
|
syncApiRepository: ref.watch(syncApiRepositoryProvider),
|
||||||
|
|
@ -21,6 +24,8 @@ final syncStreamServiceProvider = Provider(
|
||||||
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
|
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
|
||||||
localFilesManager: ref.watch(localFilesManagerRepositoryProvider),
|
localFilesManager: ref.watch(localFilesManagerRepositoryProvider),
|
||||||
storageRepository: ref.watch(storageRepositoryProvider),
|
storageRepository: ref.watch(storageRepositoryProvider),
|
||||||
|
syncMigrationRepository: ref.watch(syncMigrationRepositoryProvider),
|
||||||
|
api: ref.watch(apiServiceProvider),
|
||||||
cancelChecker: ref.watch(cancellationProvider),
|
cancelChecker: ref.watch(cancellationProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ 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';
|
||||||
|
|
||||||
|
|
@ -88,7 +89,6 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
||||||
|
|
||||||
if (version < 20 && Store.isBetaTimelineEnabled) {
|
if (version < 20 && Store.isBetaTimelineEnabled) {
|
||||||
await _syncLocalAlbumIsIosSharedAlbum(drift);
|
await _syncLocalAlbumIsIosSharedAlbum(drift);
|
||||||
await _backfillAssetExifWidthHeight(drift);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetVersion >= 12) {
|
if (targetVersion >= 12) {
|
||||||
|
|
@ -282,22 +282,6 @@ Future<void> _syncLocalAlbumIsIosSharedAlbum(Drift db) async {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _backfillAssetExifWidthHeight(Drift db) async {
|
|
||||||
try {
|
|
||||||
await db.customStatement('''
|
|
||||||
UPDATE remote_exif_entity AS remote_exif
|
|
||||||
SET width = asset.width,
|
|
||||||
height = asset.height
|
|
||||||
FROM remote_asset_entity AS asset
|
|
||||||
WHERE remote_exif.asset_id = asset.id;
|
|
||||||
''');
|
|
||||||
|
|
||||||
dPrint(() => "[MIGRATION] Successfully backfilled asset exif width and height");
|
|
||||||
} catch (error) {
|
|
||||||
dPrint(() => "[MIGRATION] Error while backfilling asset exif width and height: $error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,5 @@ import 'package:openapi/api.dart';
|
||||||
class MockAssetsApi extends Mock implements AssetsApi {}
|
class MockAssetsApi extends Mock implements AssetsApi {}
|
||||||
|
|
||||||
class MockSyncApi extends Mock implements SyncApi {}
|
class MockSyncApi extends Mock implements SyncApi {}
|
||||||
|
|
||||||
|
class MockServerApi extends Mock implements ServerApi {}
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,15 @@ import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository
|
||||||
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
import '../../api.mocks.dart';
|
||||||
import '../../fixtures/asset.stub.dart';
|
import '../../fixtures/asset.stub.dart';
|
||||||
import '../../fixtures/sync_stream.stub.dart';
|
import '../../fixtures/sync_stream.stub.dart';
|
||||||
import '../../infrastructure/repository.mock.dart';
|
import '../../infrastructure/repository.mock.dart';
|
||||||
import '../../mocks/asset_entity.mock.dart';
|
import '../../mocks/asset_entity.mock.dart';
|
||||||
import '../../repository.mocks.dart';
|
import '../../repository.mocks.dart';
|
||||||
|
import '../../service.mocks.dart';
|
||||||
|
|
||||||
class _AbortCallbackWrapper {
|
class _AbortCallbackWrapper {
|
||||||
const _AbortCallbackWrapper();
|
const _AbortCallbackWrapper();
|
||||||
|
|
@ -50,6 +53,9 @@ void main() {
|
||||||
late DriftTrashedLocalAssetRepository mockTrashedLocalAssetRepo;
|
late DriftTrashedLocalAssetRepository mockTrashedLocalAssetRepo;
|
||||||
late LocalFilesManagerRepository mockLocalFilesManagerRepo;
|
late LocalFilesManagerRepository mockLocalFilesManagerRepo;
|
||||||
late StorageRepository mockStorageRepo;
|
late StorageRepository mockStorageRepo;
|
||||||
|
late MockApiService mockApi;
|
||||||
|
late MockServerApi mockServerApi;
|
||||||
|
late MockSyncMigrationRepository mockSyncMigrationRepo;
|
||||||
late Future<void> Function(List<SyncEvent>, Function(), Function()) handleEventsCallback;
|
late Future<void> Function(List<SyncEvent>, Function(), Function()) handleEventsCallback;
|
||||||
late _MockAbortCallbackWrapper mockAbortCallbackWrapper;
|
late _MockAbortCallbackWrapper mockAbortCallbackWrapper;
|
||||||
late _MockAbortCallbackWrapper mockResetCallbackWrapper;
|
late _MockAbortCallbackWrapper mockResetCallbackWrapper;
|
||||||
|
|
@ -82,6 +88,9 @@ void main() {
|
||||||
mockStorageRepo = MockStorageRepository();
|
mockStorageRepo = MockStorageRepository();
|
||||||
mockAbortCallbackWrapper = _MockAbortCallbackWrapper();
|
mockAbortCallbackWrapper = _MockAbortCallbackWrapper();
|
||||||
mockResetCallbackWrapper = _MockAbortCallbackWrapper();
|
mockResetCallbackWrapper = _MockAbortCallbackWrapper();
|
||||||
|
mockApi = MockApiService();
|
||||||
|
mockServerApi = MockServerApi();
|
||||||
|
mockSyncMigrationRepo = MockSyncMigrationRepository();
|
||||||
|
|
||||||
when(() => mockAbortCallbackWrapper()).thenReturn(false);
|
when(() => mockAbortCallbackWrapper()).thenReturn(false);
|
||||||
|
|
||||||
|
|
@ -94,6 +103,12 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
when(() => mockSyncApiRepo.ack(any())).thenAnswer((_) async => {});
|
when(() => mockSyncApiRepo.ack(any())).thenAnswer((_) async => {});
|
||||||
|
when(() => mockSyncApiRepo.deleteSyncAck(any())).thenAnswer((_) async => {});
|
||||||
|
|
||||||
|
when(() => mockApi.serverInfoApi).thenReturn(mockServerApi);
|
||||||
|
when(() => mockServerApi.getServerVersion()).thenAnswer(
|
||||||
|
(_) async => ServerVersionResponseDto(major: 1, minor: 132, patch_: 0),
|
||||||
|
);
|
||||||
|
|
||||||
when(() => mockSyncStreamRepo.updateUsersV1(any())).thenAnswer(successHandler);
|
when(() => mockSyncStreamRepo.updateUsersV1(any())).thenAnswer(successHandler);
|
||||||
when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer(successHandler);
|
when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer(successHandler);
|
||||||
|
|
@ -127,6 +142,7 @@ void main() {
|
||||||
when(() => mockSyncStreamRepo.deletePeopleV1(any())).thenAnswer(successHandler);
|
when(() => mockSyncStreamRepo.deletePeopleV1(any())).thenAnswer(successHandler);
|
||||||
when(() => mockSyncStreamRepo.updateAssetFacesV1(any())).thenAnswer(successHandler);
|
when(() => mockSyncStreamRepo.updateAssetFacesV1(any())).thenAnswer(successHandler);
|
||||||
when(() => mockSyncStreamRepo.deleteAssetFacesV1(any())).thenAnswer(successHandler);
|
when(() => mockSyncStreamRepo.deleteAssetFacesV1(any())).thenAnswer(successHandler);
|
||||||
|
when(() => mockSyncMigrationRepo.v20260128CopyExifWidthHeightToAsset()).thenAnswer(successHandler);
|
||||||
|
|
||||||
sut = SyncStreamService(
|
sut = SyncStreamService(
|
||||||
syncApiRepository: mockSyncApiRepo,
|
syncApiRepository: mockSyncApiRepo,
|
||||||
|
|
@ -135,6 +151,8 @@ void main() {
|
||||||
trashedLocalAssetRepository: mockTrashedLocalAssetRepo,
|
trashedLocalAssetRepository: mockTrashedLocalAssetRepo,
|
||||||
localFilesManager: mockLocalFilesManagerRepo,
|
localFilesManager: mockLocalFilesManagerRepo,
|
||||||
storageRepository: mockStorageRepo,
|
storageRepository: mockStorageRepo,
|
||||||
|
api: mockApi,
|
||||||
|
syncMigrationRepository: mockSyncMigrationRepo,
|
||||||
);
|
);
|
||||||
|
|
||||||
when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).thenAnswer((_) async => {});
|
when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).thenAnswer((_) async => {});
|
||||||
|
|
@ -216,6 +234,8 @@ void main() {
|
||||||
localFilesManager: mockLocalFilesManagerRepo,
|
localFilesManager: mockLocalFilesManagerRepo,
|
||||||
storageRepository: mockStorageRepo,
|
storageRepository: mockStorageRepo,
|
||||||
cancelChecker: cancellationChecker.call,
|
cancelChecker: cancellationChecker.call,
|
||||||
|
api: mockApi,
|
||||||
|
syncMigrationRepository: mockSyncMigrationRepo,
|
||||||
);
|
);
|
||||||
await sut.sync();
|
await sut.sync();
|
||||||
|
|
||||||
|
|
@ -255,6 +275,8 @@ void main() {
|
||||||
localFilesManager: mockLocalFilesManagerRepo,
|
localFilesManager: mockLocalFilesManagerRepo,
|
||||||
storageRepository: mockStorageRepo,
|
storageRepository: mockStorageRepo,
|
||||||
cancelChecker: cancellationChecker.call,
|
cancelChecker: cancellationChecker.call,
|
||||||
|
api: mockApi,
|
||||||
|
syncMigrationRepository: mockSyncMigrationRepo,
|
||||||
);
|
);
|
||||||
|
|
||||||
await sut.sync();
|
await sut.sync();
|
||||||
|
|
@ -474,11 +496,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
final events = [
|
final events = [
|
||||||
SyncStreamStub.assetModified(
|
SyncStreamStub.assetModified(id: 'remote-1', checksum: 'checksum-trash', ack: 'asset-remote-1-11'),
|
||||||
id: 'remote-1',
|
|
||||||
checksum: 'checksum-trash',
|
|
||||||
ack: 'asset-remote-1-11',
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
await simulateEvents(events);
|
await simulateEvents(events);
|
||||||
|
|
@ -486,4 +504,75 @@ void main() {
|
||||||
verify(() => mockTrashedLocalAssetRepo.applyRestoredAssets(restoredIds)).called(1);
|
verify(() => mockTrashedLocalAssetRepo.applyRestoredAssets(restoredIds)).called(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('SyncStreamService - Sync Migration', () {
|
||||||
|
test('ensure that <2.5.0 migrations run', () async {
|
||||||
|
await Store.put(StoreKey.syncMigrationStatus, "[]");
|
||||||
|
when(
|
||||||
|
() => mockServerApi.getServerVersion(),
|
||||||
|
).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1));
|
||||||
|
|
||||||
|
await sut.sync();
|
||||||
|
|
||||||
|
verifyInOrder([
|
||||||
|
() => mockSyncApiRepo.deleteSyncAck([
|
||||||
|
SyncEntityType.assetExifV1,
|
||||||
|
SyncEntityType.partnerAssetExifV1,
|
||||||
|
SyncEntityType.albumAssetExifCreateV1,
|
||||||
|
SyncEntityType.albumAssetExifUpdateV1,
|
||||||
|
]),
|
||||||
|
() => mockSyncMigrationRepo.v20260128CopyExifWidthHeightToAsset(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// should only run on server >2.5.0
|
||||||
|
verifyNever(
|
||||||
|
() => mockSyncApiRepo.deleteSyncAck([
|
||||||
|
SyncEntityType.assetV1,
|
||||||
|
SyncEntityType.partnerAssetV1,
|
||||||
|
SyncEntityType.albumAssetCreateV1,
|
||||||
|
SyncEntityType.albumAssetUpdateV1,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test('ensure that >=2.5.0 migrations run', () async {
|
||||||
|
await Store.put(StoreKey.syncMigrationStatus, "[]");
|
||||||
|
when(
|
||||||
|
() => mockServerApi.getServerVersion(),
|
||||||
|
).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 5, patch_: 0));
|
||||||
|
await sut.sync();
|
||||||
|
|
||||||
|
verifyInOrder([
|
||||||
|
() => mockSyncApiRepo.deleteSyncAck([
|
||||||
|
SyncEntityType.assetExifV1,
|
||||||
|
SyncEntityType.partnerAssetExifV1,
|
||||||
|
SyncEntityType.albumAssetExifCreateV1,
|
||||||
|
SyncEntityType.albumAssetExifUpdateV1,
|
||||||
|
]),
|
||||||
|
() => mockSyncApiRepo.deleteSyncAck([
|
||||||
|
SyncEntityType.assetV1,
|
||||||
|
SyncEntityType.partnerAssetV1,
|
||||||
|
SyncEntityType.albumAssetCreateV1,
|
||||||
|
SyncEntityType.albumAssetUpdateV1,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// v20260128_ResetAssetV1 writes that v20260128_CopyExifWidthHeightToAsset has been completed
|
||||||
|
verifyNever(() => mockSyncMigrationRepo.v20260128CopyExifWidthHeightToAsset());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ensure that migrations do not re-run', () async {
|
||||||
|
await Store.put(
|
||||||
|
StoreKey.syncMigrationStatus,
|
||||||
|
'["${SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name}"]',
|
||||||
|
);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => mockServerApi.getServerVersion(),
|
||||||
|
).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1));
|
||||||
|
|
||||||
|
await sut.sync();
|
||||||
|
|
||||||
|
verifyNever(() => mockSyncMigrationRepo.v20260128CopyExifWidthHeightToAsset());
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import 'package:immich_mobile/infrastructure/repositories/remote_asset.repositor
|
||||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/sync_migration.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/infrastructure/repositories/trashed_local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
|
||||||
|
|
@ -46,6 +47,8 @@ class MockDriftBackupRepository extends Mock implements DriftBackupRepository {}
|
||||||
|
|
||||||
class MockUploadRepository extends Mock implements UploadRepository {}
|
class MockUploadRepository extends Mock implements UploadRepository {}
|
||||||
|
|
||||||
|
class MockSyncMigrationRepository extends Mock implements SyncMigrationRepository {}
|
||||||
|
|
||||||
// API Repos
|
// API Repos
|
||||||
class MockUserApiRepository extends Mock implements UserApiRepository {}
|
class MockUserApiRepository extends Mock implements UserApiRepository {}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue