add fast upload and download

This commit is contained in:
otsmr 2025-07-05 11:48:12 +02:00
parent 341c7a36da
commit f1dc6dff54
5 changed files with 137 additions and 59 deletions

View file

@ -45,7 +45,7 @@ final lockRetransStore = Mutex();
/// It handles errors and does automatically tries to reconnect on /// It handles errors and does automatically tries to reconnect on
/// errors or network changes. /// errors or network changes.
class ApiService { 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"; final String apiSecure = (kDebugMode) ? "" : "s";
bool appIsOutdated = false; bool appIsOutdated = false;

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
@ -75,30 +76,39 @@ Future<bool> isAllowedToDownload(bool isVideo) async {
} }
Future handleDownloadStatusUpdate(TaskStatusUpdate update) async { Future handleDownloadStatusUpdate(TaskStatusUpdate update) async {
bool failed = false;
int messageId = int.parse(update.task.taskId.replaceAll("download_", "")); int messageId = int.parse(update.task.taskId.replaceAll("download_", ""));
bool failed = false;
if (update.status == TaskStatus.failed || if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled) { update.status == TaskStatus.canceled) {
Log.error("Download failed: ${update.status}");
failed = true; failed = true;
} else if (update.status == TaskStatus.complete) { } else if (update.status == TaskStatus.complete) {
if (update.responseStatusCode == 200) { if (update.responseStatusCode == 200) {
Log.info("Download was successfully for $messageId"); failed = false;
await handleEncryptedFile(messageId);
} else { } else {
failed = true;
Log.error( Log.error(
"Got invalid response status code: ${update.responseStatusCode}"); "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) { if (failed) {
Log.error("Download failed for $messageId");
Message? message = await twonlyDB.messagesDao Message? message = await twonlyDB.messagesDao
.getMessageByMessageId(messageId) .getMessageByMessageId(messageId)
.getSingleOrNull(); .getSingleOrNull();
if (message != null) { if (message != null) {
await handleMediaError(message); 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(); downloadStartedForMediaReceived[message.messageId] = DateTime.now();
String downloadToken = uint8ListToHex(content.downloadToken!); String downloadToken = uint8ListToHex(content.downloadToken!);
@ -175,13 +180,51 @@ Future startDownloadMedia(Message message, bool force,
); );
Log.info( 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); try {
await downloadFileFast(media.messageId, apiUrl);
return result; return;
} catch (e) {
Log.error("Fast download failed: $e");
final result = await FileDownloader().enqueue(task);
return result;
}
} catch (e) { } catch (e) {
Log.error("Exception during upload: $e"); Log.error("Exception during download: $e");
}
}
Future<void> 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}");
} }
} }

View file

@ -9,6 +9,7 @@ import 'dart:io';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:http/http.dart' as http;
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@ -410,31 +411,13 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async {
); );
return; return;
} }
if (update.status == TaskStatus.failed || if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled) { update.status == TaskStatus.canceled) {
Log.error("Upload failed: ${update.status}"); Log.error("Upload failed: ${update.status}");
failed = true; failed = true;
} else if (update.status == TaskStatus.complete) { } else if (update.status == TaskStatus.complete) {
if (update.responseStatusCode == 200) { if (update.responseStatusCode == 200) {
Log.info("Upload of $mediaUploadId success!"); await handleUploadSuccess(media);
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),
),
);
}
return; return;
} else if (update.responseStatusCode != null) { } else if (update.responseStatusCode != null) {
if (update.responseStatusCode! >= 400 && if (update.responseStatusCode! >= 400 &&
@ -462,6 +445,26 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async {
'Status update for ${update.task.taskId} with status ${update.status}'); '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 { Future handleUploadError(MediaUpload mediaUpload) async {
// if the messageIds are already there notify the user about this error... // if the messageIds are already there notify the user about this error...
if (mediaUpload.messageIds != null) { if (mediaUpload.messageIds != null) {
@ -573,12 +576,13 @@ Future handleMediaUpload(MediaUpload media) async {
final uploadRequestBytes = uploadRequest.writeToBuffer(); final uploadRequestBytes = uploadRequest.writeToBuffer();
String? apiAuthToken = String? apiAuthTokenRaw =
await FlutterSecureStorage().read(key: SecureStorageKeys.apiAuthToken); await FlutterSecureStorage().read(key: SecureStorageKeys.apiAuthToken);
if (apiAuthToken == null) { if (apiAuthTokenRaw == null) {
Log.error("api auth token not defined."); Log.error("api auth token not defined.");
return; return;
} }
String apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw));
File uploadRequestFile = await writeSendMediaFile( File uploadRequestFile = await writeSendMediaFile(
media.mediaUploadId, media.mediaUploadId,
@ -590,35 +594,65 @@ Future handleMediaUpload(MediaUpload media) async {
"http${apiService.apiSecure}://${apiService.apiHost}/api/upload"; "http${apiService.apiSecure}://${apiService.apiHost}/api/upload";
try { 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}"); Log.info("Starting upload from ${media.mediaUploadId}");
final result = await FileDownloader().enqueue(task); try {
await uploadFileFast(media, uploadRequestBytes, apiUrl, apiAuthToken);
if (result) { } catch (e) {
await twonlyDB.mediaUploadsDao.updateMediaUpload( Log.error("Fast upload failed: $e. Using slow method.");
media.mediaUploadId, final task = UploadTask.fromFile(
MediaUploadsCompanion( taskId: "upload_${media.mediaUploadId}",
state: Value(UploadState.uploadTaskStarted), 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) { } catch (e) {
Log.error("Exception during upload: $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<bool> compressVideoIfExists(int mediaUploadId) async { Future<bool> compressVideoIfExists(int mediaUploadId) async {
String basePath = await getMediaFilePath(mediaUploadId, "send"); String basePath = await getMediaFilePath(mediaUploadId, "send");
File videoOriginalFile = File("$basePath.original.mp4"); File videoOriginalFile = File("$basePath.original.mp4");

View file

@ -97,7 +97,8 @@ Future performTwonlySafeBackup({bool force = false}) async {
await backupDatabaseFile.delete(); await backupDatabaseFile.delete();
await backupDatabaseFileCleaned.delete(); await backupDatabaseFileCleaned.delete();
print("twonlyDatabaseBytes = ${twonlyDatabaseBytes.lengthInBytes}"); Log.info("twonlyDatabaseBytes = ${twonlyDatabaseBytes.lengthInBytes}");
Log.info("secureStorageBytes = ${jsonEncode(secureStorageBackup)}");
final backupProto = TwonlySafeBackupContent( final backupProto = TwonlySafeBackupContent(
secureStorageJson: jsonEncode(secureStorageBackup), secureStorageJson: jsonEncode(secureStorageBackup),
@ -171,7 +172,7 @@ Future performTwonlySafeBackup({bool force = false}) async {
file: encryptedBackupBytesFile, file: encryptedBackupBytesFile,
httpRequestMethod: "PUT", httpRequestMethod: "PUT",
url: (await getTwonlySafeBackupUrl())!, url: (await getTwonlySafeBackupUrl())!,
requiresWiFi: true, // requiresWiFi: true,
priority: 5, priority: 5,
post: 'binary', post: 'binary',
retries: 2, retries: 2,

View file

@ -4,7 +4,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
# Prevent accidental publishing to pub.dev. # Prevent accidental publishing to pub.dev.
publish_to: 'none' publish_to: 'none'
version: 0.0.47+47 version: 0.0.48+48
environment: environment:
sdk: ^3.6.0 sdk: ^3.6.0