diff --git a/flutter/assets/file_transfer.svg b/flutter/assets/file_transfer.svg
new file mode 100644
index 000000000..e1d8ccbec
--- /dev/null
+++ b/flutter/assets/file_transfer.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart
index ce7b26b58..2ef04b244 100644
--- a/flutter/lib/desktop/pages/server_page.dart
+++ b/flutter/lib/desktop/pages/server_page.dart
@@ -2,6 +2,7 @@
import 'dart:async';
import 'dart:io';
+import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart';
@@ -9,12 +10,14 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:get/get.dart';
+import 'package:percent_indicator/linear_percent_indicator.dart';
import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../common.dart';
import '../../common/widgets/chat_page.dart';
+import '../../models/file_model.dart';
import '../../models/platform_model.dart';
import '../../models/server_model.dart';
@@ -32,6 +35,7 @@ class _DesktopServerPageState extends State
void initState() {
gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
windowManager.addListener(this);
+ Get.put(tabController);
tabController.onRemoved = (_, id) {
onRemoveId(id);
};
@@ -111,6 +115,7 @@ class ConnectionManagerState extends State {
});
}
windowManager.setTitle(getWindowNameWithId(client.peerId));
+ gFFI.cmFileModel.updateCurrentClientId(client.id);
}
}
};
@@ -173,7 +178,7 @@ class ConnectionManagerState extends State {
pageViewBuilder: (pageView) => Row(
children: [
Consumer(
- builder: (_, model, child) => model.isShowCMChatPage
+ builder: (_, model, child) => model.isShowCMSidePage
? Expanded(
child: buildRemoteBlock(
child: Container(
@@ -182,8 +187,7 @@ class ConnectionManagerState extends State {
right: BorderSide(
color: Theme.of(context)
.dividerColor))),
- child:
- ChatPage(type: ChatPageType.desktopCM)),
+ child: buildSidePage()),
),
flex: (kConnectionManagerWindowSizeOpenChat.width -
kConnectionManagerWindowSizeClosedChat
@@ -204,6 +208,19 @@ class ConnectionManagerState extends State {
);
}
+ Widget buildSidePage() {
+ final selected = gFFI.serverModel.tabController.state.value.selected;
+ if (selected < 0 || selected >= gFFI.serverModel.clients.length) {
+ return Offstage();
+ }
+ final clientType = gFFI.serverModel.clients[selected].type_();
+ if (clientType == ClientType.file) {
+ return _FileTransferLogPage();
+ } else {
+ return ChatPage(type: ChatPageType.desktopCM);
+ }
+ }
+
Widget buildTitleBar() {
return SizedBox(
height: kDesktopRemoteTabBarHeight,
@@ -447,14 +464,21 @@ class _CmHeaderState extends State<_CmHeader>
),
),
Offstage(
- offstage: !client.authorized || client.type_() != ClientType.remote,
+ offstage: !client.authorized ||
+ (client.type_() != ClientType.remote &&
+ client.type_() != ClientType.file),
child: IconButton(
- onPressed: () => checkClickTime(
- client.id,
- () => gFFI.chatModel
- .toggleCMChatPage(MessageKey(client.peerId, client.id)),
- ),
- icon: SvgPicture.asset('assets/chat2.svg'),
+ onPressed: () => checkClickTime(client.id, () {
+ if (client.type_() != ClientType.file) {
+ gFFI.chatModel.toggleCMSidePage();
+ } else {
+ gFFI.chatModel
+ .toggleCMChatPage(MessageKey(client.peerId, client.id));
+ }
+ }),
+ icon: SvgPicture.asset(client.type_() == ClientType.file
+ ? 'assets/file_transfer.svg'
+ : 'assets/chat2.svg'),
splashRadius: kDesktopIconButtonSplashRadius,
),
)
@@ -912,3 +936,182 @@ void checkClickTime(int id, Function() callback) async {
if (d > 120) callback();
});
}
+
+class _FileTransferLogPage extends StatefulWidget {
+ _FileTransferLogPage({Key? key}) : super(key: key);
+
+ @override
+ State<_FileTransferLogPage> createState() => __FileTransferLogPageState();
+}
+
+class __FileTransferLogPageState extends State<_FileTransferLogPage> {
+ @override
+ Widget build(BuildContext context) {
+ return statusList();
+ }
+
+ Widget generateCard(Widget child) {
+ return Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).cardColor,
+ borderRadius: BorderRadius.all(
+ Radius.circular(15.0),
+ ),
+ ),
+ child: child,
+ );
+ }
+
+ Widget statusList() {
+ return PreferredSize(
+ preferredSize: const Size(200, double.infinity),
+ child: Container(
+ margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0),
+ padding: const EdgeInsets.all(8.0),
+ child: Obx(
+ () {
+ final jobTable = gFFI.cmFileModel.currentJobTable;
+ statusListView(List jobs) => ListView.builder(
+ controller: ScrollController(),
+ itemBuilder: (BuildContext context, int index) {
+ final item = jobs[index];
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 5),
+ child: generateCard(
+ Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ SizedBox(
+ width: 50,
+ child: Column(
+ children: [
+ Transform.rotate(
+ angle: item.isRemoteToLocal ? 0 : pi,
+ child: SvgPicture.asset(
+ "assets/arrow.svg",
+ color: Theme.of(context)
+ .tabBarTheme
+ .labelColor,
+ ),
+ ),
+ Text(item.isRemoteToLocal
+ ? translate('Send')
+ : translate('Receive'))
+ ],
+ ),
+ ).paddingOnly(left: 15),
+ const SizedBox(
+ width: 16.0,
+ ),
+ Expanded(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment:
+ CrossAxisAlignment.start,
+ children: [
+ Text(
+ item.fileName,
+ ).paddingSymmetric(vertical: 10),
+ if (item.totalSize > 0)
+ Text(
+ '${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
+ style: TextStyle(
+ fontSize: 12,
+ color: MyTheme.darkGray,
+ ),
+ ),
+ if (item.totalSize > 0)
+ Offstage(
+ offstage: item.state !=
+ JobState.inProgress,
+ child: Text(
+ '${translate("Speed")} ${readableFileSize(item.speed)}/s',
+ style: TextStyle(
+ fontSize: 12,
+ color: MyTheme.darkGray,
+ ),
+ ),
+ ),
+ Offstage(
+ offstage:
+ item.state == JobState.inProgress,
+ child: Text(
+ translate(
+ item.display(),
+ ),
+ style: TextStyle(
+ fontSize: 12,
+ color: MyTheme.darkGray,
+ ),
+ ),
+ ),
+ if (item.totalSize > 0)
+ Offstage(
+ offstage: item.state !=
+ JobState.inProgress,
+ child: LinearPercentIndicator(
+ padding:
+ EdgeInsets.only(right: 15),
+ animateFromLastPercent: true,
+ center: Text(
+ '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
+ ),
+ barRadius: Radius.circular(15),
+ percent: item.finishedSize /
+ item.totalSize,
+ progressColor: MyTheme.accent,
+ backgroundColor:
+ Theme.of(context).hoverColor,
+ lineHeight:
+ kDesktopFileTransferRowHeight,
+ ).paddingSymmetric(vertical: 15),
+ ),
+ ],
+ ),
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [],
+ ),
+ ],
+ ),
+ ],
+ ).paddingSymmetric(vertical: 10),
+ ),
+ );
+ },
+ itemCount: jobTable.length,
+ );
+
+ return jobTable.isEmpty
+ ? generateCard(
+ Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SvgPicture.asset(
+ "assets/transfer.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ height: 40,
+ ).paddingOnly(bottom: 10),
+ Text(
+ translate("No transfers in progress"),
+ textAlign: TextAlign.center,
+ textScaleFactor: 1.20,
+ style: TextStyle(
+ color:
+ Theme.of(context).tabBarTheme.labelColor),
+ ),
+ ],
+ ),
+ ),
+ )
+ : statusListView(jobTable);
+ },
+ )),
+ );
+ }
+}
diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart
index 898427351..487f57e4d 100644
--- a/flutter/lib/models/chat_model.dart
+++ b/flutter/lib/models/chat_model.dart
@@ -86,13 +86,13 @@ class ChatModel with ChangeNotifier {
late final Map _messages = {};
MessageKey _currentKey = MessageKey('', -2); // -2 is invalid value
- late bool _isShowCMChatPage = false;
+ late bool _isShowCMSidePage = false;
Map get messages => _messages;
MessageKey get currentKey => _currentKey;
- bool get isShowCMChatPage => _isShowCMChatPage;
+ bool get isShowCMSidePage => _isShowCMSidePage;
void setOverlayState(BlockableOverlayState blockableOverlayState) {
_blockableOverlayState = blockableOverlayState;
@@ -255,7 +255,7 @@ class ChatModel with ChangeNotifier {
showChatPage(MessageKey key) async {
if (isDesktop) {
if (isConnManager) {
- if (!_isShowCMChatPage) {
+ if (!_isShowCMSidePage) {
await toggleCMChatPage(key);
}
} else {
@@ -272,12 +272,26 @@ class ChatModel with ChangeNotifier {
}
}
+ showSidePage() async {
+ if (isDesktop) {
+ if (isConnManager) {
+ if (!_isShowCMSidePage) {
+ await toggleCMSidePage();
+ }
+ }
+ }
+ }
+
toggleCMChatPage(MessageKey key) async {
if (gFFI.chatModel.currentKey != key) {
gFFI.chatModel.changeCurrentKey(key);
}
- if (_isShowCMChatPage) {
- _isShowCMChatPage = !_isShowCMChatPage;
+ await toggleCMSidePage();
+ }
+
+ toggleCMSidePage() async {
+ if (_isShowCMSidePage) {
+ _isShowCMSidePage = !_isShowCMSidePage;
notifyListeners();
await windowManager.show();
await windowManager.setSizeAlignment(
@@ -287,7 +301,7 @@ class ChatModel with ChangeNotifier {
await windowManager.show();
await windowManager.setSizeAlignment(
kConnectionManagerWindowSizeOpenChat, Alignment.topRight);
- _isShowCMChatPage = !_isShowCMChatPage;
+ _isShowCMSidePage = !_isShowCMSidePage;
notifyListeners();
}
}
diff --git a/flutter/lib/models/cm_file_model.dart b/flutter/lib/models/cm_file_model.dart
new file mode 100644
index 000000000..064ce9d53
--- /dev/null
+++ b/flutter/lib/models/cm_file_model.dart
@@ -0,0 +1,154 @@
+import 'dart:collection';
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/models/model.dart';
+import 'package:flutter_hbb/models/server_model.dart';
+import 'package:get/get.dart';
+import 'file_model.dart';
+
+class CmFileModel {
+ final WeakReference parent;
+ final currentJobTable = RxList();
+ final _jobTables = HashMap>.fromEntries([]);
+ Stopwatch stopwatch = Stopwatch();
+ int _lastElapsed = 0;
+ bool _jobAdded = false;
+ bool _showing = false;
+
+ CmFileModel(this.parent);
+
+ void updateCurrentClientId(int id) {
+ if (_jobTables[id] == null) {
+ _jobTables[id] = RxList();
+ }
+ Future.delayed(Duration.zero, () {
+ currentJobTable.value = _jobTables[id]!;
+ });
+ }
+
+ onFileTransferLog(dynamic log) {
+ try {
+ dynamic d = jsonDecode(log);
+ if (!stopwatch.isRunning) stopwatch.start();
+ bool calcSpeed = stopwatch.elapsedMilliseconds - _lastElapsed >= 1000;
+ if (calcSpeed) {
+ _lastElapsed = stopwatch.elapsedMilliseconds;
+ }
+ if (d is List) {
+ for (var l in d) {
+ _dealOneJob(l, calcSpeed);
+ }
+ } else {
+ _dealOneJob(d, calcSpeed);
+ }
+ currentJobTable.refresh();
+ Future.delayed(Duration.zero, () async {
+ if (_jobAdded) {
+ _jobAdded = false;
+ if (!_showing) {
+ _showing = true;
+ await gFFI.chatModel.showSidePage();
+ _showing = false;
+ }
+ }
+ });
+ } catch (e) {
+ debugPrint("onFileTransferLog:$e");
+ }
+ }
+
+ _dealOneJob(dynamic l, bool calcSpeed) {
+ final data = TransferJobSerdeData.fromJson(l);
+ Client? client =
+ gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
+ var jobTable = _jobTables[data.connId];
+ if (jobTable == null) {
+ debugPrint("jobTable should not be null");
+ return;
+ }
+ JobProgress? job = jobTable.firstWhereOrNull((e) => e.id == data.id);
+ if (job == null) {
+ job = JobProgress();
+ jobTable.add(job);
+ _jobAdded = true;
+ final currentSelectedTab =
+ gFFI.serverModel.tabController.state.value.selectedTabInfo;
+ if (currentSelectedTab.key != data.connId.toString()) {
+ client?.unreadChatMessageCount.value += 1;
+ }
+ }
+ job.id = data.id;
+ job.isRemoteToLocal = data.isRemote;
+ job.fileName = data.path;
+ job.totalSize = data.totalSize;
+ job.finishedSize = data.finishedSize;
+ if (job.finishedSize > data.totalSize) {
+ job.finishedSize = data.totalSize;
+ }
+ job.isRemoteToLocal = data.isRemote;
+
+ if (job.finishedSize > 0) {
+ if (job.finishedSize < job.totalSize) {
+ job.state = JobState.inProgress;
+ } else {
+ job.state = JobState.done;
+ }
+ }
+ if (data.done) {
+ job.state = JobState.done;
+ } else if (data.cancel || data.error == 'skipped') {
+ job.state = JobState.done;
+ job.err = 'skipped';
+ } else if (data.error.isNotEmpty) {
+ job.state = JobState.error;
+ job.err = data.error;
+ }
+ if (calcSpeed) {
+ job.speed = (data.transferred - job.lastTransferredSize) * 1.0;
+ job.lastTransferredSize = data.transferred;
+ }
+ jobTable.refresh();
+ }
+}
+
+class TransferJobSerdeData {
+ int connId;
+ int id;
+ String path;
+ bool isRemote;
+ int totalSize;
+ int finishedSize;
+ int transferred;
+ bool done;
+ bool cancel;
+ String error;
+
+ TransferJobSerdeData({
+ required this.connId,
+ required this.id,
+ required this.path,
+ required this.isRemote,
+ required this.totalSize,
+ required this.finishedSize,
+ required this.transferred,
+ required this.done,
+ required this.cancel,
+ required this.error,
+ });
+
+ TransferJobSerdeData.fromJson(dynamic d)
+ : this(
+ connId: d['connId'] ?? 0,
+ id: int.tryParse(d['id'].toString()) ?? 0,
+ path: d['path'] ?? '',
+ isRemote: d['isRemote'] ?? false,
+ totalSize: d['totalSize'] ?? 0,
+ finishedSize: d['finishedSize'] ?? 0,
+ transferred: d['transferred'] ?? 0,
+ done: d['done'] ?? false,
+ cancel: d['cancel'] ?? false,
+ error: d['error'] ?? '',
+ );
+}
diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart
index 35c0d020d..108c76f1e 100644
--- a/flutter/lib/models/file_model.dart
+++ b/flutter/lib/models/file_model.dart
@@ -1029,6 +1029,7 @@ class JobProgress {
var to = "";
var showHidden = false;
var err = "";
+ int lastTransferredSize = 0;
clear() {
state = JobState.none;
diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart
index 14b6b4df6..a07a0d5e7 100644
--- a/flutter/lib/models/model.dart
+++ b/flutter/lib/models/model.dart
@@ -11,6 +11,7 @@ import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/generated_bridge.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/chat_model.dart';
+import 'package:flutter_hbb/models/cm_file_model.dart';
import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_hbb/models/group_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
@@ -317,6 +318,10 @@ class FfiModel with ChangeNotifier {
}
}
}
+ } else if (name == "cm_file_transfer_log") {
+ if (isDesktop) {
+ gFFI.cmFileModel.onFileTransferLog(evt['log']);
+ }
} else {
debugPrint('Unknown event name: $name');
}
@@ -1696,6 +1701,7 @@ class FFI {
late final RecordingModel recordingModel; // session
late final InputModel inputModel; // session
late final ElevationModel elevationModel; // session
+ late final CmFileModel cmFileModel; // cm
FFI(SessionID? sId) {
sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId);
@@ -1714,6 +1720,7 @@ class FFI {
recordingModel = RecordingModel(WeakReference(this));
inputModel = InputModel(WeakReference(this));
elevationModel = ElevationModel(WeakReference(this));
+ cmFileModel = CmFileModel(WeakReference(this));
}
/// Mobile reuse FFI
diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto
index 82206cbf2..47de31e45 100644
--- a/libs/hbb_common/protos/message.proto
+++ b/libs/hbb_common/protos/message.proto
@@ -397,6 +397,7 @@ message FileTransferReceiveRequest {
string path = 2; // path written to
repeated FileEntry files = 3;
int32 file_num = 4;
+ uint64 total_size = 5;
}
message FileRemoveDir {
diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs
index 33902e575..b7ea836b7 100644
--- a/libs/hbb_common/src/fs.rs
+++ b/libs/hbb_common/src/fs.rs
@@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde_derive::{Deserialize, Serialize};
+use serde_json::json;
use tokio::{fs::File, io::*};
use crate::{anyhow::anyhow, bail, get_version_number, message_proto::*, ResultType, Stream};
@@ -194,7 +195,8 @@ pub fn can_enable_overwrite_detection(version: i64) -> bool {
version >= get_version_number("1.1.10")
}
-#[derive(Default)]
+#[derive(Default, Serialize, Debug)]
+#[serde(rename_all = "camelCase")]
pub struct TransferJob {
pub id: i32,
pub remote: String,
@@ -203,10 +205,13 @@ pub struct TransferJob {
pub is_remote: bool,
pub is_last_job: bool,
pub file_num: i32,
+ #[serde(skip_serializing)]
pub files: Vec,
+ pub conn_id: i32, // server only
+ #[serde(skip_serializing)]
file: Option,
- total_size: u64,
+ pub total_size: u64,
finished_size: u64,
transferred: u64,
enable_overwrite_detection: bool,
@@ -695,13 +700,20 @@ pub fn new_send_confirm(r: FileTransferSendConfirmRequest) -> Message {
}
#[inline]
-pub fn new_receive(id: i32, path: String, file_num: i32, files: Vec) -> Message {
+pub fn new_receive(
+ id: i32,
+ path: String,
+ file_num: i32,
+ files: Vec,
+ total_size: u64,
+) -> Message {
let mut action = FileAction::new();
action.set_receive(FileTransferReceiveRequest {
id,
path,
files,
file_num,
+ total_size,
..Default::default()
});
let mut msg_out = Message::new();
@@ -748,10 +760,16 @@ pub fn get_job(id: i32, jobs: &mut [TransferJob]) -> Option<&mut TransferJob> {
jobs.iter_mut().find(|x| x.id() == id)
}
+#[inline]
+pub fn get_job_immutable(id: i32, jobs: &[TransferJob]) -> Option<&TransferJob> {
+ jobs.iter().find(|x| x.id() == id)
+}
+
pub async fn handle_read_jobs(
jobs: &mut Vec,
stream: &mut crate::Stream,
-) -> ResultType<()> {
+) -> ResultType {
+ let mut job_log = Default::default();
let mut finished = Vec::new();
for job in jobs.iter_mut() {
if job.is_last_job {
@@ -768,9 +786,11 @@ pub async fn handle_read_jobs(
}
Ok(None) => {
if job.job_completed() {
+ job_log = serialize_transfer_job(job, true, false, "");
finished.push(job.id());
match job.job_error() {
Some(err) => {
+ job_log = serialize_transfer_job(job, false, false, &err);
stream
.send(&new_error(job.id(), err, job.file_num()))
.await?
@@ -786,7 +806,7 @@ pub async fn handle_read_jobs(
for id in finished {
remove_job(id, jobs);
}
- Ok(())
+ Ok(job_log)
}
pub fn remove_all_empty_dir(path: &PathBuf) -> ResultType<()> {
@@ -861,3 +881,20 @@ pub fn is_write_need_confirmation(
Ok(DigestCheckResult::NoSuchFile)
}
}
+
+pub fn serialize_transfer_jobs(jobs: &[TransferJob]) -> String {
+ let mut v = vec![];
+ for job in jobs {
+ let value = serde_json::to_value(job).unwrap_or_default();
+ v.push(value);
+ }
+ serde_json::to_string(&v).unwrap_or_default()
+}
+
+pub fn serialize_transfer_job(job: &TransferJob, done: bool, cancel: bool, error: &str) -> String {
+ let mut value = serde_json::to_value(job).unwrap_or_default();
+ value["done"] = json!(done);
+ value["cancel"] = json!(cancel);
+ value["error"] = json!(error);
+ serde_json::to_string(&value).unwrap_or_default()
+}
diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs
index aaf426e28..ddf4b64ac 100644
--- a/src/client/io_loop.rs
+++ b/src/client/io_loop.rs
@@ -483,9 +483,13 @@ impl Remote {
// peer is not windows, need transform \ to /
fs::transform_windows_path(&mut files);
}
+ let total_size = job.total_size();
self.read_jobs.push(job);
self.timer = time::interval(MILLI1);
- allow_err!(peer.send(&fs::new_receive(id, to, file_num, files)).await);
+ allow_err!(
+ peer.send(&fs::new_receive(id, to, file_num, files, total_size))
+ .await
+ );
}
}
}
@@ -568,7 +572,8 @@ impl Remote {
id,
job.path.to_string_lossy().to_string(),
job.file_num,
- job.files.clone()
+ job.files.clone(),
+ job.total_size(),
))
.await
);
diff --git a/src/flutter.rs b/src/flutter.rs
index af9580587..817503c68 100644
--- a/src/flutter.rs
+++ b/src/flutter.rs
@@ -907,6 +907,10 @@ pub mod connection_manager {
let client_json = serde_json::to_string(&client).unwrap_or("".into());
self.push_event("update_voice_call_state", vec![("client", &client_json)]);
}
+
+ fn file_transfer_log(&self, log: String) {
+ self.push_event("cm_file_transfer_log", vec![("log", &log.to_string())]);
+ }
}
impl FlutterHandler {
diff --git a/src/ipc.rs b/src/ipc.rs
index 1d0c99ea3..1cbd994bb 100644
--- a/src/ipc.rs
+++ b/src/ipc.rs
@@ -70,6 +70,8 @@ pub enum FS {
file_num: i32,
files: Vec<(String, u64)>,
overwrite_detection: bool,
+ total_size: u64,
+ conn_id: i32,
},
CancelWrite {
id: i32,
@@ -231,6 +233,7 @@ pub enum Data {
Plugin(Plugin),
#[cfg(windows)]
SyncWinCpuUsage(Option),
+ FileTransferLog(String),
}
#[tokio::main(flavor = "current_thread")]
diff --git a/src/server/connection.rs b/src/server/connection.rs
index a0f05c43a..c9f07eb78 100644
--- a/src/server/connection.rs
+++ b/src/server/connection.rs
@@ -188,6 +188,7 @@ pub struct Connection {
lr: LoginRequest,
last_recv_time: Arc>,
chat_unanswered: bool,
+ file_transferred: bool,
#[cfg(windows)]
portable: PortableState,
from_switch: bool,
@@ -320,6 +321,7 @@ impl Connection {
lr: Default::default(),
last_recv_time: Arc::new(Mutex::new(Instant::now())),
chat_unanswered: false,
+ file_transferred: false,
#[cfg(windows)]
portable: Default::default(),
from_switch: false,
@@ -399,6 +401,7 @@ impl Connection {
}
ipc::Data::Close => {
conn.chat_unanswered = false; // seen
+ conn.file_transferred = false; //seen
conn.send_close_reason_no_retry("").await;
conn.on_close("connection manager", true).await;
break;
@@ -536,9 +539,17 @@ impl Connection {
},
_ = conn.file_timer.tick() => {
if !conn.read_jobs.is_empty() {
- if let Err(err) = fs::handle_read_jobs(&mut conn.read_jobs, &mut conn.stream).await {
- conn.on_close(&err.to_string(), false).await;
- break;
+ conn.send_to_cm(ipc::Data::FileTransferLog(fs::serialize_transfer_jobs(&conn.read_jobs)));
+ match fs::handle_read_jobs(&mut conn.read_jobs, &mut conn.stream).await {
+ Ok(log) => {
+ if !log.is_empty() {
+ conn.send_to_cm(ipc::Data::FileTransferLog(log));
+ }
+ }
+ Err(err) => {
+ conn.on_close(&err.to_string(), false).await;
+ break;
+ }
}
} else {
conn.file_timer = time::interval_at(Instant::now() + SEC30, SEC30);
@@ -1717,6 +1728,7 @@ impl Connection {
}
}
Some(file_action::Union::Send(s)) => {
+ // server to client
let id = s.id;
let od = can_enable_overwrite_detection(get_version_number(
&self.lr.version,
@@ -1734,10 +1746,12 @@ impl Connection {
Err(err) => {
self.send(fs::new_error(id, err, 0)).await;
}
- Ok(job) => {
+ Ok(mut job) => {
self.send(fs::new_dir(id, path, job.files().to_vec()))
.await;
let mut files = job.files().to_owned();
+ job.is_remote = true;
+ job.conn_id = self.inner.id();
self.read_jobs.push(job);
self.file_timer = time::interval(MILLI1);
self.post_file_audit(
@@ -1751,8 +1765,10 @@ impl Connection {
);
}
}
+ self.file_transferred = true;
}
Some(file_action::Union::Receive(r)) => {
+ // client to server
// note: 1.1.10 introduced identical file detection, which breaks original logic of send/recv files
// whenever got send/recv request, check peer version to ensure old version of rustdesk
let od = can_enable_overwrite_detection(get_version_number(
@@ -1769,6 +1785,8 @@ impl Connection {
.map(|f| (f.name, f.modified_time))
.collect(),
overwrite_detection: od,
+ total_size: r.total_size,
+ conn_id: self.inner.id(),
});
self.post_file_audit(
FileAuditType::RemoteReceive,
@@ -1780,6 +1798,7 @@ impl Connection {
.collect(),
json!({}),
);
+ self.file_transferred = true;
}
Some(file_action::Union::RemoveDir(d)) => {
self.send_fs(ipc::FS::RemoveDir {
@@ -1803,6 +1822,11 @@ impl Connection {
}
Some(file_action::Union::Cancel(c)) => {
self.send_fs(ipc::FS::CancelWrite { id: c.id });
+ if let Some(job) = fs::get_job_immutable(c.id, &self.read_jobs) {
+ self.send_to_cm(ipc::Data::FileTransferLog(
+ fs::serialize_transfer_job(job, false, true, ""),
+ ));
+ }
fs::remove_job(c.id, &mut self.read_jobs);
}
Some(file_action::Union::SendConfirm(r)) => {
@@ -2254,7 +2278,7 @@ impl Connection {
lock_screen().await;
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
- let data = if self.chat_unanswered {
+ let data = if self.chat_unanswered || self.file_transferred {
ipc::Data::Disconnected
} else {
ipc::Data::Close
diff --git a/src/ui/cm.rs b/src/ui/cm.rs
index a574b5e88..b827b76b1 100644
--- a/src/ui/cm.rs
+++ b/src/ui/cm.rs
@@ -59,13 +59,11 @@ impl InvokeUiCM for SciterHandler {
fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) {
self.call(
"updateVoiceCallState",
- &make_args!(
- client.id,
- client.in_voice_call,
- client.incoming_voice_call
- ),
+ &make_args!(client.id, client.in_voice_call, client.incoming_voice_call),
);
}
+
+ fn file_transfer_log(&self, _log: String) {}
}
impl SciterHandler {
diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs
index 16fa59631..ceb185443 100644
--- a/src/ui_cm_interface.rs
+++ b/src/ui_cm_interface.rs
@@ -99,6 +99,8 @@ pub trait InvokeUiCM: Send + Clone + 'static + Sized {
fn show_elevation(&self, show: bool);
fn update_voice_call_state(&self, client: &Client);
+
+ fn file_transfer_log(&self, log: String);
}
impl Deref for ConnectionManager {
@@ -357,6 +359,7 @@ impl IpcTaskRunner {
);
}
}
+ let (tx_log, mut rx_log) = mpsc::unbounded_channel::();
self.running = false;
loop {
@@ -403,11 +406,16 @@ impl IpcTaskRunner {
if let ipc::FS::WriteBlock { id, file_num, data: _, compressed } = fs {
if let Ok(bytes) = self.stream.next_raw().await {
fs = ipc::FS::WriteBlock{id, file_num, data:bytes.into(), compressed};
- handle_fs(fs, &mut write_jobs, &self.tx).await;
+ handle_fs(fs, &mut write_jobs, &self.tx, Some(&tx_log)).await;
}
} else {
- handle_fs(fs, &mut write_jobs, &self.tx).await;
+ handle_fs(fs, &mut write_jobs, &self.tx, Some(&tx_log)).await;
}
+ let log = fs::serialize_transfer_jobs(&write_jobs);
+ self.cm.ui_handler.file_transfer_log(log);
+ }
+ Data::FileTransferLog(log) => {
+ self.cm.ui_handler.file_transfer_log(log);
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Data::ClipboardFile(_clip) => {
@@ -509,6 +517,9 @@ impl IpcTaskRunner {
//
}
},
+ Some(job_log) = rx_log.recv() => {
+ self.cm.ui_handler.file_transfer_log(job_log);
+ }
}
}
}
@@ -632,7 +643,7 @@ pub async fn start_listen(
cm.new_message(current_id, text);
}
Some(Data::FS(fs)) => {
- handle_fs(fs, &mut write_jobs, &tx).await;
+ handle_fs(fs, &mut write_jobs, &tx, None).await;
}
Some(Data::Close) => {
break;
@@ -647,7 +658,14 @@ pub async fn start_listen(
}
#[cfg(not(any(target_os = "ios")))]
-async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec, tx: &UnboundedSender) {
+async fn handle_fs(
+ fs: ipc::FS,
+ write_jobs: &mut Vec,
+ tx: &UnboundedSender,
+ tx_log: Option<&UnboundedSender>,
+) {
+ use hbb_common::fs::serialize_transfer_job;
+
match fs {
ipc::FS::ReadDir {
dir,
@@ -674,10 +692,12 @@ async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec, tx: &Unbo
file_num,
mut files,
overwrite_detection,
+ total_size,
+ conn_id,
} => {
// cm has no show_hidden context
// dummy remote, show_hidden, is_remote
- write_jobs.push(fs::TransferJob::new_write(
+ let mut job = fs::TransferJob::new_write(
id,
"".to_string(),
path,
@@ -693,11 +713,17 @@ async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec, tx: &Unbo
})
.collect(),
overwrite_detection,
- ));
+ );
+ job.total_size = total_size;
+ job.conn_id = conn_id;
+ write_jobs.push(job);
}
ipc::FS::CancelWrite { id } => {
if let Some(job) = fs::get_job(id, write_jobs) {
job.remove_download_file();
+ tx_log.map(|tx: &UnboundedSender| {
+ tx.send(serialize_transfer_job(job, false, true, ""))
+ });
fs::remove_job(id, write_jobs);
}
}
@@ -705,11 +731,13 @@ async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec, tx: &Unbo
if let Some(job) = fs::get_job(id, write_jobs) {
job.modify_time();
send_raw(fs::new_done(id, file_num), tx);
+ tx_log.map(|tx| tx.send(serialize_transfer_job(job, true, false, "")));
fs::remove_job(id, write_jobs);
}
}
ipc::FS::WriteError { id, file_num, err } => {
if let Some(job) = fs::get_job(id, write_jobs) {
+ tx_log.map(|tx| tx.send(serialize_transfer_job(job, false, false, &err)));
send_raw(fs::new_error(job.id(), err, file_num), tx);
fs::remove_job(job.id(), write_jobs);
}