diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 906433adff..227fa39920 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -23,6 +23,8 @@ PODS: - Flutter - FMDB (>= 2.7.5) - Toast (4.0.0) + - url_launcher_ios (0.0.1): + - Flutter - video_player_avfoundation (0.0.1): - Flutter - wakelock (0.0.1): @@ -37,6 +39,7 @@ DEPENDENCIES: - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) - photo_manager (from `.symlinks/plugins/photo_manager/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`) - wakelock (from `.symlinks/plugins/wakelock/ios`) @@ -63,6 +66,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/photo_manager/ios" sqflite: :path: ".symlinks/plugins/sqflite/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" video_player_avfoundation: :path: ".symlinks/plugins/video_player_avfoundation/ios" wakelock: @@ -80,6 +85,7 @@ SPEC CHECKSUMS: SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 + url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f diff --git a/mobile/lib/modules/backup/models/backup_state.model.dart b/mobile/lib/modules/backup/models/backup_state.model.dart index a1bc20c01a..f24b2f408f 100644 --- a/mobile/lib/modules/backup/models/backup_state.model.dart +++ b/mobile/lib/modules/backup/models/backup_state.model.dart @@ -1,4 +1,4 @@ -import 'package:dio/dio.dart'; +import 'package:cancellation_token_http/http.dart'; import 'package:equatable/equatable.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -12,7 +12,7 @@ class BackUpState extends Equatable { final BackUpProgressEnum backupProgress; final List allAssetOnDatabase; final double progressInPercentage; - final CancelToken cancelToken; + final CancellationToken cancelToken; final ServerInfo serverInfo; /// All available albums on the device @@ -43,7 +43,7 @@ class BackUpState extends Equatable { BackUpProgressEnum? backupProgress, List? allAssetOnDatabase, double? progressInPercentage, - CancelToken? cancelToken, + CancellationToken? cancelToken, ServerInfo? serverInfo, List? availableAlbums, Set? selectedBackupAlbums, diff --git a/mobile/lib/modules/backup/providers/backup.provider.dart b/mobile/lib/modules/backup/providers/backup.provider.dart index f71e248d73..639c2138d0 100644 --- a/mobile/lib/modules/backup/providers/backup.provider.dart +++ b/mobile/lib/modules/backup/providers/backup.provider.dart @@ -1,3 +1,4 @@ +import 'package:cancellation_token_http/http.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:hive_flutter/hive_flutter.dart'; @@ -19,7 +20,7 @@ class BackupNotifier extends StateNotifier { backupProgress: BackUpProgressEnum.idle, allAssetOnDatabase: const [], progressInPercentage: 0, - cancelToken: CancelToken(), + cancelToken: CancellationToken(), serverInfo: ServerInfo( diskAvailable: "0", diskAvailableRaw: 0, @@ -266,7 +267,7 @@ class BackupNotifier extends StateNotifier { } // Perform Backup - state = state.copyWith(cancelToken: CancelToken()); + state = state.copyWith(cancelToken: CancellationToken()); _backupService.backupAsset(assetsWillBeBackup, state.cancelToken, _onAssetUploaded, _onUploadProgress); } else { PhotoManager.openSetting(); @@ -274,7 +275,7 @@ class BackupNotifier extends StateNotifier { } void cancelBackup() { - state.cancelToken.cancel('Cancel Backup'); + state.cancelToken.cancel(); state = state.copyWith(backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0); } diff --git a/mobile/lib/modules/backup/services/backup.service.dart b/mobile/lib/modules/backup/services/backup.service.dart index c542efaa32..5d2f50fca7 100644 --- a/mobile/lib/modules/backup/services/backup.service.dart +++ b/mobile/lib/modules/backup/services/backup.service.dart @@ -8,11 +8,11 @@ import 'package:hive/hive.dart'; import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/shared/services/network.service.dart'; import 'package:immich_mobile/shared/models/device_info.model.dart'; -import 'package:immich_mobile/utils/dio_http_interceptor.dart'; import 'package:immich_mobile/utils/files_helper.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:http_parser/http_parser.dart'; import 'package:path/path.dart' as p; +import 'package:cancellation_token_http/http.dart' as http; class BackupService { final NetworkService _networkService = NetworkService(); @@ -26,17 +26,13 @@ class BackupService { return result.cast(); } - backupAsset(Set assetList, CancelToken cancelToken, Function(String, String) singleAssetDoneCb, - Function(int, int) uploadProgress) async { - var dio = Dio(); - dio.interceptors.add(AuthenticatedRequestInterceptor()); - + backupAsset(Set assetList, http.CancellationToken cancelToken, + Function(String, String) singleAssetDoneCb, Function(int, int) uploadProgress) async { String deviceId = Hive.box(userInfoBox).get(deviceIdKey); String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey); File? file; - MultipartFile assetRawUploadData; - MultipartFile thumbnailUploadData; + http.MultipartFile? thumbnailUploadData; for (var entity in assetList) { try { @@ -47,35 +43,27 @@ class BackupService { } if (file != null) { - FormData formData; String originalFileName = await entity.titleAsync; String fileNameWithoutPath = originalFileName.toString().split(".")[0]; var fileExtension = p.extension(file.path); var mimeType = FileHelper.getMimeType(file.path); - assetRawUploadData = await MultipartFile.fromFile( - file.path, + var fileStream = file.openRead(); + var assetRawUploadData = http.MultipartFile( + "assetData", + fileStream, + file.lengthSync(), filename: fileNameWithoutPath, contentType: MediaType( mimeType["type"], mimeType["subType"], ), ); - formData = FormData.fromMap({ - 'deviceAssetId': entity.id, - 'deviceId': deviceId, - 'assetType': _getAssetType(entity.type), - 'createdAt': entity.createDateTime.toIso8601String(), - 'modifiedAt': entity.modifiedDateTime.toIso8601String(), - 'isFavorite': entity.isFavorite, - 'fileExtension': fileExtension, - 'duration': entity.videoDuration, - 'assetData': [assetRawUploadData] - }); // Build thumbnail multipart data var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(1440, 2560)); if (thumbnailData != null) { - thumbnailUploadData = MultipartFile.fromBytes( + thumbnailUploadData = http.MultipartFile.fromBytes( + "thumbnailData", List.from(thumbnailData), filename: fileNameWithoutPath, contentType: MediaType( @@ -83,39 +71,37 @@ class BackupService { "jpeg", ), ); - - // Send thumbnail data if it is exist - formData = FormData.fromMap({ - 'deviceAssetId': entity.id, - 'deviceId': deviceId, - 'assetType': _getAssetType(entity.type), - 'createdAt': entity.createDateTime.toIso8601String(), - 'modifiedAt': entity.modifiedDateTime.toIso8601String(), - 'isFavorite': entity.isFavorite, - 'fileExtension': fileExtension, - 'duration': entity.videoDuration, - 'thumbnailData': [thumbnailUploadData], - 'assetData': [assetRawUploadData] - }); } - Response res = await dio.post( - '$savedEndpoint/asset/upload', - data: formData, - cancelToken: cancelToken, - onSendProgress: (sent, total) => uploadProgress(sent, total), - ); + var box = Hive.box(userInfoBox); + + var req = MultipartRequest('POST', Uri.parse('$savedEndpoint/asset/upload'), + onProgress: ((bytes, totalBytes) => uploadProgress(bytes, totalBytes))); + req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}"; + + req.fields['deviceAssetId'] = entity.id; + req.fields['deviceId'] = deviceId; + req.fields['assetType'] = _getAssetType(entity.type); + req.fields['createdAt'] = entity.createDateTime.toIso8601String(); + req.fields['modifiedAt'] = entity.modifiedDateTime.toIso8601String(); + req.fields['isFavorite'] = entity.isFavorite.toString(); + req.fields['fileExtension'] = fileExtension; + req.fields['duration'] = entity.videoDuration.toString(); + + if (thumbnailUploadData != null) { + req.files.add(thumbnailUploadData); + } + req.files.add(assetRawUploadData); + + var res = await req.send(cancellationToken: cancelToken); if (res.statusCode == 201) { singleAssetDoneCb(entity.id, deviceId); } } - } on DioError catch (e) { - debugPrint("DioError backupAsset: ${e.response}"); - if (e.type == DioErrorType.cancel || e.type == DioErrorType.other) { - return; - } - continue; + } on http.CancelledException { + debugPrint("Backup was cancelled by the user"); + return; } catch (e) { debugPrint("ERROR backupAsset: ${e.toString()}"); continue; @@ -150,3 +136,35 @@ class BackupService { return DeviceInfoRemote.fromJson(res.toString()); } } + +class MultipartRequest extends http.MultipartRequest { + /// Creates a new [MultipartRequest]. + MultipartRequest( + String method, + Uri url, { + required this.onProgress, + }) : super(method, url); + + final void Function(int bytes, int totalBytes) onProgress; + + /// Freezes all mutable fields and returns a + /// single-subscription [http.ByteStream] + /// that will emit the request body. + @override + http.ByteStream finalize() { + final byteStream = super.finalize(); + + final total = contentLength; + var bytes = 0; + + final t = StreamTransformer.fromHandlers( + handleData: (List data, EventSink> sink) { + bytes += data.length; + onProgress.call(bytes, total); + sink.add(data); + }, + ); + final stream = byteStream.transform(t); + return http.ByteStream(stream); + } +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index a39306df51..d06aedafea 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -141,6 +141,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" + cancellation_token: + dependency: transitive + description: + name: cancellation_token + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + cancellation_token_http: + dependency: "direct main" + description: + name: cancellation_token_http + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" characters: dependency: transitive description: @@ -437,7 +451,7 @@ packages: source: hosted version: "0.15.0" http: - dependency: transitive + dependency: "direct main" description: name: http url: "https://pub.dartlang.org" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 62922e7fe3..bb3e19c5ba 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -40,6 +40,8 @@ dependencies: equatable: ^2.0.3 image_picker: ^0.8.5+3 url_launcher: ^6.1.3 + http: 0.13.4 + cancellation_token_http: ^1.1.0 dev_dependencies: flutter_test: