From f1dc6dff54094ea316bb729dc2c5f69f376f831d Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 5 Jul 2025 11:48:12 +0200 Subject: [PATCH] add fast upload and download --- lib/src/services/api.service.dart | 2 +- lib/src/services/api/media_download.dart | 71 ++++++++--- lib/src/services/api/media_upload.dart | 116 +++++++++++------- .../create_backup.twonly_safe.dart | 5 +- pubspec.yaml | 2 +- 5 files changed, 137 insertions(+), 59 deletions(-) diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 9d9c90a..2030357 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -45,7 +45,7 @@ final lockRetransStore = Mutex(); /// It handles errors and does automatically tries to reconnect on /// errors or network changes. class ApiService { - final String apiHost = (kDebugMode) ? "10.99.0.140:3030" : "api.twonly.eu"; + final String apiHost = (kDebugMode) ? "192.168.178.89:3030" : "api.twonly.eu"; final String apiSecure = (kDebugMode) ? "" : "s"; bool appIsOutdated = false; diff --git a/lib/src/services/api/media_download.dart b/lib/src/services/api/media_download.dart index 0048864..bcade48 100644 --- a/lib/src/services/api/media_download.dart +++ b/lib/src/services/api/media_download.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:drift/drift.dart'; +import 'package:http/http.dart' as http; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:twonly/globals.dart'; @@ -75,30 +76,39 @@ Future isAllowedToDownload(bool isVideo) async { } Future handleDownloadStatusUpdate(TaskStatusUpdate update) async { - bool failed = false; int messageId = int.parse(update.task.taskId.replaceAll("download_", "")); + bool failed = false; if (update.status == TaskStatus.failed || update.status == TaskStatus.canceled) { - Log.error("Download failed: ${update.status}"); failed = true; } else if (update.status == TaskStatus.complete) { if (update.responseStatusCode == 200) { - Log.info("Download was successfully for $messageId"); - await handleEncryptedFile(messageId); + failed = false; } else { + failed = true; Log.error( "Got invalid response status code: ${update.responseStatusCode}"); } + } else { + Log.info("Got $update for $messageId"); + return; } + await handleDownloadStatusUpdateInternal(messageId, failed); +} +Future handleDownloadStatusUpdateInternal(int messageId, bool failed) async { if (failed) { + Log.error("Download failed for $messageId"); Message? message = await twonlyDB.messagesDao .getMessageByMessageId(messageId) .getSingleOrNull(); if (message != null) { await handleMediaError(message); } + } else { + Log.info("Download was successfully for $messageId"); + await handleEncryptedFile(messageId); } } @@ -151,11 +161,6 @@ Future startDownloadMedia(Message message, bool force, ); } - // int offset = 0; - // Uint8List? bytes = await readMediaFile(media.messageId, "encrypted"); - // if (bytes != null && bytes.isNotEmpty) { - // offset = bytes.length; - downloadStartedForMediaReceived[message.messageId] = DateTime.now(); String downloadToken = uint8ListToHex(content.downloadToken!); @@ -175,13 +180,51 @@ Future startDownloadMedia(Message message, bool force, ); Log.info( - "Got media file. Starting download: ${downloadToken.substring(0, 10)}"); + "Got media file. Starting download: ${downloadToken.substring(0, 10)}", + ); - final result = await FileDownloader().enqueue(task); - - return result; + try { + await downloadFileFast(media.messageId, apiUrl); + return; + } catch (e) { + Log.error("Fast download failed: $e"); + final result = await FileDownloader().enqueue(task); + return result; + } } catch (e) { - Log.error("Exception during upload: $e"); + Log.error("Exception during download: $e"); + } +} + +Future downloadFileFast( + int messageId, + String apiUrl, +) async { + final String directoryPath = + "${(await getApplicationSupportDirectory()).path}/media/received/"; + final String filename = "$messageId.encrypted"; + + final Directory directory = Directory(directoryPath); + if (!await directory.exists()) { + await directory.create(recursive: true); + } + + final String filePath = "${directory.path}/$filename"; + + final response = + await http.get(Uri.parse(apiUrl)).timeout(Duration(seconds: 6)); + + if (response.statusCode == 200) { + await File(filePath).writeAsBytes(response.bodyBytes); + Log.info('Download successful: $filePath'); + await handleDownloadStatusUpdateInternal(messageId, false); + return; + } else { + if (response.statusCode == 404) { + await handleDownloadStatusUpdateInternal(messageId, true); + return; + } + throw Exception("Fast upload failed with status: ${response.statusCode}"); } } diff --git a/lib/src/services/api/media_upload.dart b/lib/src/services/api/media_upload.dart index 0a12bde..60b117e 100644 --- a/lib/src/services/api/media_upload.dart +++ b/lib/src/services/api/media_upload.dart @@ -9,6 +9,7 @@ import 'dart:io'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:drift/drift.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:http/http.dart' as http; import 'package:mutex/mutex.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; @@ -410,31 +411,13 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async { ); return; } - if (update.status == TaskStatus.failed || update.status == TaskStatus.canceled) { Log.error("Upload failed: ${update.status}"); failed = true; } else if (update.status == TaskStatus.complete) { if (update.responseStatusCode == 200) { - Log.info("Upload of $mediaUploadId success!"); - - await twonlyDB.mediaUploadsDao.updateMediaUpload( - mediaUploadId, - MediaUploadsCompanion( - state: Value(UploadState.receiverNotified), - ), - ); - - for (final messageId in media.messageIds!) { - await twonlyDB.messagesDao.updateMessageByMessageId( - messageId, - MessagesCompanion( - acknowledgeByServer: Value(true), - errorWhileSending: Value(false), - ), - ); - } + await handleUploadSuccess(media); return; } else if (update.responseStatusCode != null) { if (update.responseStatusCode! >= 400 && @@ -462,6 +445,26 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async { 'Status update for ${update.task.taskId} with status ${update.status}'); } +Future handleUploadSuccess(MediaUpload media) async { + Log.info("Upload of ${media.mediaUploadId} success!"); + await twonlyDB.mediaUploadsDao.updateMediaUpload( + media.mediaUploadId, + MediaUploadsCompanion( + state: Value(UploadState.receiverNotified), + ), + ); + + for (final messageId in media.messageIds!) { + await twonlyDB.messagesDao.updateMessageByMessageId( + messageId, + MessagesCompanion( + acknowledgeByServer: Value(true), + errorWhileSending: Value(false), + ), + ); + } +} + Future handleUploadError(MediaUpload mediaUpload) async { // if the messageIds are already there notify the user about this error... if (mediaUpload.messageIds != null) { @@ -573,12 +576,13 @@ Future handleMediaUpload(MediaUpload media) async { final uploadRequestBytes = uploadRequest.writeToBuffer(); - String? apiAuthToken = + String? apiAuthTokenRaw = await FlutterSecureStorage().read(key: SecureStorageKeys.apiAuthToken); - if (apiAuthToken == null) { + if (apiAuthTokenRaw == null) { Log.error("api auth token not defined."); return; } + String apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw)); File uploadRequestFile = await writeSendMediaFile( media.mediaUploadId, @@ -590,35 +594,65 @@ Future handleMediaUpload(MediaUpload media) async { "http${apiService.apiSecure}://${apiService.apiHost}/api/upload"; try { - final task = UploadTask.fromFile( - taskId: "upload_${media.mediaUploadId}", - displayName: (media.metadata?.isVideo ?? false) ? "image" : "video", - file: uploadRequestFile, - url: apiUrl, - priority: 0, - retries: 10, - headers: { - 'x-twonly-auth-token': uint8ListToHex(base64Decode(apiAuthToken)) - }, - ); - Log.info("Starting upload from ${media.mediaUploadId}"); - final result = await FileDownloader().enqueue(task); - - if (result) { - await twonlyDB.mediaUploadsDao.updateMediaUpload( - media.mediaUploadId, - MediaUploadsCompanion( - state: Value(UploadState.uploadTaskStarted), - ), + try { + await uploadFileFast(media, uploadRequestBytes, apiUrl, apiAuthToken); + } catch (e) { + Log.error("Fast upload failed: $e. Using slow method."); + final task = UploadTask.fromFile( + taskId: "upload_${media.mediaUploadId}", + displayName: (media.metadata?.isVideo ?? false) ? "image" : "video", + file: uploadRequestFile, + url: apiUrl, + priority: 0, + retries: 10, + headers: { + 'x-twonly-auth-token': apiAuthToken, + }, ); + await FileDownloader().enqueue(task); } + + await twonlyDB.mediaUploadsDao.updateMediaUpload( + media.mediaUploadId, + MediaUploadsCompanion( + state: Value(UploadState.uploadTaskStarted), + ), + ); } catch (e) { Log.error("Exception during upload: $e"); } } +Future uploadFileFast( + MediaUpload media, + Uint8List uploadRequestFile, + String apiUrl, + String apiAuthToken, +) async { + var requestMultipart = http.MultipartRequest( + "POST", + Uri.parse(apiUrl), + ); + requestMultipart.headers['x-twonly-auth-token'] = apiAuthToken; + + requestMultipart.files.add(http.MultipartFile.fromBytes( + "file", + uploadRequestFile, + filename: "upload", + )); + + final response = await requestMultipart.send().timeout(Duration(seconds: 3)); + if (response.statusCode == 200) { + Log.info('Upload successful!'); + await handleUploadSuccess(media); + return; + } else { + Log.info('Upload failed with status: ${response.statusCode}'); + } +} + Future compressVideoIfExists(int mediaUploadId) async { String basePath = await getMediaFilePath(mediaUploadId, "send"); File videoOriginalFile = File("$basePath.original.mp4"); diff --git a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart index d81b3ba..c673e69 100644 --- a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart +++ b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart @@ -97,7 +97,8 @@ Future performTwonlySafeBackup({bool force = false}) async { await backupDatabaseFile.delete(); await backupDatabaseFileCleaned.delete(); - print("twonlyDatabaseBytes = ${twonlyDatabaseBytes.lengthInBytes}"); + Log.info("twonlyDatabaseBytes = ${twonlyDatabaseBytes.lengthInBytes}"); + Log.info("secureStorageBytes = ${jsonEncode(secureStorageBackup)}"); final backupProto = TwonlySafeBackupContent( secureStorageJson: jsonEncode(secureStorageBackup), @@ -171,7 +172,7 @@ Future performTwonlySafeBackup({bool force = false}) async { file: encryptedBackupBytesFile, httpRequestMethod: "PUT", url: (await getTwonlySafeBackupUrl())!, - requiresWiFi: true, + // requiresWiFi: true, priority: 5, post: 'binary', retries: 2, diff --git a/pubspec.yaml b/pubspec.yaml index b0c3cef..f193c91 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec # Prevent accidental publishing to pub.dev. publish_to: 'none' -version: 0.0.47+47 +version: 0.0.48+48 environment: sdk: ^3.6.0