sync_stream.service depend on repos

refactor assets restoration
update dependencies in tests
This commit is contained in:
Peter Ombodi 2025-10-07 14:18:45 +03:00
parent ca43c7907e
commit ebfab4b01b
9 changed files with 112 additions and 50 deletions

View file

@ -328,19 +328,9 @@ class LocalSyncService {
Future<void> _applyRemoteRestoreToLocal() async { Future<void> _applyRemoteRestoreToLocal() async {
final remoteAssetsToRestore = await _trashedLocalAssetRepository.getToRestore(); final remoteAssetsToRestore = await _trashedLocalAssetRepository.getToRestore();
final toRestoreIds = <String>[];
if (remoteAssetsToRestore.isNotEmpty) { if (remoteAssetsToRestore.isNotEmpty) {
_log.info("remoteAssetsToRestore: $remoteAssetsToRestore"); final restoredIds = await _localFilesManager.restoreAssetsFromTrash(remoteAssetsToRestore);
for (final asset in remoteAssetsToRestore) { await _trashedLocalAssetRepository.applyRestoredAssets(restoredIds);
_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);
} else { } else {
_log.info("No remote assets found for restoration"); _log.info("No remote assets found for restoration");
} }

View file

@ -1,9 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:immich_mobile/domain/models/sync_event.model.dart'; 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_api.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/repositories/local_files_manager.repository.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@ -12,17 +16,26 @@ class SyncStreamService {
final SyncApiRepository _syncApiRepository; final SyncApiRepository _syncApiRepository;
final SyncStreamRepository _syncStreamRepository; final SyncStreamRepository _syncStreamRepository;
final TrashSyncService _trashSyncService; final DriftLocalAssetRepository _localAssetRepository;
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
final LocalFilesManagerRepository _localFilesManager;
final StorageRepository _storageRepository;
final bool Function()? _cancelChecker; final bool Function()? _cancelChecker;
SyncStreamService({ SyncStreamService({
required SyncApiRepository syncApiRepository, required SyncApiRepository syncApiRepository,
required SyncStreamRepository syncStreamRepository, required SyncStreamRepository syncStreamRepository,
required TrashSyncService trashSyncService, required DriftLocalAssetRepository localAssetRepository,
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
required LocalFilesManagerRepository localFilesManager,
required StorageRepository storageRepository,
bool Function()? cancelChecker, bool Function()? cancelChecker,
}) : _syncApiRepository = syncApiRepository, }) : _syncApiRepository = syncApiRepository,
_syncStreamRepository = syncStreamRepository, _syncStreamRepository = syncStreamRepository,
_trashSyncService = trashSyncService, _localAssetRepository = localAssetRepository,
_trashedLocalAssetRepository = trashedLocalAssetRepository,
_localFilesManager = localFilesManager,
_storageRepository = storageRepository,
_cancelChecker = cancelChecker; _cancelChecker = cancelChecker;
bool get isCancelled => _cancelChecker?.call() ?? false; bool get isCancelled => _cancelChecker?.call() ?? false;
@ -88,12 +101,12 @@ class SyncStreamService {
return _syncStreamRepository.deletePartnerV1(data.cast()); return _syncStreamRepository.deletePartnerV1(data.cast());
case SyncEntityType.assetV1: case SyncEntityType.assetV1:
final remoteSyncAssets = data.cast<SyncAssetV1>(); final remoteSyncAssets = data.cast<SyncAssetV1>();
if (_trashSyncService.isTrashSyncMode) { await _syncStreamRepository.updateAssetsV1(remoteSyncAssets);
await _trashSyncService.handleRemoteTrashed( if (CurrentPlatform.isAndroid) {
remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.checksum), await _handleRemoteTrashed(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.checksum));
); await _applyRemoteRestoreToLocal();
} }
return _syncStreamRepository.updateAssetsV1(remoteSyncAssets); return Future.value();
case SyncEntityType.assetDeleteV1: case SyncEntityType.assetDeleteV1:
return _syncStreamRepository.deleteAssetsV1(data.cast()); return _syncStreamRepository.deleteAssetsV1(data.cast());
case SyncEntityType.assetExifV1: case SyncEntityType.assetExifV1:
@ -221,4 +234,36 @@ class SyncStreamService {
_logger.severe("Error processing AssetUploadReadyV1 websocket batch events", error, stackTrace); _logger.severe("Error processing AssetUploadReadyV1 websocket batch events", error, stackTrace);
} }
} }
Future<void> _handleRemoteTrashed(Iterable<String> 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<void> _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");
}
}
} }

