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,
);