diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart index bf3cd1ae90e9..bcbbfbb70633 100644 --- a/mobile/lib/domain/services/remote_album.service.dart +++ b/mobile/lib/domain/services/remote_album.service.dart @@ -257,7 +257,7 @@ class RemoteAlbumService { }, ); - await _uploadService.uploadManual(localAssets, callbacks: wrappedCallbacks); + await _uploadService.uploadManual(localAssets, cancelToken: null, callbacks: wrappedCallbacks); await Future.wait(pendingAdds); return addedCount; } diff --git a/mobile/lib/infrastructure/repositories/storage.repository.dart b/mobile/lib/infrastructure/repositories/storage.repository.dart index 7c0e9927e349..5630c012c085 100644 --- a/mobile/lib/infrastructure/repositories/storage.repository.dart +++ b/mobile/lib/infrastructure/repositories/storage.repository.dart @@ -6,31 +6,25 @@ import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:logging/logging.dart'; import 'package:photo_manager/photo_manager.dart'; +typedef OnProgress = void Function(String id, double progress); + class StorageRepository { static final log = Logger('StorageRepository'); const StorageRepository(); - Future getFileForAsset( - String assetId, { - void Function(String id, double progress)? onProgress, - Completer? cancelToken, - }) { + Future getAssetFile(String assetId, {OnProgress? onProgress, Completer? cancelToken}) { return _getFileForAsset(assetId, isMotion: false, onProgress: onProgress, cancelToken: cancelToken); } - Future getMotionFileForAsset( - String assetId, { - void Function(String id, double progress)? onProgress, - Completer? cancelToken, - }) { + Future getMotionFile(String assetId, {OnProgress? onProgress, Completer? cancelToken}) { return _getFileForAsset(assetId, isMotion: true, onProgress: onProgress, cancelToken: cancelToken); } Future _getFileForAsset( String assetId, { bool isMotion = false, - void Function(String id, double progress)? onProgress, + OnProgress? onProgress, Completer? cancelToken, }) async { final entity = await AssetEntity.fromId(assetId); diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart index 97ca8ace1061..8efc380dd517 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -6,13 +6,13 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; import 'package:native_video_player/native_video_player.dart'; @@ -108,7 +108,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg try { if (videoAsset.hasLocal && videoAsset.livePhotoVideoId == null) { final id = videoAsset is LocalAsset ? videoAsset.id : (videoAsset as RemoteAsset).localId!; - final file = await StorageRepository().getFileForAsset(id); + final file = await ref.read(storageRepositoryProvider).getAssetFile(id); if (!mounted) { return null; } diff --git a/mobile/lib/providers/backup/drift_backup.provider.dart b/mobile/lib/providers/backup/drift_backup.provider.dart index 536dbe80c52d..51bab1b06b14 100644 --- a/mobile/lib/providers/backup/drift_backup.provider.dart +++ b/mobile/lib/providers/backup/drift_backup.provider.dart @@ -283,7 +283,7 @@ class DriftBackupNotifier extends StateNotifier { _cancelToken?.complete(); _cancelToken = null; _uploadSpeedManager.clear(); - state = state.copyWith(uploadItems: {}, iCloudDownloadProgress: {}); + state = state.copyWith(uploadItems: const {}, iCloudDownloadProgress: const {}); } void _handleICloudProgress(String localAssetId, double progress) { diff --git a/mobile/lib/providers/infrastructure/storage.provider.dart b/mobile/lib/providers/infrastructure/storage.provider.dart index 82d1209c97c2..ccca9640272a 100644 --- a/mobile/lib/providers/infrastructure/storage.provider.dart +++ b/mobile/lib/providers/infrastructure/storage.provider.dart @@ -1,4 +1,4 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; -final storageRepositoryProvider = Provider((ref) => StorageRepository()); +final storageRepositoryProvider = Provider((ref) => const StorageRepository()); diff --git a/mobile/lib/services/background_upload.service.dart b/mobile/lib/services/background_upload.service.dart index 37577e3666af..77c1a9763e56 100644 --- a/mobile/lib/services/background_upload.service.dart +++ b/mobile/lib/services/background_upload.service.dart @@ -266,8 +266,6 @@ class BackgroundUploadService { return null; } - File? file; - /// iOS LivePhoto has two files: a photo and a video. /// They are uploaded separately, with video file being upload first, then returned with the assetId /// The assetId is then used as a metadata for the photo file upload task. @@ -278,11 +276,9 @@ class BackgroundUploadService { /// The cancel operation will only cancel the video group (normal group), the photo group will not /// be touched, as the video file is already uploaded. - if (entity.isLivePhoto) { - file = await _storageRepository.getMotionFileForAsset(asset); - } else { - file = await _storageRepository.getFileForAsset(asset.id); - } + final file = await (entity.isLivePhoto + ? _storageRepository.getMotionFile(asset.id) + : _storageRepository.getAssetFile(asset.id)); if (file == null) { _logger.warning("Failed to get file for asset ${asset.id} - ${asset.name}"); @@ -330,7 +326,7 @@ class BackgroundUploadService { return null; } - final file = await _storageRepository.getFileForAsset(asset.id); + final file = await _storageRepository.getAssetFile(asset.id); if (file == null) { return null; } diff --git a/mobile/lib/services/foreground_upload.service.dart b/mobile/lib/services/foreground_upload.service.dart index f195ccee5402..3daf48f108e5 100644 --- a/mobile/lib/services/foreground_upload.service.dart +++ b/mobile/lib/services/foreground_upload.service.dart @@ -98,7 +98,7 @@ class ForegroundUploadService { final requireWifi = _shouldRequireWiFi(asset); return requireWifi && !hasWifi; }, - processItem: (asset) => _uploadSingleAsset(asset, cancelToken, callbacks: callbacks), + processItem: (asset) => _uploadSingleAsset(asset, cancelToken: cancelToken, callbacks: callbacks), ); } } @@ -124,14 +124,14 @@ class ForegroundUploadService { continue; } - await _uploadSingleAsset(asset, cancelToken, callbacks: callbacks); + await _uploadSingleAsset(asset, cancelToken: cancelToken, callbacks: callbacks); } } /// Manually upload picked local assets Future uploadManual( List localAssets, { - Completer? cancelToken, + required Completer? cancelToken, UploadCallbacks callbacks = const UploadCallbacks(), }) async { if (localAssets.isEmpty) { @@ -141,7 +141,7 @@ class ForegroundUploadService { await _executeWithWorkerPool( items: localAssets, cancelToken: cancelToken, - processItem: (asset) => _uploadSingleAsset(asset, cancelToken, callbacks: callbacks), + processItem: (asset) => _uploadSingleAsset(asset, cancelToken: cancelToken, callbacks: callbacks), ); } @@ -232,12 +232,12 @@ class ForegroundUploadService { } Future _uploadSingleAsset( - LocalAsset asset, - Completer? cancelToken, { + LocalAsset asset, { + required Completer? cancelToken, required UploadCallbacks callbacks, }) async { final UploadCallbacks(:onProgress, :onSuccess, :onError, :onICloudProgress) = callbacks; - File? file; + File? assetFile; File? livePhotoFile; try { @@ -250,9 +250,10 @@ class ForegroundUploadService { return; } + File? file; if (entity.isLivePhoto) { - final liveFile = await _storageRepository.getMotionFileForAsset(asset.id, onProgress: onICloudProgress); - if (liveFile == null) { + file = await _storageRepository.getMotionFile(asset.id, cancelToken: cancelToken, onProgress: onICloudProgress); + if (file == null) { _logger.warning("Failed to obtain motion part of the livePhoto - ${asset.name}"); onError?.call( asset.localId!, @@ -260,11 +261,11 @@ class ForegroundUploadService { ); return; } - livePhotoFile = liveFile; + livePhotoFile = file; } - final assetFile = await _storageRepository.getFileForAsset(asset.id, onProgress: onICloudProgress); - if (assetFile == null) { + file = await _storageRepository.getAssetFile(asset.id, cancelToken: cancelToken, onProgress: onICloudProgress); + if (file == null) { _logger.warning("Failed to get file ${asset.id} - ${asset.name}"); onError?.call( asset.localId!, @@ -272,7 +273,7 @@ class ForegroundUploadService { ); return; } - file = assetFile; + assetFile = file; String fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name; @@ -368,10 +369,10 @@ class ForegroundUploadService { } finally { if (Platform.isIOS) { unawaited( - Future.wait([if (file != null) file.delete(), if (livePhotoFile != null) livePhotoFile.delete()]).onError(( - error, - stackTrace, - ) { + Future.wait([ + if (assetFile != null) assetFile.delete(), + if (livePhotoFile != null) livePhotoFile.delete(), + ]).onError((error, stackTrace) { _logger.severe("Post-upload file cleanup failed", error, stackTrace); return const []; }), diff --git a/mobile/test/services/background_upload.service_test.dart b/mobile/test/services/background_upload.service_test.dart index dd19f2b1cc9b..dcda387836ea 100644 --- a/mobile/test/services/background_upload.service_test.dart +++ b/mobile/test/services/background_upload.service_test.dart @@ -75,7 +75,7 @@ void main() { when(() => mockEntity.isLivePhoto).thenReturn(false); when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity); - when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile); + when(() => mockStorageRepository.getAssetFile(asset.id)).thenAnswer((_) async => mockFile); when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'OriginalPhoto.jpg'); final task = await sut.getUploadTask(asset); @@ -92,7 +92,7 @@ void main() { when(() => mockEntity.isLivePhoto).thenReturn(false); when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity); - when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile); + when(() => mockStorageRepository.getAssetFile(asset.id)).thenAnswer((_) async => mockFile); when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => null); final task = await sut.getUploadTask(asset); @@ -109,7 +109,7 @@ void main() { when(() => mockEntity.isLivePhoto).thenReturn(true); when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity); - when(() => mockStorageRepository.getMotionFileForAsset(asset)).thenAnswer((_) async => mockFile); + when(() => mockStorageRepository.getMotionFile(asset.id)).thenAnswer((_) async => mockFile); when( () => mockAssetMediaRepository.getOriginalFilename(asset.id), ).thenAnswer((_) async => 'OriginalLivePhoto.HEIC'); @@ -130,7 +130,7 @@ void main() { when(() => mockEntity.isLivePhoto).thenReturn(true); when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity); - when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile); + when(() => mockStorageRepository.getAssetFile(asset.id)).thenAnswer((_) async => mockFile); when( () => mockAssetMediaRepository.getOriginalFilename(asset.id), ).thenAnswer((_) async => 'OriginalLivePhoto.HEIC'); @@ -150,7 +150,7 @@ void main() { when(() => mockEntity.isLivePhoto).thenReturn(true); when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity); - when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile); + when(() => mockStorageRepository.getAssetFile(asset.id)).thenAnswer((_) async => mockFile); when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => null); final task = await sut.getLivePhotoUploadTask(asset, 'video-id-456'); @@ -194,7 +194,7 @@ void main() { when(() => mockEntity.isLivePhoto).thenReturn(false); when(() => mockStorageRepository.getAssetEntityForAsset(assetWithCloudId)).thenAnswer((_) async => mockEntity); - when(() => mockStorageRepository.getFileForAsset(assetWithCloudId.id)).thenAnswer((_) async => mockFile); + when(() => mockStorageRepository.getAssetFile(assetWithCloudId.id)).thenAnswer((_) async => mockFile); when(() => mockAssetMediaRepository.getOriginalFilename(assetWithCloudId.id)).thenAnswer((_) async => 'test.jpg'); final task = await sutWithV24.getUploadTask(assetWithCloudId); @@ -243,7 +243,7 @@ void main() { when(() => mockEntity.isLivePhoto).thenReturn(false); when(() => mockStorageRepository.getAssetEntityForAsset(assetWithCloudId)).thenAnswer((_) async => mockEntity); - when(() => mockStorageRepository.getFileForAsset(assetWithCloudId.id)).thenAnswer((_) async => mockFile); + when(() => mockStorageRepository.getAssetFile(assetWithCloudId.id)).thenAnswer((_) async => mockFile); when(() => mockAssetMediaRepository.getOriginalFilename(assetWithCloudId.id)).thenAnswer((_) async => 'test.jpg'); final task = await sutAndroid.getUploadTask(assetWithCloudId); @@ -281,7 +281,7 @@ void main() { when(() => mockEntity.isLivePhoto).thenReturn(false); when(() => mockStorageRepository.getAssetEntityForAsset(assetWithoutCloudId)).thenAnswer((_) async => mockEntity); - when(() => mockStorageRepository.getFileForAsset(assetWithoutCloudId.id)).thenAnswer((_) async => mockFile); + when(() => mockStorageRepository.getAssetFile(assetWithoutCloudId.id)).thenAnswer((_) async => mockFile); when( () => mockAssetMediaRepository.getOriginalFilename(assetWithoutCloudId.id), ).thenAnswer((_) async => 'test.jpg'); @@ -323,7 +323,7 @@ void main() { when(() => mockEntity.isLivePhoto).thenReturn(true); when(() => mockStorageRepository.getAssetEntityForAsset(assetWithCloudId)).thenAnswer((_) async => mockEntity); - when(() => mockStorageRepository.getFileForAsset(assetWithCloudId.id)).thenAnswer((_) async => mockFile); + when(() => mockStorageRepository.getAssetFile(assetWithCloudId.id)).thenAnswer((_) async => mockFile); when( () => mockAssetMediaRepository.getOriginalFilename(assetWithCloudId.id), ).thenAnswer((_) async => 'livephoto.heic');