From 7135f986b72816f7a066d002aa7b0d9dac1f12d4 Mon Sep 17 00:00:00 2001 From: Sebastian Wilke Date: Thu, 26 Sep 2024 18:27:17 +0200 Subject: [PATCH] dev: switch from pub cancellation_token_http to background_downloader to use URLSessionTasks on iOS for background sync --- .../BackgroundSyncAppShortcut.swift | 1 + .../BackgroundSyncShortcutIntent.swift | 5 + mobile/lib/services/backup.service.dart | 117 +++++++++--------- mobile/pubspec.lock | 12 +- mobile/pubspec.yaml | 3 +- 5 files changed, 79 insertions(+), 59 deletions(-) diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundSyncAppShortcut.swift b/mobile/ios/Runner/BackgroundSync/BackgroundSyncAppShortcut.swift index a7d3e2ee7ed79..71b9f2ea451c0 100644 --- a/mobile/ios/Runner/BackgroundSync/BackgroundSyncAppShortcut.swift +++ b/mobile/ios/Runner/BackgroundSync/BackgroundSyncAppShortcut.swift @@ -13,6 +13,7 @@ struct BackgroundSyncAppShortcut: AppShortcutsProvider { @AppShortcutsBuilder static var appShortcuts: [AppShortcut] { AppShortcut(intent: BackgroundSyncShortcutIntent(), phrases: [ + // TODO: localized title "Upload gallery using \(.applicationName)"], systemImageName: "square.and.arrow.up.on.square") } diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundSyncShortcutIntent.swift b/mobile/ios/Runner/BackgroundSync/BackgroundSyncShortcutIntent.swift index 3480344d511e2..97cf3d31fa32d 100644 --- a/mobile/ios/Runner/BackgroundSync/BackgroundSyncShortcutIntent.swift +++ b/mobile/ios/Runner/BackgroundSync/BackgroundSyncShortcutIntent.swift @@ -11,7 +11,12 @@ import SwiftUI @available(iOS 16.0, *) struct BackgroundSyncShortcutIntent: AppIntent { + // TODO: localized title and description static var title: LocalizedStringResource = "Sync gallery" + + static var openAppWhenRun: Bool = true + + static var isDiscoverable: Bool = true func perform() async throws -> some IntentResult { diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 19d731d773d75..a7328231e7318 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:background_downloader/background_downloader.dart'; import 'package:cancellation_token_http/http.dart' as http; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -339,31 +340,29 @@ class BackupService { } } - final fileStream = file.openRead(); - final assetRawUploadData = http.MultipartFile( - "assetData", - fileStream, - file.lengthSync(), + final fileLength = file.lengthSync(); + + final (baseDir, dir, _) = await Task.split(file: file); + + final backgroundRequest = UploadTask( filename: originalFileName, - ); + baseDirectory: baseDir, + directory: dir, + url: '$savedEndpoint/assets', + httpRequestMethod: 'POST', + priority: 10, + ); // Priority 10 for testing purposes; maybe we could change that to a lower value later - final baseRequest = MultipartRequest( - 'POST', - Uri.parse('$savedEndpoint/assets'), - onProgress: ((bytes, totalBytes) => onProgress(bytes, totalBytes)), - ); - - baseRequest.headers.addAll(ApiService.getRequestHeaders()); - baseRequest.headers["Transfer-Encoding"] = "chunked"; - baseRequest.fields['deviceAssetId'] = asset.localId!; - baseRequest.fields['deviceId'] = deviceId; - baseRequest.fields['fileCreatedAt'] = + backgroundRequest.headers.addAll(ApiService.getRequestHeaders()); + backgroundRequest.headers["Transfer-Encoding"] = "chunked"; + backgroundRequest.fields['deviceAssetId'] = asset.localId!; + backgroundRequest.fields['deviceId'] = deviceId; + backgroundRequest.fields['fileCreatedAt'] = asset.fileCreatedAt.toUtc().toIso8601String(); - baseRequest.fields['fileModifiedAt'] = + backgroundRequest.fields['fileModifiedAt'] = asset.fileModifiedAt.toUtc().toIso8601String(); - baseRequest.fields['isFavorite'] = asset.isFavorite.toString(); - baseRequest.fields['duration'] = asset.duration.toString(); - baseRequest.files.add(assetRawUploadData); + backgroundRequest.fields['isFavorite'] = asset.isFavorite.toString(); + backgroundRequest.fields['duration'] = asset.duration.toString(); onCurrentAsset( CurrentUploadAsset( @@ -383,29 +382,37 @@ class BackupService { livePhotoVideoId = await uploadLivePhotoVideo( originalFileName, livePhotoFile, - baseRequest, + backgroundRequest, cancelToken, ); } if (livePhotoVideoId != null) { - baseRequest.fields['livePhotoVideoId'] = livePhotoVideoId; + backgroundRequest.fields['livePhotoVideoId'] = livePhotoVideoId; } - final response = await httpClient.send( - baseRequest, - cancellationToken: cancelToken, + final response = await FileDownloader().upload( + backgroundRequest, + onProgress: (percentage) => { + // onProgress returns a double in [0.0;1.0] for percentage + if (percentage > 0) + onProgress( + (percentage * fileLength).toInt(), + fileLength, + ), + }, ); - final responseBody = - jsonDecode(await response.stream.bytesToString()); + final responseBody = jsonDecode(response.responseBody ?? "{}"); - if (![200, 201].contains(response.statusCode)) { - final error = responseBody; - final errorMessage = error['message'] ?? error['error']; + if (response.status == TaskStatus.failed || + ![200, 201].contains(response.responseStatusCode)) { + final error = response.exception != null + ? response.exception!.description + : responseBody; debugPrint( - "Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}", + "Error(${response.responseStatusCode}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | $error", ); onError( @@ -415,11 +422,11 @@ class BackupService { fileCreatedAt: asset.fileCreatedAt, fileName: originalFileName, fileType: _getAssetType(candidate.asset.type), - errorMessage: errorMessage, + errorMessage: error, ), ); - if (errorMessage == "Quota has been exceeded!") { + if (error == "Quota has been exceeded!") { anyErrors = true; break; } @@ -428,7 +435,7 @@ class BackupService { } bool isDuplicate = false; - if (response.statusCode == 200) { + if (response.responseStatusCode == 200) { isDuplicate = true; duplicatedAssetIds.add(asset.localId!); } @@ -478,7 +485,7 @@ class BackupService { Future uploadLivePhotoVideo( String originalFileName, File? livePhotoVideoFile, - MultipartRequest baseRequest, + UploadTask baseRequest, http.CancellationToken cancelToken, ) async { if (livePhotoVideoFile == null) { @@ -488,35 +495,33 @@ class BackupService { originalFileName, p.extension(livePhotoVideoFile.path), ); - final fileStream = livePhotoVideoFile.openRead(); - final livePhotoRawUploadData = http.MultipartFile( - "assetData", - fileStream, - livePhotoVideoFile.lengthSync(), + + final (baseDir, dir, _) = await Task.split(file: livePhotoVideoFile); + + final backgroundRequest = UploadTask( filename: livePhotoTitle, - ); - final livePhotoReq = MultipartRequest( - baseRequest.method, - baseRequest.url, - onProgress: baseRequest.onProgress, + baseDirectory: baseDir, + directory: dir, + url: baseRequest.url, + httpRequestMethod: baseRequest.httpRequestMethod, + priority: baseRequest.priority, ) ..headers.addAll(baseRequest.headers) ..fields.addAll(baseRequest.fields); - livePhotoReq.files.add(livePhotoRawUploadData); + final response = await FileDownloader() + .upload(backgroundRequest); //TODO: onProgress callback? - var response = await httpClient.send( - livePhotoReq, - cancellationToken: cancelToken, - ); + var responseBody = jsonDecode(response.responseBody ?? "{}"); - var responseBody = jsonDecode(await response.stream.bytesToString()); - - if (![200, 201].contains(response.statusCode)) { - var error = responseBody; + if (response.status == TaskStatus.failed || + ![200, 201].contains(response.responseStatusCode)) { + final error = response.exception != null + ? response.exception!.description + : responseBody; debugPrint( - "Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | ${error['error']}", + "Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | $error", ); } diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index aaea00d699bbe..9dadbd1028a64 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -78,6 +78,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.0.0" + background_downloader: + dependency: "direct main" + description: + name: background_downloader + sha256: "6a945db1a1c7727a4bc9c1d7c882cfb1a819f873b77e01d5e5dd6a3fb231cb28" + url: "https://pub.dev" + source: hosted + version: "8.5.5" boolean_selector: dependency: transitive description: @@ -744,10 +752,10 @@ packages: dependency: "direct main" description: name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "0.13.6" + version: "1.2.2" http_multi_server: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 0f75463547d6b..a66453a5fcd93 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -32,7 +32,7 @@ dependencies: flutter_svg: ^2.0.9 package_info_plus: ^8.0.1 url_launcher: ^6.2.4 - http: ^0.13.6 + http: ^1.1.0 cancellation_token_http: ^2.0.0 easy_localization: ^3.0.3 share_plus: ^10.0.0 @@ -56,6 +56,7 @@ dependencies: thumbhash: 0.1.0+1 async: ^2.11.0 dynamic_color: ^1.7.0 #package to apply system theme + background_downloader: ^8.5.5 #image editing packages crop_image: ^1.0.13