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
/// 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;

View file

@ -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<bool> 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<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: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<bool> compressVideoIfExists(int mediaUploadId) async {
String basePath = await getMediaFilePath(mediaUploadId, "send");
File videoOriginalFile = File("$basePath.original.mp4");

View file

@ -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,

View file

@ -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