diff --git a/mobile/ios/Podfile b/mobile/ios/Podfile index a98032db20..009dc5e8d4 100644 --- a/mobile/ios/Podfile +++ b/mobile/ios/Podfile @@ -45,7 +45,7 @@ post_install do |installer| installer.generated_projects.each do |project| project.targets.each do |target| target.build_configurations.each do |config| - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.6' end end end diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 908ee84aed..17b606aef2 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -224,7 +224,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: - background_downloader: b42a56120f5348bff70e74222f0e9e6f7f1a1537 + background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c @@ -261,6 +261,6 @@ SPEC CHECKSUMS: url_launcher_ios: 694010445543906933d732453a59da0a173ae33d wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49 -PODFILE CHECKSUM: 03b7eead4ee77b9e778179eeb0f3b5513617451c +PODFILE CHECKSUM: 04655a9b6714fa7a2b4fb559982ee8bed4b3b718 COCOAPODS: 1.16.2 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 83c231d741..f1b241ab8c 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -546,7 +546,7 @@ DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -690,7 +690,7 @@ DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -720,7 +720,7 @@ DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/mobile/lib/interfaces/upload.interface.dart b/mobile/lib/interfaces/upload.interface.dart index d4b2298a14..1208fef13f 100644 --- a/mobile/lib/interfaces/upload.interface.dart +++ b/mobile/lib/interfaces/upload.interface.dart @@ -4,7 +4,7 @@ abstract interface class IUploadRepository { void Function(TaskStatusUpdate)? onUploadStatus; void Function(TaskProgressUpdate)? onTaskProgress; - Future upload(UploadTask task); + void enqueue(UploadTask task); Future cancel(String id); Future deleteAllTrackingRecords(); Future deleteRecordsWithIds(List id); diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index a4f4fea45c..d5910b59d7 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -1,10 +1,12 @@ import 'dart:io'; +import 'package:background_downloader/background_downloader.dart'; import 'package:cancellation_token_http/http.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; @@ -20,6 +22,7 @@ import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; +import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; @@ -30,6 +33,7 @@ import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/services/backup_album.service.dart'; import 'package:immich_mobile/services/server_info.service.dart'; +import 'package:immich_mobile/services/upload.service.dart'; import 'package:immich_mobile/utils/backup_progress.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:logging/logging.dart'; @@ -47,6 +51,7 @@ final backupProvider = ref.watch(albumMediaRepositoryProvider), ref.watch(fileMediaRepositoryProvider), ref.watch(backupAlbumServiceProvider), + ref.watch(uploadServiceProvider), ref, ); }); @@ -61,6 +66,7 @@ class BackupNotifier extends StateNotifier { this._albumMediaRepository, this._fileMediaRepository, this._backupAlbumService, + this._uploadService, this.ref, ) : super( BackUpState( @@ -100,7 +106,10 @@ class BackupNotifier extends StateNotifier { ), iCloudDownloadProgress: 0.0, ), - ); + ) { + _uploadService.onUploadStatus = _uploadStatusCallback; + _uploadService.onTaskProgress = _taskProgressCallback; + } final log = Logger('BackupNotifier'); final BackupService _backupService; @@ -111,6 +120,7 @@ class BackupNotifier extends StateNotifier { final IAlbumMediaRepository _albumMediaRepository; final IFileMediaRepository _fileMediaRepository; final BackupAlbumService _backupAlbumService; + final UploadService _uploadService; final Ref ref; /// @@ -488,7 +498,7 @@ class BackupNotifier extends StateNotifier { Future startBackupProcess() async { debugPrint("Start backup process"); assert(state.backupProgress == BackUpProgressEnum.idle); - state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress); + // state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress); await getBackupInfo(); @@ -522,21 +532,89 @@ class BackupNotifier extends StateNotifier { state = state.copyWith(iCloudDownloadProgress: progress); }); - await _backupService.backupAsset( - assetsWillBeBackup, - state.cancelToken, - pmProgressHandler: pmProgressHandler, - onSuccess: _onAssetUploaded, - onProgress: _onUploadProgress, - onCurrentAsset: _onSetCurrentBackupAsset, - onError: _onBackupError, - ); + // await _backupService.backupAsset( + // assetsWillBeBackup, + // state.cancelToken, + // pmProgressHandler: pmProgressHandler, + // onSuccess: _onAssetUploaded, + // onProgress: _onUploadProgress, + // onCurrentAsset: _onSetCurrentBackupAsset, + // onError: _onBackupError, + // ); + + await _backupService.uploadAssets(assetsWillBeBackup); await notifyBackgroundServiceCanRun(); } else { openAppSettings(); } } + void _updateUploadStatus(TaskStatusUpdate task, TaskStatus status) async { + if (status == TaskStatus.canceled) { + return; + } + + final taskId = task.task.taskId; + final uploadStatus = switch (task.status) { + TaskStatus.complete => UploadStatus.complete, + TaskStatus.failed => UploadStatus.failed, + TaskStatus.canceled => UploadStatus.canceled, + TaskStatus.enqueued => UploadStatus.enqueued, + TaskStatus.running => UploadStatus.running, + TaskStatus.paused => UploadStatus.paused, + TaskStatus.notFound => UploadStatus.notFound, + TaskStatus.waitingToRetry => UploadStatus.waitingtoRetry + }; + + // state = [ + // for (final attachment in state) + // if (attachment.id == taskId.toInt()) + // attachment.copyWith(status: uploadStatus) + // else + // attachment, + // ]; + } + + void _uploadStatusCallback(TaskStatusUpdate update) { + _updateUploadStatus(update, update.status); + + switch (update.status) { + case TaskStatus.complete: + if (update.responseStatusCode == 200) { + if (kDebugMode) { + debugPrint("[COMPLETE] ${update.task.taskId} - DUPLICATE"); + } + } else { + if (kDebugMode) { + debugPrint("[COMPLETE] ${update.task.taskId}"); + } + } + break; + + default: + break; + } + } + + void _taskProgressCallback(TaskProgressUpdate update) { + // Ignore if the task is canceled or completed + if (update.progress == downloadFailed || + update.progress == downloadCompleted) { + return; + } + + // print("[_taskProgressCallback] $update"); + + final taskId = update.task.taskId; + // state = [ + // for (final attachment in state) + // if (attachment.id == taskId.toInt()) + // attachment.copyWith(uploadProgress: update.progress) + // else + // attachment, + // ]; + } + void setAvailableAlbums(availableAlbums) { state = state.copyWith( availableAlbums: availableAlbums, diff --git a/mobile/lib/repositories/upload.repository.dart b/mobile/lib/repositories/upload.repository.dart index 6445d144f6..ce5329a5a1 100644 --- a/mobile/lib/repositories/upload.repository.dart +++ b/mobile/lib/repositories/upload.repository.dart @@ -12,7 +12,12 @@ class UploadRepository implements IUploadRepository { @override void Function(TaskProgressUpdate)? onTaskProgress; + final taskQueue = MemoryTaskQueue(); + UploadRepository() { + // taskQueue.minInterval = const Duration(milliseconds: 5); + // taskQueue.maxConcurrent = 2; + FileDownloader().addTaskQueue(taskQueue); FileDownloader().registerCallbacks( group: uploadGroup, taskStatusCallback: (update) => onUploadStatus?.call(update), @@ -21,8 +26,8 @@ class UploadRepository implements IUploadRepository { } @override - Future upload(UploadTask task) { - return FileDownloader().enqueue(task); + void enqueue(UploadTask task) { + taskQueue.add(task); } @override diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index a6468f249b..9196c86f97 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -28,6 +28,7 @@ import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/services/upload.service.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:path/path.dart' as p; @@ -43,6 +44,7 @@ final backupServiceProvider = Provider( ref.watch(fileMediaRepositoryProvider), ref.watch(assetRepositoryProvider), ref.watch(assetMediaRepositoryProvider), + ref.watch(uploadServiceProvider), ), ); @@ -56,6 +58,7 @@ class BackupService { final IFileMediaRepository _fileMediaRepository; final IAssetRepository _assetRepository; final IAssetMediaRepository _assetMediaRepository; + final UploadService _uploadService; BackupService( this._apiService, @@ -65,6 +68,7 @@ class BackupService { this._fileMediaRepository, this._assetRepository, this._assetMediaRepository, + this._uploadService, ); Future?> getDeviceBackupAsset() async { @@ -249,6 +253,42 @@ class BackupService { ); } + uploadAssets( + Iterable assets, + ) async { + final hasPermission = await _checkPermissions(); + if (!hasPermission) { + return false; + } + + List candidates = assets.toList(); + for (final candidate in candidates) { + final Asset asset = candidate.asset; + File? file; + File? livePhotoFile; + file = await asset.local!.originFile.timeout(const Duration(seconds: 5)); + + if (asset.local!.isLivePhoto) { + livePhotoFile = await asset.local!.originFileWithSubtype + .timeout(const Duration(seconds: 5)); + } + + if (file != null) { + String? originalFileName = + await _assetMediaRepository.getOriginalFilename(asset.localId!) ?? + asset.fileName; + + await _uploadService.upload( + file, + originalFileName: originalFileName, + deviceAssetId: asset.localId, + ); + } + + break; + } + } + Future backupAsset( Iterable assets, http.CancellationToken cancelToken, { diff --git a/mobile/lib/services/upload.service.dart b/mobile/lib/services/upload.service.dart index 0734e57212..e526aea068 100644 --- a/mobile/lib/services/upload.service.dart +++ b/mobile/lib/services/upload.service.dart @@ -42,19 +42,27 @@ class UploadService { return FileDownloader().cancelTaskWithId(id); } - Future upload(File file) async { + Future upload( + File file, { + Map? fields, + String? originalFileName, + String? deviceAssetId, + }) async { final task = await _buildUploadTask( - hash(file.path).toString(), + deviceAssetId ?? hash(file.path).toString(), file, + fields: fields, + originalFileName: originalFileName, ); - await _uploadRepository.upload(task); + _uploadRepository.enqueue(task); } Future _buildUploadTask( String id, File file, { Map? fields, + String? originalFileName, }) async { final serverEndpoint = Store.get(StoreKey.serverEndpoint); final url = Uri.parse('$serverEndpoint/assets').toString(); @@ -66,9 +74,8 @@ class UploadService { final stats = await file.stat(); final fileCreatedAt = stats.changed; final fileModifiedAt = stats.modified; - final fieldsMap = { - 'filename': filename, + 'filename': originalFileName ?? filename, 'deviceAssetId': id, 'deviceId': deviceId, 'fileCreatedAt': fileCreatedAt.toUtc().toIso8601String(), @@ -78,15 +85,13 @@ class UploadService { if (fields != null) ...fields, }; - return UploadTask( + return UploadTask.fromFile( + file: file, taskId: id, httpRequestMethod: 'POST', url: url, headers: headers, - filename: filename, fields: fieldsMap, - baseDirectory: baseDirectory, - directory: directory, fileField: 'assetData', group: uploadGroup, updates: Updates.statusAndProgress,