mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 12:48:41 +00:00
add fast upload and download
This commit is contained in:
parent
341c7a36da
commit
f1dc6dff54
5 changed files with 137 additions and 59 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
} catch (e) {
|
||||||
Log.error("Exception during upload: $e");
|
Log.error("Fast download failed: $e");
|
||||||
|
final result = await FileDownloader().enqueue(task);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} catch (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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,6 +594,12 @@ Future handleMediaUpload(MediaUpload media) async {
|
||||||
"http${apiService.apiSecure}://${apiService.apiHost}/api/upload";
|
"http${apiService.apiSecure}://${apiService.apiHost}/api/upload";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
Log.info("Starting upload from ${media.mediaUploadId}");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await uploadFileFast(media, uploadRequestBytes, apiUrl, apiAuthToken);
|
||||||
|
} catch (e) {
|
||||||
|
Log.error("Fast upload failed: $e. Using slow method.");
|
||||||
final task = UploadTask.fromFile(
|
final task = UploadTask.fromFile(
|
||||||
taskId: "upload_${media.mediaUploadId}",
|
taskId: "upload_${media.mediaUploadId}",
|
||||||
displayName: (media.metadata?.isVideo ?? false) ? "image" : "video",
|
displayName: (media.metadata?.isVideo ?? false) ? "image" : "video",
|
||||||
|
|
@ -598,27 +608,51 @@ Future handleMediaUpload(MediaUpload media) async {
|
||||||
priority: 0,
|
priority: 0,
|
||||||
retries: 10,
|
retries: 10,
|
||||||
headers: {
|
headers: {
|
||||||
'x-twonly-auth-token': uint8ListToHex(base64Decode(apiAuthToken))
|
'x-twonly-auth-token': apiAuthToken,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
await FileDownloader().enqueue(task);
|
||||||
|
}
|
||||||
|
|
||||||
Log.info("Starting upload from ${media.mediaUploadId}");
|
|
||||||
|
|
||||||
final result = await FileDownloader().enqueue(task);
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
await twonlyDB.mediaUploadsDao.updateMediaUpload(
|
await twonlyDB.mediaUploadsDao.updateMediaUpload(
|
||||||
media.mediaUploadId,
|
media.mediaUploadId,
|
||||||
MediaUploadsCompanion(
|
MediaUploadsCompanion(
|
||||||
state: Value(UploadState.uploadTaskStarted),
|
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");
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue