dev: switch from pub cancellation_token_http to background_downloader to use URLSessionTasks on iOS for background sync

This commit is contained in:
Sebastian Wilke 2024-09-26 18:27:17 +02:00
parent 67df25cc9e
commit 7135f986b7
5 changed files with 79 additions and 59 deletions

View File

@ -13,6 +13,7 @@ struct BackgroundSyncAppShortcut: AppShortcutsProvider {
@AppShortcutsBuilder static var appShortcuts: [AppShortcut] { @AppShortcutsBuilder static var appShortcuts: [AppShortcut] {
AppShortcut(intent: BackgroundSyncShortcutIntent(), phrases: [ AppShortcut(intent: BackgroundSyncShortcutIntent(), phrases: [
// TODO: localized title
"Upload gallery using \(.applicationName)"], systemImageName: "square.and.arrow.up.on.square") "Upload gallery using \(.applicationName)"], systemImageName: "square.and.arrow.up.on.square")
} }

View File

@ -11,8 +11,13 @@ import SwiftUI
@available(iOS 16.0, *) @available(iOS 16.0, *)
struct BackgroundSyncShortcutIntent: AppIntent { struct BackgroundSyncShortcutIntent: AppIntent {
// TODO: localized title and description
static var title: LocalizedStringResource = "Sync gallery" static var title: LocalizedStringResource = "Sync gallery"
static var openAppWhenRun: Bool = true
static var isDiscoverable: Bool = true
func perform() async throws -> some IntentResult { func perform() async throws -> some IntentResult {
let backgroundWorker = BackgroundSyncWorker { _ in () } let backgroundWorker = BackgroundSyncWorker { _ in () }

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:cancellation_token_http/http.dart' as http; import 'package:cancellation_token_http/http.dart' as http;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -339,31 +340,29 @@ class BackupService {
} }
} }
final fileStream = file.openRead(); final fileLength = file.lengthSync();
final assetRawUploadData = http.MultipartFile(
"assetData", final (baseDir, dir, _) = await Task.split(file: file);
fileStream,
file.lengthSync(), final backgroundRequest = UploadTask(
filename: originalFileName, 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( backgroundRequest.headers.addAll(ApiService.getRequestHeaders());
'POST', backgroundRequest.headers["Transfer-Encoding"] = "chunked";
Uri.parse('$savedEndpoint/assets'), backgroundRequest.fields['deviceAssetId'] = asset.localId!;
onProgress: ((bytes, totalBytes) => onProgress(bytes, totalBytes)), backgroundRequest.fields['deviceId'] = deviceId;
); backgroundRequest.fields['fileCreatedAt'] =
baseRequest.headers.addAll(ApiService.getRequestHeaders());
baseRequest.headers["Transfer-Encoding"] = "chunked";
baseRequest.fields['deviceAssetId'] = asset.localId!;
baseRequest.fields['deviceId'] = deviceId;
baseRequest.fields['fileCreatedAt'] =
asset.fileCreatedAt.toUtc().toIso8601String(); asset.fileCreatedAt.toUtc().toIso8601String();
baseRequest.fields['fileModifiedAt'] = backgroundRequest.fields['fileModifiedAt'] =
asset.fileModifiedAt.toUtc().toIso8601String(); asset.fileModifiedAt.toUtc().toIso8601String();
baseRequest.fields['isFavorite'] = asset.isFavorite.toString(); backgroundRequest.fields['isFavorite'] = asset.isFavorite.toString();
baseRequest.fields['duration'] = asset.duration.toString(); backgroundRequest.fields['duration'] = asset.duration.toString();
baseRequest.files.add(assetRawUploadData);
onCurrentAsset( onCurrentAsset(
CurrentUploadAsset( CurrentUploadAsset(
@ -383,29 +382,37 @@ class BackupService {
livePhotoVideoId = await uploadLivePhotoVideo( livePhotoVideoId = await uploadLivePhotoVideo(
originalFileName, originalFileName,
livePhotoFile, livePhotoFile,
baseRequest, backgroundRequest,
cancelToken, cancelToken,
); );
} }
if (livePhotoVideoId != null) { if (livePhotoVideoId != null) {
baseRequest.fields['livePhotoVideoId'] = livePhotoVideoId; backgroundRequest.fields['livePhotoVideoId'] = livePhotoVideoId;
} }
final response = await httpClient.send( final response = await FileDownloader().upload(
baseRequest, backgroundRequest,
cancellationToken: cancelToken, onProgress: (percentage) => {
// onProgress returns a double in [0.0;1.0] for percentage
if (percentage > 0)
onProgress(
(percentage * fileLength).toInt(),
fileLength,
),
},
); );
final responseBody = final responseBody = jsonDecode(response.responseBody ?? "{}");
jsonDecode(await response.stream.bytesToString());
if (![200, 201].contains(response.statusCode)) { if (response.status == TaskStatus.failed ||
final error = responseBody; ![200, 201].contains(response.responseStatusCode)) {
final errorMessage = error['message'] ?? error['error']; final error = response.exception != null
? response.exception!.description
: responseBody;
debugPrint( 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( onError(
@ -415,11 +422,11 @@ class BackupService {
fileCreatedAt: asset.fileCreatedAt, fileCreatedAt: asset.fileCreatedAt,
fileName: originalFileName, fileName: originalFileName,
fileType: _getAssetType(candidate.asset.type), fileType: _getAssetType(candidate.asset.type),
errorMessage: errorMessage, errorMessage: error,
), ),
); );
if (errorMessage == "Quota has been exceeded!") { if (error == "Quota has been exceeded!") {
anyErrors = true; anyErrors = true;
break; break;
} }
@ -428,7 +435,7 @@ class BackupService {
} }
bool isDuplicate = false; bool isDuplicate = false;
if (response.statusCode == 200) { if (response.responseStatusCode == 200) {
isDuplicate = true; isDuplicate = true;
duplicatedAssetIds.add(asset.localId!); duplicatedAssetIds.add(asset.localId!);
} }
@ -478,7 +485,7 @@ class BackupService {
Future<String?> uploadLivePhotoVideo( Future<String?> uploadLivePhotoVideo(
String originalFileName, String originalFileName,
File? livePhotoVideoFile, File? livePhotoVideoFile,
MultipartRequest baseRequest, UploadTask baseRequest,
http.CancellationToken cancelToken, http.CancellationToken cancelToken,
) async { ) async {
if (livePhotoVideoFile == null) { if (livePhotoVideoFile == null) {
@ -488,35 +495,33 @@ class BackupService {
originalFileName, originalFileName,
p.extension(livePhotoVideoFile.path), p.extension(livePhotoVideoFile.path),
); );
final fileStream = livePhotoVideoFile.openRead();
final livePhotoRawUploadData = http.MultipartFile( final (baseDir, dir, _) = await Task.split(file: livePhotoVideoFile);
"assetData",
fileStream, final backgroundRequest = UploadTask(
livePhotoVideoFile.lengthSync(),
filename: livePhotoTitle, filename: livePhotoTitle,
); baseDirectory: baseDir,
final livePhotoReq = MultipartRequest( directory: dir,
baseRequest.method, url: baseRequest.url,
baseRequest.url, httpRequestMethod: baseRequest.httpRequestMethod,
onProgress: baseRequest.onProgress, priority: baseRequest.priority,
) )
..headers.addAll(baseRequest.headers) ..headers.addAll(baseRequest.headers)
..fields.addAll(baseRequest.fields); ..fields.addAll(baseRequest.fields);
livePhotoReq.files.add(livePhotoRawUploadData); final response = await FileDownloader()
.upload(backgroundRequest); //TODO: onProgress callback?
var response = await httpClient.send( var responseBody = jsonDecode(response.responseBody ?? "{}");
livePhotoReq,
cancellationToken: cancelToken,
);
var responseBody = jsonDecode(await response.stream.bytesToString()); if (response.status == TaskStatus.failed ||
![200, 201].contains(response.responseStatusCode)) {
if (![200, 201].contains(response.statusCode)) { final error = response.exception != null
var error = responseBody; ? response.exception!.description
: responseBody;
debugPrint( debugPrint(
"Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | ${error['error']}", "Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | $error",
); );
} }

View File

@ -78,6 +78,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.0.0" 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: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -744,10 +752,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: http name: http
sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.6" version: "1.2.2"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:

View File

@ -32,7 +32,7 @@ dependencies:
flutter_svg: ^2.0.9 flutter_svg: ^2.0.9
package_info_plus: ^8.0.1 package_info_plus: ^8.0.1
url_launcher: ^6.2.4 url_launcher: ^6.2.4
http: ^0.13.6 http: ^1.1.0
cancellation_token_http: ^2.0.0 cancellation_token_http: ^2.0.0
easy_localization: ^3.0.3 easy_localization: ^3.0.3
share_plus: ^10.0.0 share_plus: ^10.0.0
@ -56,6 +56,7 @@ dependencies:
thumbhash: 0.1.0+1 thumbhash: 0.1.0+1
async: ^2.11.0 async: ^2.11.0
dynamic_color: ^1.7.0 #package to apply system theme dynamic_color: ^1.7.0 #package to apply system theme
background_downloader: ^8.5.5
#image editing packages #image editing packages
crop_image: ^1.0.13 crop_image: ^1.0.13