From ebfab4b01ba84548c28ca32e2953c85ef1b85303 Mon Sep 17 00:00:00 2001 From: Peter Ombodi Date: Tue, 7 Oct 2025 14:18:45 +0300 Subject: [PATCH] sync_stream.service depend on repos refactor assets restoration update dependencies in tests --- .../domain/services/local_sync.service.dart | 14 +---- .../domain/services/sync_stream.service.dart | 63 ++++++++++++++++--- .../trashed_local_asset.repository.dart | 2 +- .../infrastructure/sync.provider.dart | 7 ++- .../infrastructure/trash_sync.provider.dart | 14 ----- .../local_files_manager.repository.dart | 19 +++++- .../sync_status_and_actions.dart | 9 ++- mobile/test/domain/service.mock.dart | 2 - .../services/sync_stream_service_test.dart | 32 +++++++--- 9 files changed, 112 insertions(+), 50 deletions(-) diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart index 579eba15d4..7fec2d4d4f 100644 --- a/mobile/lib/domain/services/local_sync.service.dart +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -328,19 +328,9 @@ class LocalSyncService { Future _applyRemoteRestoreToLocal() async { final remoteAssetsToRestore = await _trashedLocalAssetRepository.getToRestore(); - final toRestoreIds = []; if (remoteAssetsToRestore.isNotEmpty) { - _log.info("remoteAssetsToRestore: $remoteAssetsToRestore"); - for (final asset in remoteAssetsToRestore) { - _log.info("Restoring from trash, localId: ${asset.id}, remoteId: ${asset.checksum}"); - try { - await _localFilesManager.restoreFromTrashById(asset.id, asset.type.index); - toRestoreIds.add(asset.id); - } catch (e) { - _log.warning("Restoring failure: $e"); - } - } - await _trashedLocalAssetRepository.restoreLocalAssets(toRestoreIds); + final restoredIds = await _localFilesManager.restoreAssetsFromTrash(remoteAssetsToRestore); + await _trashedLocalAssetRepository.applyRestoredAssets(restoredIds); } else { _log.info("No remote assets found for restoration"); } diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index b4349de523..ba3fabf620 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -1,9 +1,13 @@ import 'dart:async'; import 'package:immich_mobile/domain/models/sync_event.model.dart'; -import 'package:immich_mobile/domain/services/trash_sync.service.dart'; +import 'package:immich_mobile/extensions/platform_extensions.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/sync_api.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/repositories/local_files_manager.repository.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; @@ -12,17 +16,26 @@ class SyncStreamService { final SyncApiRepository _syncApiRepository; final SyncStreamRepository _syncStreamRepository; - final TrashSyncService _trashSyncService; + final DriftLocalAssetRepository _localAssetRepository; + final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository; + final LocalFilesManagerRepository _localFilesManager; + final StorageRepository _storageRepository; final bool Function()? _cancelChecker; SyncStreamService({ required SyncApiRepository syncApiRepository, required SyncStreamRepository syncStreamRepository, - required TrashSyncService trashSyncService, + required DriftLocalAssetRepository localAssetRepository, + required DriftTrashedLocalAssetRepository trashedLocalAssetRepository, + required LocalFilesManagerRepository localFilesManager, + required StorageRepository storageRepository, bool Function()? cancelChecker, }) : _syncApiRepository = syncApiRepository, _syncStreamRepository = syncStreamRepository, - _trashSyncService = trashSyncService, + _localAssetRepository = localAssetRepository, + _trashedLocalAssetRepository = trashedLocalAssetRepository, + _localFilesManager = localFilesManager, + _storageRepository = storageRepository, _cancelChecker = cancelChecker; bool get isCancelled => _cancelChecker?.call() ?? false; @@ -88,12 +101,12 @@ class SyncStreamService { return _syncStreamRepository.deletePartnerV1(data.cast()); case SyncEntityType.assetV1: final remoteSyncAssets = data.cast(); - if (_trashSyncService.isTrashSyncMode) { - await _trashSyncService.handleRemoteTrashed( - remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.checksum), - ); + await _syncStreamRepository.updateAssetsV1(remoteSyncAssets); + if (CurrentPlatform.isAndroid) { + await _handleRemoteTrashed(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.checksum)); + await _applyRemoteRestoreToLocal(); } - return _syncStreamRepository.updateAssetsV1(remoteSyncAssets); + return Future.value(); case SyncEntityType.assetDeleteV1: return _syncStreamRepository.deleteAssetsV1(data.cast()); case SyncEntityType.assetExifV1: @@ -221,4 +234,36 @@ class SyncStreamService { _logger.severe("Error processing AssetUploadReadyV1 websocket batch events", error, stackTrace); } } + + Future _handleRemoteTrashed(Iterable checksums) async { + if (checksums.isEmpty) { + return Future.value(); + } else { + final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(checksums); + if (localAssetsToTrash.isNotEmpty) { + final mediaUrls = await Future.wait( + localAssetsToTrash.values + .expand((e) => e) + .map((localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl())), + ); + _logger.info("Moving to trash ${mediaUrls.join(", ")} assets"); + final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList()); + if (result) { + await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash); + } + } else { + _logger.info("No assets found in backup-enabled albums for assets: $checksums"); + } + } + } + + Future _applyRemoteRestoreToLocal() async { + final remoteAssetsToRestore = await _trashedLocalAssetRepository.getToRestore(); + if (remoteAssetsToRestore.isNotEmpty) { + final restoredIds = await _localFilesManager.restoreAssetsFromTrash(remoteAssetsToRestore); + await _trashedLocalAssetRepository.applyRestoredAssets(restoredIds); + } else { + _logger.info("No remote assets found for restoration"); + } + } } diff --git a/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart b/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart index 9774bb144c..d653c58a6b 100644 --- a/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart @@ -199,7 +199,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { }); } - Future restoreLocalAssets(Iterable ids) async { + Future applyRestoredAssets(Iterable ids) async { if (ids.isEmpty) { return; } diff --git a/mobile/lib/providers/infrastructure/sync.provider.dart b/mobile/lib/providers/infrastructure/sync.provider.dart index f783d47559..2fbb3670d2 100644 --- a/mobile/lib/providers/infrastructure/sync.provider.dart +++ b/mobile/lib/providers/infrastructure/sync.provider.dart @@ -10,14 +10,17 @@ import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; final syncStreamServiceProvider = Provider( (ref) => SyncStreamService( syncApiRepository: ref.watch(syncApiRepositoryProvider), syncStreamRepository: ref.watch(syncStreamRepositoryProvider), - trashSyncService: ref.watch(trashSyncServiceProvider), + localAssetRepository: ref.watch(localAssetRepository), + trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository), + localFilesManager: ref.watch(localFilesManagerRepositoryProvider), + storageRepository: ref.watch(storageRepositoryProvider), cancelChecker: ref.watch(cancellationProvider), ), ); diff --git a/mobile/lib/providers/infrastructure/trash_sync.provider.dart b/mobile/lib/providers/infrastructure/trash_sync.provider.dart index 0fe3876c8a..a783080f33 100644 --- a/mobile/lib/providers/infrastructure/trash_sync.provider.dart +++ b/mobile/lib/providers/infrastructure/trash_sync.provider.dart @@ -1,23 +1,9 @@ import 'package:async/async.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/trash_sync.service.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; -import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; typedef TrashedAssetsCount = ({int total, int hashed}); -final trashSyncServiceProvider = Provider( - (ref) => TrashSyncService( - appSettingsService: ref.watch(appSettingsServiceProvider), - localAssetRepository: ref.watch(localAssetRepository), - trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository), - localFilesManager: ref.watch(localFilesManagerRepositoryProvider), - storageRepository: ref.watch(storageRepositoryProvider), - ), -); - final trashedAssetsCountProvider = StreamProvider((ref) { final repo = ref.watch(trashedLocalAssetRepository); final total$ = repo.watchCount(); diff --git a/mobile/lib/repositories/local_files_manager.repository.dart b/mobile/lib/repositories/local_files_manager.repository.dart index cfa040e4c0..ba2adf3e14 100644 --- a/mobile/lib/repositories/local_files_manager.repository.dart +++ b/mobile/lib/repositories/local_files_manager.repository.dart @@ -1,13 +1,16 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/services/local_files_manager.service.dart'; +import 'package:logging/logging.dart'; final localFilesManagerRepositoryProvider = Provider( (ref) => LocalFilesManagerRepository(ref.watch(localFileManagerServiceProvider)), ); class LocalFilesManagerRepository { - const LocalFilesManagerRepository(this._service); + LocalFilesManagerRepository(this._service); + final Logger _logger = Logger('SyncStreamService'); final LocalFilesManagerService _service; Future moveToTrash(List mediaUrls) async { @@ -25,4 +28,18 @@ class LocalFilesManagerRepository { Future requestManageMediaPermission() async { return await _service.requestManageMediaPermission(); } + + Future> restoreAssetsFromTrash(Iterable assets) async { + final restoredIds = []; + for (final asset in assets) { + _logger.info("Restoring from trash, localId: ${asset.id}, remoteId: ${asset.checksum}"); + try { + await _service.restoreFromTrashById(asset.id, asset.type.index); + restoredIds.add(asset.id); + } catch (e) { + _logger.warning("Restoring failure: $e"); + } + } + return restoredIds; + } } diff --git a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart index 76481004ec..0296a6bd99 100644 --- a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart +++ b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart @@ -3,7 +3,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; @@ -12,6 +14,7 @@ import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; @@ -230,7 +233,7 @@ class _SyncStatsCounts extends ConsumerWidget { final localAlbumService = ref.watch(localAlbumServiceProvider); final remoteAlbumService = ref.watch(remoteAlbumServiceProvider); final memoryService = ref.watch(driftMemoryServiceProvider); - final trashSyncService = ref.watch(trashSyncServiceProvider); + final appSettingsService = ref.watch(appSettingsServiceProvider); Future> loadCounts() async { final assetCounts = assetService.getAssetCounts(); @@ -353,7 +356,9 @@ class _SyncStatsCounts extends ConsumerWidget { ], ), ), - if (trashSyncService.isTrashSyncMode) ...[ + // To be removed once the experimental feature is stable + if (CurrentPlatform.isAndroid && + appSettingsService.getSetting(AppSettingsEnum.manageLocalMediaAndroid)) ...[ _SectionHeaderText(text: "trash".t(context: context)), Consumer( builder: (context, ref, _) { diff --git a/mobile/test/domain/service.mock.dart b/mobile/test/domain/service.mock.dart index 174a892abc..0bab675889 100644 --- a/mobile/test/domain/service.mock.dart +++ b/mobile/test/domain/service.mock.dart @@ -1,5 +1,4 @@ import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/domain/services/trash_sync.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; @@ -19,4 +18,3 @@ class MockAppSettingsService extends Mock implements AppSettingsService {} class MockUploadService extends Mock implements UploadService {} -class MockTrashSyncService extends Mock implements TrashSyncService {} diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index abc33e435f..90a217189a 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -3,14 +3,17 @@ import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/services/sync_stream.service.dart'; -import 'package:immich_mobile/domain/services/trash_sync.service.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/sync_api.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/repositories/local_files_manager.repository.dart'; import 'package:mocktail/mocktail.dart'; import '../../fixtures/sync_stream.stub.dart'; import '../../infrastructure/repository.mock.dart'; -import '../service.mock.dart'; +import '../../repository.mocks.dart'; class _AbortCallbackWrapper { const _AbortCallbackWrapper(); @@ -32,7 +35,10 @@ void main() { late SyncStreamService sut; late SyncStreamRepository mockSyncStreamRepo; late SyncApiRepository mockSyncApiRepo; - late TrashSyncService mockTrashService; + late DriftLocalAssetRepository mockLocalAssetRepo; + late DriftTrashedLocalAssetRepository mockTrashedLocalAssetRepo; + late LocalFilesManagerRepository mockLocalFilesManagerRepo; + late StorageRepository mockStorageRepo; late Future Function(List, Function(), Function()) handleEventsCallback; late _MockAbortCallbackWrapper mockAbortCallbackWrapper; late _MockAbortCallbackWrapper mockResetCallbackWrapper; @@ -42,8 +48,11 @@ void main() { setUp(() { mockSyncStreamRepo = MockSyncStreamRepository(); mockSyncApiRepo = MockSyncApiRepository(); + mockLocalAssetRepo = MockLocalAssetRepository(); + mockTrashedLocalAssetRepo = MockTrashedLocalAssetRepository(); + mockLocalFilesManagerRepo = MockLocalFilesManagerRepository(); + mockStorageRepo = MockStorageRepository(); mockAbortCallbackWrapper = _MockAbortCallbackWrapper(); - mockTrashService = MockTrashSyncService(); mockResetCallbackWrapper = _MockAbortCallbackWrapper(); when(() => mockAbortCallbackWrapper()).thenReturn(false); @@ -94,7 +103,10 @@ void main() { sut = SyncStreamService( syncApiRepository: mockSyncApiRepo, syncStreamRepository: mockSyncStreamRepo, - trashSyncService: mockTrashService, + localAssetRepository: mockLocalAssetRepo, + trashedLocalAssetRepository: mockTrashedLocalAssetRepo, + localFilesManager: mockLocalFilesManagerRepo, + storageRepository: mockStorageRepo, ); }); @@ -160,7 +172,10 @@ void main() { sut = SyncStreamService( syncApiRepository: mockSyncApiRepo, syncStreamRepository: mockSyncStreamRepo, - trashSyncService: mockTrashService, + localAssetRepository: mockLocalAssetRepo, + trashedLocalAssetRepository: mockTrashedLocalAssetRepo, + localFilesManager: mockLocalFilesManagerRepo, + storageRepository: mockStorageRepo, cancelChecker: cancellationChecker.call, ); await sut.sync(); @@ -196,7 +211,10 @@ void main() { sut = SyncStreamService( syncApiRepository: mockSyncApiRepo, syncStreamRepository: mockSyncStreamRepo, - trashSyncService: mockTrashService, + localAssetRepository: mockLocalAssetRepo, + trashedLocalAssetRepository: mockTrashedLocalAssetRepo, + localFilesManager: mockLocalFilesManagerRepo, + storageRepository: mockStorageRepo, cancelChecker: cancellationChecker.call, );