View file

@ -199,7 +199,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
}); });
} }
Future<void> restoreLocalAssets(Iterable<String> ids) async { Future<void> applyRestoredAssets(Iterable<String> ids) async {
if (ids.isEmpty) { if (ids.isEmpty) {
return; return;
} }

View file

@ -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/cancel.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.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/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'; import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
final syncStreamServiceProvider = Provider( final syncStreamServiceProvider = Provider(
(ref) => SyncStreamService( (ref) => SyncStreamService(
syncApiRepository: ref.watch(syncApiRepositoryProvider), syncApiRepository: ref.watch(syncApiRepositoryProvider),
syncStreamRepository: ref.watch(syncStreamRepositoryProvider), 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), cancelChecker: ref.watch(cancellationProvider),
), ),
); );

View file

@ -1,23 +1,9 @@
import 'package:async/async.dart'; import 'package:async/async.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/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}); 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<TrashedAssetsCount>((ref) { final trashedAssetsCountProvider = StreamProvider<TrashedAssetsCount>((ref) {
final repo = ref.watch(trashedLocalAssetRepository); final repo = ref.watch(trashedLocalAssetRepository);
final total$ = repo.watchCount(); final total$ = repo.watchCount();

View file

@ -1,13 +1,16 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; 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:immich_mobile/services/local_files_manager.service.dart';
import 'package:logging/logging.dart';
final localFilesManagerRepositoryProvider = Provider( final localFilesManagerRepositoryProvider = Provider(
(ref) => LocalFilesManagerRepository(ref.watch(localFileManagerServiceProvider)), (ref) => LocalFilesManagerRepository(ref.watch(localFileManagerServiceProvider)),
); );
class LocalFilesManagerRepository { class LocalFilesManagerRepository {
const LocalFilesManagerRepository(this._service); LocalFilesManagerRepository(this._service);
final Logger _logger = Logger('SyncStreamService');
final LocalFilesManagerService _service; final LocalFilesManagerService _service;
Future<bool> moveToTrash(List<String> mediaUrls) async { Future<bool> moveToTrash(List<String> mediaUrls) async {
@ -25,4 +28,18 @@ class LocalFilesManagerRepository {
Future<bool> requestManageMediaPermission() async { Future<bool> requestManageMediaPermission() async {
return await _service.requestManageMediaPermission(); return await _service.requestManageMediaPermission();
} }
Future<List<String>> restoreAssetsFromTrash(Iterable<LocalAsset> assets) async {
final restoredIds = <String>[];
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;
}
} }

View file

@ -3,7 +3,9 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.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/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/background_sync.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.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/storage.provider.dart';
import 'package:immich_mobile/providers/infrastructure/trash_sync.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/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:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@ -230,7 +233,7 @@ class _SyncStatsCounts extends ConsumerWidget {
final localAlbumService = ref.watch(localAlbumServiceProvider); final localAlbumService = ref.watch(localAlbumServiceProvider);
final remoteAlbumService = ref.watch(remoteAlbumServiceProvider); final remoteAlbumService = ref.watch(remoteAlbumServiceProvider);
final memoryService = ref.watch(driftMemoryServiceProvider); final memoryService = ref.watch(driftMemoryServiceProvider);
final trashSyncService = ref.watch(trashSyncServiceProvider); final appSettingsService = ref.watch(appSettingsServiceProvider);
Future<List<dynamic>> loadCounts() async { Future<List<dynamic>> loadCounts() async {
final assetCounts = assetService.getAssetCounts(); 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<bool>(AppSettingsEnum.manageLocalMediaAndroid)) ...[
_SectionHeaderText(text: "trash".t(context: context)), _SectionHeaderText(text: "trash".t(context: context)),
Consumer( Consumer(
builder: (context, ref, _) { builder: (context, ref, _) {

View file

@ -1,5 +1,4 @@
import 'package:immich_mobile/domain/services/store.service.dart'; 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/services/user.service.dart';
import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart';
import 'package:immich_mobile/platform/native_sync_api.g.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 MockUploadService extends Mock implements UploadService {}
class MockTrashSyncService extends Mock implements TrashSyncService {}

View file

@ -3,14 +3,17 @@ import 'dart:async';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/sync_event.model.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/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_api.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/repositories/local_files_manager.repository.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import '../../fixtures/sync_stream.stub.dart'; import '../../fixtures/sync_stream.stub.dart';
import '../../infrastructure/repository.mock.dart'; import '../../infrastructure/repository.mock.dart';
import '../service.mock.dart'; import '../../repository.mocks.dart';
class _AbortCallbackWrapper { class _AbortCallbackWrapper {
const _AbortCallbackWrapper(); const _AbortCallbackWrapper();
@ -32,7 +35,10 @@ void main() {
late SyncStreamService sut; late SyncStreamService sut;
late SyncStreamRepository mockSyncStreamRepo; late SyncStreamRepository mockSyncStreamRepo;
late SyncApiRepository mockSyncApiRepo; late SyncApiRepository mockSyncApiRepo;
late TrashSyncService mockTrashService; late DriftLocalAssetRepository mockLocalAssetRepo;
late DriftTrashedLocalAssetRepository mockTrashedLocalAssetRepo;
late LocalFilesManagerRepository mockLocalFilesManagerRepo;
late StorageRepository mockStorageRepo;
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;
@ -42,8 +48,11 @@ void main() {
setUp(() { setUp(() {
mockSyncStreamRepo = MockSyncStreamRepository(); mockSyncStreamRepo = MockSyncStreamRepository();
mockSyncApiRepo = MockSyncApiRepository(); mockSyncApiRepo = MockSyncApiRepository();
mockLocalAssetRepo = MockLocalAssetRepository();
mockTrashedLocalAssetRepo = MockTrashedLocalAssetRepository();
mockLocalFilesManagerRepo = MockLocalFilesManagerRepository();
mockStorageRepo = MockStorageRepository();
mockAbortCallbackWrapper = _MockAbortCallbackWrapper(); mockAbortCallbackWrapper = _MockAbortCallbackWrapper();
mockTrashService = MockTrashSyncService();
mockResetCallbackWrapper = _MockAbortCallbackWrapper(); mockResetCallbackWrapper = _MockAbortCallbackWrapper();
when(() => mockAbortCallbackWrapper()).thenReturn(false); when(() => mockAbortCallbackWrapper()).thenReturn(false);
@ -94,7 +103,10 @@ void main() {
sut = SyncStreamService( sut = SyncStreamService(
syncApiRepository: mockSyncApiRepo, syncApiRepository: mockSyncApiRepo,
syncStreamRepository: mockSyncStreamRepo, syncStreamRepository: mockSyncStreamRepo,
trashSyncService: mockTrashService, localAssetRepository: mockLocalAssetRepo,
trashedLocalAssetRepository: mockTrashedLocalAssetRepo,
localFilesManager: mockLocalFilesManagerRepo,
storageRepository: mockStorageRepo,
); );
}); });
@ -160,7 +172,10 @@ void main() {
sut = SyncStreamService( sut = SyncStreamService(
syncApiRepository: mockSyncApiRepo, syncApiRepository: mockSyncApiRepo,
syncStreamRepository: mockSyncStreamRepo, syncStreamRepository: mockSyncStreamRepo,
trashSyncService: mockTrashService, localAssetRepository: mockLocalAssetRepo,
trashedLocalAssetRepository: mockTrashedLocalAssetRepo,
localFilesManager: mockLocalFilesManagerRepo,
storageRepository: mockStorageRepo,
cancelChecker: cancellationChecker.call, cancelChecker: cancellationChecker.call,
); );
await sut.sync(); await sut.sync();
@ -196,7 +211,10 @@ void main() {
sut = SyncStreamService( sut = SyncStreamService(
syncApiRepository: mockSyncApiRepo, syncApiRepository: mockSyncApiRepo,
syncStreamRepository: mockSyncStreamRepo, syncStreamRepository: mockSyncStreamRepo,
trashSyncService: mockTrashService, localAssetRepository: mockLocalAssetRepo,
trashedLocalAssetRepository: mockTrashedLocalAssetRepo,
localFilesManager: mockLocalFilesManagerRepo,
storageRepository: mockStorageRepo,
cancelChecker: cancellationChecker.call, cancelChecker: cancellationChecker.call,
); );