mirror of
https://github.com/kunkundi/crossdesk.git
synced 2025-12-25 01:46:41 +08:00
Compare commits
9 Commits
latest
...
file-trans
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b322181853 | ||
|
|
3ad66f5e0b | ||
|
|
4035e0dd13 | ||
|
|
832b820096 | ||
|
|
d337971de0 | ||
|
|
a967dc72d7 | ||
|
|
5066fcda48 | ||
|
|
e7bdf42694 | ||
|
|
875fea88ee |
@@ -185,6 +185,8 @@ static std::vector<std::string> enable_daemon = {
|
|||||||
static std::vector<std::string> takes_effect_after_restart = {
|
static std::vector<std::string> takes_effect_after_restart = {
|
||||||
reinterpret_cast<const char*>(u8"重启后生效"),
|
reinterpret_cast<const char*>(u8"重启后生效"),
|
||||||
"Takes effect after restart"};
|
"Takes effect after restart"};
|
||||||
|
static std::vector<std::string> select_file = {
|
||||||
|
reinterpret_cast<const char*>(u8"选择文件"), "Select File"};
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
static std::vector<std::string> minimize_to_tray = {
|
static std::vector<std::string> minimize_to_tray = {
|
||||||
reinterpret_cast<const char*>(u8"退出时最小化到系统托盘:"),
|
reinterpret_cast<const char*>(u8"退出时最小化到系统托盘:"),
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ int Render::LocalWindow() {
|
|||||||
ImGui::SetCursorPos(
|
ImGui::SetCursorPos(
|
||||||
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.02f));
|
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.02f));
|
||||||
|
|
||||||
|
ImGui::SetWindowFontScale(0.9f);
|
||||||
ImGui::TextColored(
|
ImGui::TextColored(
|
||||||
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
|
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
|
||||||
localization::local_desktop[localization_language_index_].c_str());
|
localization::local_desktop[localization_language_index_].c_str());
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ int Render::RecentConnectionsWindow() {
|
|||||||
ImGui::SetCursorPos(
|
ImGui::SetCursorPos(
|
||||||
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.02f));
|
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.02f));
|
||||||
|
|
||||||
|
ImGui::SetWindowFontScale(0.9f);
|
||||||
ImGui::TextColored(
|
ImGui::TextColored(
|
||||||
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
|
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
|
||||||
localization::recent_connections[localization_language_index_].c_str());
|
localization::recent_connections[localization_language_index_].c_str());
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ int Render::RemoteWindow() {
|
|||||||
ImGui::SetCursorPos(
|
ImGui::SetCursorPos(
|
||||||
ImVec2(io.DisplaySize.x * 0.057f, io.DisplaySize.y * 0.02f));
|
ImVec2(io.DisplaySize.x * 0.057f, io.DisplaySize.y * 0.02f));
|
||||||
|
|
||||||
|
ImGui::SetWindowFontScale(0.9f);
|
||||||
ImGui::TextColored(
|
ImGui::TextColored(
|
||||||
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
|
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
|
||||||
localization::remote_desktop[localization_language_index_].c_str());
|
localization::remote_desktop[localization_language_index_].c_str());
|
||||||
@@ -189,11 +190,11 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
|
|||||||
props->params_.user_id = props->local_id_.c_str();
|
props->params_.user_id = props->local_id_.c_str();
|
||||||
props->peer_ = CreatePeer(&props->params_);
|
props->peer_ = CreatePeer(&props->params_);
|
||||||
|
|
||||||
props->control_window_width_ = title_bar_height_ * 8.0f;
|
props->control_window_width_ = title_bar_height_ * 9.0f;
|
||||||
props->control_window_height_ = title_bar_height_ * 1.3f;
|
props->control_window_height_ = title_bar_height_ * 1.3f;
|
||||||
props->control_window_min_width_ = title_bar_height_ * 0.65f;
|
props->control_window_min_width_ = title_bar_height_ * 0.65f;
|
||||||
props->control_window_min_height_ = title_bar_height_ * 1.3f;
|
props->control_window_min_height_ = title_bar_height_ * 1.3f;
|
||||||
props->control_window_max_width_ = title_bar_height_ * 8.0f;
|
props->control_window_max_width_ = title_bar_height_ * 9.0f;
|
||||||
props->control_window_max_height_ = title_bar_height_ * 6.0f;
|
props->control_window_max_height_ = title_bar_height_ * 6.0f;
|
||||||
|
|
||||||
if (!props->peer_) {
|
if (!props->peer_) {
|
||||||
@@ -205,7 +206,8 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
|
|||||||
AddVideoStream(props->peer_, display_info.name.c_str());
|
AddVideoStream(props->peer_, display_info.name.c_str());
|
||||||
}
|
}
|
||||||
AddAudioStream(props->peer_, props->audio_label_.c_str());
|
AddAudioStream(props->peer_, props->audio_label_.c_str());
|
||||||
AddDataStream(props->peer_, props->data_label_.c_str());
|
AddDataStream(props->peer_, props->data_label_.c_str(), false);
|
||||||
|
AddDataStream(props->peer_, props->file_label_.c_str(), true);
|
||||||
|
|
||||||
props->connection_status_ = ConnectionStatus::Connecting;
|
props->connection_status_ = ConnectionStatus::Connecting;
|
||||||
|
|
||||||
|
|||||||
@@ -713,7 +713,8 @@ int Render::CreateConnectionPeer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AddAudioStream(peer_, audio_label_.c_str());
|
AddAudioStream(peer_, audio_label_.c_str());
|
||||||
AddDataStream(peer_, data_label_.c_str());
|
AddDataStream(peer_, data_label_.c_str(), false);
|
||||||
|
AddDataStream(peer_, file_label_.c_str(), true);
|
||||||
return 0;
|
return 0;
|
||||||
} else {
|
} else {
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class Render {
|
|||||||
PeerPtr* peer_ = nullptr;
|
PeerPtr* peer_ = nullptr;
|
||||||
std::string audio_label_ = "control_audio";
|
std::string audio_label_ = "control_audio";
|
||||||
std::string data_label_ = "control_data";
|
std::string data_label_ = "control_data";
|
||||||
|
std::string file_label_ = "file";
|
||||||
std::string local_id_ = "";
|
std::string local_id_ = "";
|
||||||
std::string remote_id_ = "";
|
std::string remote_id_ = "";
|
||||||
bool exit_ = false;
|
bool exit_ = false;
|
||||||
@@ -122,6 +123,19 @@ class Render {
|
|||||||
int frame_count_ = 0;
|
int frame_count_ = 0;
|
||||||
std::chrono::steady_clock::time_point last_time_;
|
std::chrono::steady_clock::time_point last_time_;
|
||||||
XNetTrafficStats net_traffic_stats_;
|
XNetTrafficStats net_traffic_stats_;
|
||||||
|
|
||||||
|
// File transfer progress
|
||||||
|
std::atomic<bool> file_sending_ = false;
|
||||||
|
std::atomic<uint64_t> file_sent_bytes_ = 0;
|
||||||
|
std::atomic<uint64_t> file_total_bytes_ = 0;
|
||||||
|
std::atomic<uint32_t> file_send_rate_bps_ = 0; // bytes per second
|
||||||
|
std::string file_sending_name_ = "";
|
||||||
|
std::mutex file_transfer_mutex_;
|
||||||
|
std::chrono::steady_clock::time_point file_send_start_time_;
|
||||||
|
std::chrono::steady_clock::time_point file_send_last_update_time_;
|
||||||
|
uint64_t file_send_last_bytes_ = 0;
|
||||||
|
bool file_transfer_window_visible_ = false;
|
||||||
|
bool file_transfer_completed_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -172,6 +186,7 @@ class Render {
|
|||||||
int ShowRecentConnections();
|
int ShowRecentConnections();
|
||||||
void Hyperlink(const std::string& label, const std::string& url,
|
void Hyperlink(const std::string& label, const std::string& url,
|
||||||
const float window_width);
|
const float window_width);
|
||||||
|
int FileTransferWindow(std::shared_ptr<SubStreamWindowProperties>& props);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int ConnectTo(const std::string& remote_id, const char* password,
|
int ConnectTo(const std::string& remote_id, const char* password,
|
||||||
@@ -201,14 +216,17 @@ class Render {
|
|||||||
public:
|
public:
|
||||||
static void OnReceiveVideoBufferCb(const XVideoFrame* video_frame,
|
static void OnReceiveVideoBufferCb(const XVideoFrame* video_frame,
|
||||||
const char* user_id, size_t user_id_size,
|
const char* user_id, size_t user_id_size,
|
||||||
|
const char* src_id, size_t src_id_size,
|
||||||
void* user_data);
|
void* user_data);
|
||||||
|
|
||||||
static void OnReceiveAudioBufferCb(const char* data, size_t size,
|
static void OnReceiveAudioBufferCb(const char* data, size_t size,
|
||||||
const char* user_id, size_t user_id_size,
|
const char* user_id, size_t user_id_size,
|
||||||
|
const char* src_id, size_t src_id_size,
|
||||||
void* user_data);
|
void* user_data);
|
||||||
|
|
||||||
static void OnReceiveDataBufferCb(const char* data, size_t size,
|
static void OnReceiveDataBufferCb(const char* data, size_t size,
|
||||||
const char* user_id, size_t user_id_size,
|
const char* user_id, size_t user_id_size,
|
||||||
|
const char* src_id, size_t src_id_size,
|
||||||
void* user_data);
|
void* user_data);
|
||||||
|
|
||||||
static void OnSignalStatusCb(SignalStatus status, const char* user_id,
|
static void OnSignalStatusCb(SignalStatus status, const char* user_id,
|
||||||
@@ -464,6 +482,7 @@ class Render {
|
|||||||
std::string video_secondary_label_ = "secondary_display";
|
std::string video_secondary_label_ = "secondary_display";
|
||||||
std::string audio_label_ = "audio";
|
std::string audio_label_ = "audio";
|
||||||
std::string data_label_ = "data";
|
std::string data_label_ = "data";
|
||||||
|
std::string file_label_ = "file";
|
||||||
Params params_;
|
Params params_;
|
||||||
SDL_AudioDeviceID input_dev_;
|
SDL_AudioDeviceID input_dev_;
|
||||||
SDL_AudioDeviceID output_dev_;
|
SDL_AudioDeviceID output_dev_;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
#include <chrono>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
||||||
#include "device_controller.h"
|
#include "device_controller.h"
|
||||||
|
#include "file_transfer.h"
|
||||||
#include "localization.h"
|
#include "localization.h"
|
||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
@@ -212,6 +216,7 @@ void Render::SdlCaptureAudioOut([[maybe_unused]] void* userdata,
|
|||||||
|
|
||||||
void Render::OnReceiveVideoBufferCb(const XVideoFrame* video_frame,
|
void Render::OnReceiveVideoBufferCb(const XVideoFrame* video_frame,
|
||||||
const char* user_id, size_t user_id_size,
|
const char* user_id, size_t user_id_size,
|
||||||
|
const char* src_id, size_t src_id_size,
|
||||||
void* user_data) {
|
void* user_data) {
|
||||||
Render* render = (Render*)user_data;
|
Render* render = (Render*)user_data;
|
||||||
if (!render) {
|
if (!render) {
|
||||||
@@ -279,6 +284,7 @@ void Render::OnReceiveVideoBufferCb(const XVideoFrame* video_frame,
|
|||||||
|
|
||||||
void Render::OnReceiveAudioBufferCb(const char* data, size_t size,
|
void Render::OnReceiveAudioBufferCb(const char* data, size_t size,
|
||||||
const char* user_id, size_t user_id_size,
|
const char* user_id, size_t user_id_size,
|
||||||
|
const char* src_id, size_t src_id_size,
|
||||||
void* user_data) {
|
void* user_data) {
|
||||||
Render* render = (Render*)user_data;
|
Render* render = (Render*)user_data;
|
||||||
if (!render) {
|
if (!render) {
|
||||||
@@ -298,12 +304,21 @@ void Render::OnReceiveAudioBufferCb(const char* data, size_t size,
|
|||||||
|
|
||||||
void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||||
const char* user_id, size_t user_id_size,
|
const char* user_id, size_t user_id_size,
|
||||||
|
const char* src_id, size_t src_id_size,
|
||||||
void* user_data) {
|
void* user_data) {
|
||||||
Render* render = (Render*)user_data;
|
Render* render = (Render*)user_data;
|
||||||
if (!render) {
|
if (!render) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string source_id = std::string(src_id, src_id_size);
|
||||||
|
if (source_id == "file") {
|
||||||
|
// try to parse as file-transfer chunk first
|
||||||
|
static FileReceiver receiver;
|
||||||
|
receiver.OnData(data, size);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
std::string json_str(data, size);
|
std::string json_str(data, size);
|
||||||
RemoteAction remote_action;
|
RemoteAction remote_action;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,32 @@
|
|||||||
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "file_transfer.h"
|
||||||
#include "layout.h"
|
#include "layout.h"
|
||||||
#include "localization.h"
|
#include "localization.h"
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
#include "render.h"
|
#include "render.h"
|
||||||
|
#include "tinyfiledialogs.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
|
std::string OpenFileDialog(std::string title) {
|
||||||
|
const char* path = tinyfd_openFileDialog(title.c_str(),
|
||||||
|
"", // default path
|
||||||
|
0, // number of filters
|
||||||
|
nullptr, // filters
|
||||||
|
nullptr, // filter description
|
||||||
|
0 // no multiple selection
|
||||||
|
);
|
||||||
|
|
||||||
|
return path ? path : "";
|
||||||
|
}
|
||||||
|
|
||||||
int CountDigits(int number) {
|
int CountDigits(int number) {
|
||||||
if (number == 0) return 1;
|
if (number == 0) return 1;
|
||||||
return (int)std::floor(std::log10(std::abs(number))) + 1;
|
return (int)std::floor(std::log10(std::abs(number))) + 1;
|
||||||
@@ -41,14 +63,14 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
|||||||
if (props->control_bar_expand_) {
|
if (props->control_bar_expand_) {
|
||||||
ImGui::SetCursorPosX(props->is_control_bar_in_left_
|
ImGui::SetCursorPosX(props->is_control_bar_in_left_
|
||||||
? props->control_window_width_ * 1.03f
|
? props->control_window_width_ * 1.03f
|
||||||
: props->control_window_width_ * 0.2f);
|
: props->control_window_width_ * 0.17f);
|
||||||
|
|
||||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||||
if (!props->is_control_bar_in_left_) {
|
if (!props->is_control_bar_in_left_) {
|
||||||
draw_list->AddLine(
|
draw_list->AddLine(
|
||||||
ImVec2(ImGui::GetCursorScreenPos().x - button_height * 0.56f,
|
ImVec2(ImGui::GetCursorScreenPos().x - button_height * 0.5f,
|
||||||
ImGui::GetCursorScreenPos().y + button_height * 0.2f),
|
ImGui::GetCursorScreenPos().y + button_height * 0.2f),
|
||||||
ImVec2(ImGui::GetCursorScreenPos().x - button_height * 0.56f,
|
ImVec2(ImGui::GetCursorScreenPos().x - button_height * 0.5f,
|
||||||
ImGui::GetCursorScreenPos().y + button_height * 0.8f),
|
ImGui::GetCursorScreenPos().y + button_height * 0.8f),
|
||||||
IM_COL32(178, 178, 178, 255), 2.0f);
|
IM_COL32(178, 178, 178, 255), 2.0f);
|
||||||
}
|
}
|
||||||
@@ -175,6 +197,156 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
|||||||
line_thickness);
|
line_thickness);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
std::string open_folder = ICON_FA_FOLDER_OPEN;
|
||||||
|
if (ImGui::Button(open_folder.c_str(),
|
||||||
|
ImVec2(button_width, button_height))) {
|
||||||
|
std::string title =
|
||||||
|
localization::select_file[localization_language_index_];
|
||||||
|
std::string path = OpenFileDialog(title);
|
||||||
|
if (!path.empty()) {
|
||||||
|
LOG_INFO("Selected file: {}", path.c_str());
|
||||||
|
|
||||||
|
// Send selected file over file data channel in a background thread.
|
||||||
|
auto peer = props->peer_;
|
||||||
|
props->file_sending_ = true;
|
||||||
|
std::filesystem::path file_path = std::filesystem::path(path);
|
||||||
|
std::string file_label = file_label_;
|
||||||
|
auto props_weak = std::weak_ptr<SubStreamWindowProperties>(props);
|
||||||
|
|
||||||
|
std::thread([peer, file_path, file_label, props_weak]() {
|
||||||
|
auto props_locked = props_weak.lock();
|
||||||
|
if (!props_locked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize file transfer progress
|
||||||
|
std::error_code ec;
|
||||||
|
uint64_t total_size = std::filesystem::file_size(file_path, ec);
|
||||||
|
if (ec) {
|
||||||
|
LOG_ERROR("Failed to get file size: {}", ec.message().c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set file transfer status (atomic variables don't need mutex)
|
||||||
|
props_locked->file_sending_ = true;
|
||||||
|
props_locked->file_sent_bytes_ = 0;
|
||||||
|
props_locked->file_total_bytes_ = total_size;
|
||||||
|
props_locked->file_send_rate_bps_ = 0;
|
||||||
|
props_locked->file_transfer_window_visible_ = true;
|
||||||
|
props_locked->file_transfer_completed_ = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(
|
||||||
|
props_locked->file_transfer_mutex_);
|
||||||
|
props_locked->file_sending_name_ = file_path.filename().string();
|
||||||
|
}
|
||||||
|
props_locked->file_send_start_time_ =
|
||||||
|
std::chrono::steady_clock::now();
|
||||||
|
props_locked->file_send_last_update_time_ =
|
||||||
|
props_locked->file_send_start_time_;
|
||||||
|
props_locked->file_send_last_bytes_ = 0;
|
||||||
|
|
||||||
|
LOG_INFO(
|
||||||
|
"File transfer started: {} ({} bytes), file_sending_={}, "
|
||||||
|
"total_bytes_={}",
|
||||||
|
file_path.filename().string(), total_size,
|
||||||
|
props_locked->file_sending_.load(),
|
||||||
|
props_locked->file_total_bytes_.load());
|
||||||
|
|
||||||
|
FileSender sender;
|
||||||
|
auto last_progress_update = std::chrono::steady_clock::now();
|
||||||
|
auto last_rate_update = std::chrono::steady_clock::now();
|
||||||
|
uint64_t last_actual_sent_bytes = 0;
|
||||||
|
|
||||||
|
int ret = sender.SendFile(
|
||||||
|
file_path, file_path.filename().string(),
|
||||||
|
[peer, file_label, props_weak, &last_progress_update,
|
||||||
|
&last_rate_update, &last_actual_sent_bytes,
|
||||||
|
total_size](const char* buf, size_t sz) -> int {
|
||||||
|
int send_ret =
|
||||||
|
SendReliableDataFrame(peer, buf, sz, file_label.c_str());
|
||||||
|
if (send_ret == 0) {
|
||||||
|
// Update progress periodically (every 50ms) by querying
|
||||||
|
// actual sent bytes
|
||||||
|
auto now = std::chrono::steady_clock::now();
|
||||||
|
auto elapsed_progress =
|
||||||
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
now - last_progress_update)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
if (elapsed_progress >= 50) {
|
||||||
|
// Query actual sent bytes from the transport layer
|
||||||
|
uint64_t actual_sent_bytes =
|
||||||
|
GetDataChannelSentBytes(peer, file_label.c_str());
|
||||||
|
|
||||||
|
auto props_locked = props_weak.lock();
|
||||||
|
if (props_locked) {
|
||||||
|
props_locked->file_sent_bytes_ = actual_sent_bytes;
|
||||||
|
|
||||||
|
// Update rate every 100ms
|
||||||
|
auto elapsed_rate =
|
||||||
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
now - last_rate_update)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
if (elapsed_rate >= 100) {
|
||||||
|
std::lock_guard<std::mutex> lock(
|
||||||
|
props_locked->file_transfer_mutex_);
|
||||||
|
uint64_t bytes_sent_since_last =
|
||||||
|
actual_sent_bytes - last_actual_sent_bytes;
|
||||||
|
// Calculate rate in bits per second
|
||||||
|
uint32_t rate_bps = static_cast<uint32_t>(
|
||||||
|
(bytes_sent_since_last * 8 * 1000) / elapsed_rate);
|
||||||
|
props_locked->file_send_rate_bps_ = rate_bps;
|
||||||
|
last_actual_sent_bytes = actual_sent_bytes;
|
||||||
|
last_rate_update = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last_progress_update = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return send_ret;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset file transfer progress and show completion
|
||||||
|
auto props_locked_final = props_weak.lock();
|
||||||
|
if (props_locked_final) {
|
||||||
|
// Reset atomic variables first
|
||||||
|
props_locked_final->file_sending_ = false;
|
||||||
|
props_locked_final->file_sent_bytes_ = 0;
|
||||||
|
props_locked_final->file_total_bytes_ = 0;
|
||||||
|
props_locked_final->file_send_rate_bps_ = 0;
|
||||||
|
|
||||||
|
// Show completion window
|
||||||
|
if (ret == 0) {
|
||||||
|
props_locked_final->file_transfer_completed_ = true;
|
||||||
|
props_locked_final->file_transfer_window_visible_ = true;
|
||||||
|
} else {
|
||||||
|
props_locked_final->file_transfer_completed_ = false;
|
||||||
|
props_locked_final->file_transfer_window_visible_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(
|
||||||
|
props_locked_final->file_transfer_mutex_);
|
||||||
|
// Keep file name for completion message
|
||||||
|
if (ret != 0) {
|
||||||
|
props_locked_final->file_sending_name_ = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOG_INFO("File transfer progress reset, completed={}", ret == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ret != 0) {
|
||||||
|
LOG_ERROR("FileSender::SendFile failed for [{}], ret={}",
|
||||||
|
file_path.string().c_str(), ret);
|
||||||
|
} else {
|
||||||
|
LOG_INFO("File send finished: {}", file_path.string().c_str());
|
||||||
|
}
|
||||||
|
}).detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
// net traffic stats button
|
// net traffic stats button
|
||||||
bool button_color_style_pushed = false;
|
bool button_color_style_pushed = false;
|
||||||
@@ -238,9 +410,9 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
|||||||
|
|
||||||
if (props->is_control_bar_in_left_) {
|
if (props->is_control_bar_in_left_) {
|
||||||
draw_list->AddLine(
|
draw_list->AddLine(
|
||||||
ImVec2(ImGui::GetCursorScreenPos().x + button_height * 0.2f,
|
ImVec2(ImGui::GetCursorScreenPos().x + button_height * 0.13f,
|
||||||
ImGui::GetCursorScreenPos().y + button_height * 0.2f),
|
ImGui::GetCursorScreenPos().y + button_height * 0.2f),
|
||||||
ImVec2(ImGui::GetCursorScreenPos().x + button_height * 0.2f,
|
ImVec2(ImGui::GetCursorScreenPos().x + button_height * 0.13f,
|
||||||
ImGui::GetCursorScreenPos().y + button_height * 0.8f),
|
ImGui::GetCursorScreenPos().y + button_height * 0.8f),
|
||||||
IM_COL32(178, 178, 178, 255), 2.0f);
|
IM_COL32(178, 178, 178, 255), 2.0f);
|
||||||
}
|
}
|
||||||
@@ -250,7 +422,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
|||||||
|
|
||||||
float expand_button_pos_x =
|
float expand_button_pos_x =
|
||||||
props->control_bar_expand_ ? (props->is_control_bar_in_left_
|
props->control_bar_expand_ ? (props->is_control_bar_in_left_
|
||||||
? props->control_window_width_ * 1.91f
|
? props->control_window_width_ * 1.917f
|
||||||
: props->control_window_width_ * 0.03f)
|
: props->control_window_width_ * 0.03f)
|
||||||
: (props->is_control_bar_in_left_
|
: (props->is_control_bar_in_left_
|
||||||
? props->control_window_width_ * 1.02f
|
? props->control_window_width_ * 1.02f
|
||||||
@@ -278,6 +450,16 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
|||||||
|
|
||||||
if (props->net_traffic_stats_button_pressed_ && props->control_bar_expand_) {
|
if (props->net_traffic_stats_button_pressed_ && props->control_bar_expand_) {
|
||||||
NetTrafficStats(props);
|
NetTrafficStats(props);
|
||||||
|
} else {
|
||||||
|
// Debug: log why NetTrafficStats is not being called
|
||||||
|
static bool logged_once = false;
|
||||||
|
if (!logged_once && props->file_sending_.load()) {
|
||||||
|
LOG_INFO(
|
||||||
|
"NetTrafficStats not called: button_pressed={}, "
|
||||||
|
"control_bar_expand={}",
|
||||||
|
props->net_traffic_stats_button_pressed_, props->control_bar_expand_);
|
||||||
|
logged_once = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
|
|||||||
169
src/gui/windows/file_transfer_window.cpp
Normal file
169
src/gui/windows/file_transfer_window.cpp
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#include "IconsFontAwesome6.h"
|
||||||
|
#include "layout.h"
|
||||||
|
#include "localization.h"
|
||||||
|
#include "rd_log.h"
|
||||||
|
#include "render.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
int CountDigits(int number) {
|
||||||
|
if (number == 0) return 1;
|
||||||
|
return (int)std::floor(std::log10(std::abs(number))) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int BitrateDisplay(int bitrate) {
|
||||||
|
int num_of_digits = CountDigits(bitrate);
|
||||||
|
if (num_of_digits <= 3) {
|
||||||
|
ImGui::Text("%d bps", bitrate);
|
||||||
|
} else if (num_of_digits > 3 && num_of_digits <= 6) {
|
||||||
|
ImGui::Text("%d kbps", bitrate / 1000);
|
||||||
|
} else {
|
||||||
|
ImGui::Text("%.1f mbps", bitrate / 1000000.0f);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int Render::FileTransferWindow(
|
||||||
|
std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||||
|
if (!props->file_transfer_window_visible_ &&
|
||||||
|
!props->file_transfer_completed_) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
|
||||||
|
// Position window at bottom-left of stream window
|
||||||
|
float window_width = 400.0f;
|
||||||
|
float window_height = 120.0f;
|
||||||
|
float pos_x = 10.0f;
|
||||||
|
float pos_y = 10.0f;
|
||||||
|
|
||||||
|
// Get stream window size and position
|
||||||
|
ImVec2 stream_window_size =
|
||||||
|
ImVec2(stream_window_width_, stream_window_height_);
|
||||||
|
if (fullscreen_button_pressed_) {
|
||||||
|
pos_y = stream_window_size.y - window_height - 10.0f;
|
||||||
|
} else {
|
||||||
|
pos_y = stream_window_size.y - window_height - 10.0f - title_bar_height_;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetNextWindowPos(ImVec2(pos_x, pos_y), ImGuiCond_Always);
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(window_width, window_height),
|
||||||
|
ImGuiCond_Always);
|
||||||
|
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.95f));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.5f, 0.5f, 1.0f));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_TitleBg, ImVec4(0.3f, 0.3f, 0.3f, 1.0f));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.4f, 0.4f, 0.4f, 1.0f));
|
||||||
|
|
||||||
|
std::string window_title = "File Transfer";
|
||||||
|
bool window_opened = true;
|
||||||
|
|
||||||
|
if (ImGui::Begin("FileTransferWindow", &window_opened,
|
||||||
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
|
||||||
|
ImGuiWindowFlags_NoMove |
|
||||||
|
ImGuiWindowFlags_NoSavedSettings)) {
|
||||||
|
ImGui::PopStyleColor(4);
|
||||||
|
ImGui::PopStyleVar(2);
|
||||||
|
|
||||||
|
// Close button handling
|
||||||
|
if (!window_opened) {
|
||||||
|
props->file_transfer_window_visible_ = false;
|
||||||
|
props->file_transfer_completed_ = false;
|
||||||
|
ImGui::End();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetWindowFontScale(0.6f);
|
||||||
|
|
||||||
|
bool is_sending = props->file_sending_.load();
|
||||||
|
|
||||||
|
if (props->file_transfer_completed_ && !is_sending) {
|
||||||
|
// Show completion message
|
||||||
|
ImGui::SetCursorPos(ImVec2(10, 30));
|
||||||
|
ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f),
|
||||||
|
"%s File transfer completed!", ICON_FA_CHECK);
|
||||||
|
|
||||||
|
std::string file_name;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(props->file_transfer_mutex_);
|
||||||
|
file_name = props->file_sending_name_;
|
||||||
|
}
|
||||||
|
if (!file_name.empty()) {
|
||||||
|
ImGui::SetCursorPos(ImVec2(10, 50));
|
||||||
|
ImGui::Text("File: %s", file_name.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetCursorPos(ImVec2(10, 70));
|
||||||
|
if (ImGui::Button("OK", ImVec2(80, 25))) {
|
||||||
|
props->file_transfer_completed_ = false;
|
||||||
|
props->file_transfer_window_visible_ = false;
|
||||||
|
}
|
||||||
|
} else if (is_sending) {
|
||||||
|
// Show transfer progress
|
||||||
|
uint64_t sent = props->file_sent_bytes_.load();
|
||||||
|
uint64_t total = props->file_total_bytes_.load();
|
||||||
|
float progress =
|
||||||
|
total > 0 ? static_cast<float>(sent) / static_cast<float>(total)
|
||||||
|
: 0.0f;
|
||||||
|
progress = (std::max)(0.0f, (std::min)(1.0f, progress));
|
||||||
|
|
||||||
|
// File name
|
||||||
|
std::string file_name;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(props->file_transfer_mutex_);
|
||||||
|
file_name = props->file_sending_name_;
|
||||||
|
}
|
||||||
|
if (file_name.empty()) {
|
||||||
|
file_name = "Sending...";
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetCursorPos(ImVec2(10, 30));
|
||||||
|
ImGui::Text("File: %s", file_name.c_str());
|
||||||
|
|
||||||
|
// Progress bar
|
||||||
|
ImGui::SetCursorPos(ImVec2(10, 50));
|
||||||
|
ImGui::ProgressBar(progress, ImVec2(window_width - 40, 0), "");
|
||||||
|
ImGui::SameLine(0, 5);
|
||||||
|
ImGui::Text("%.1f%%", progress * 100.0f);
|
||||||
|
|
||||||
|
// Transfer rate and size info
|
||||||
|
ImGui::SetCursorPos(ImVec2(10, 75));
|
||||||
|
uint32_t rate_bps = props->file_send_rate_bps_.load();
|
||||||
|
ImGui::Text("Speed: ");
|
||||||
|
ImGui::SameLine();
|
||||||
|
BitrateDisplay(static_cast<int>(rate_bps));
|
||||||
|
|
||||||
|
ImGui::SameLine(0, 20);
|
||||||
|
// Format size display
|
||||||
|
char size_str[64];
|
||||||
|
if (total < 1024) {
|
||||||
|
snprintf(size_str, sizeof(size_str), "%llu B",
|
||||||
|
(unsigned long long)total);
|
||||||
|
} else if (total < 1024 * 1024) {
|
||||||
|
snprintf(size_str, sizeof(size_str), "%.2f KB", total / 1024.0f);
|
||||||
|
} else {
|
||||||
|
snprintf(size_str, sizeof(size_str), "%.2f MB",
|
||||||
|
total / (1024.0f * 1024.0f));
|
||||||
|
}
|
||||||
|
ImGui::Text("Size: %s", size_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetWindowFontScale(1.0f);
|
||||||
|
ImGui::End();
|
||||||
|
} else {
|
||||||
|
ImGui::PopStyleColor(4);
|
||||||
|
ImGui::PopStyleVar(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
@@ -230,6 +230,7 @@ int Render::SelfHostedServerWindow() {
|
|||||||
|
|
||||||
// ShowSimpleFileBrowser();
|
// ShowSimpleFileBrowser();
|
||||||
// }
|
// }
|
||||||
|
|
||||||
{
|
{
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
if (ImGui::Button(localization::reset_cert_fingerprint
|
if (ImGui::Button(localization::reset_cert_fingerprint
|
||||||
|
|||||||
@@ -139,6 +139,9 @@ int Render::StreamWindow() {
|
|||||||
|
|
||||||
ControlWindow(props);
|
ControlWindow(props);
|
||||||
|
|
||||||
|
// Show file transfer window if needed
|
||||||
|
FileTransferWindow(props);
|
||||||
|
|
||||||
focused_remote_id_ = props->remote_id_;
|
focused_remote_id_ = props->remote_id_;
|
||||||
|
|
||||||
if (!props->peer_) {
|
if (!props->peer_) {
|
||||||
@@ -233,6 +236,10 @@ int Render::StreamWindow() {
|
|||||||
UpdateRenderRect();
|
UpdateRenderRect();
|
||||||
|
|
||||||
ControlWindow(props);
|
ControlWindow(props);
|
||||||
|
|
||||||
|
// Show file transfer window if needed
|
||||||
|
FileTransferWindow(props);
|
||||||
|
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
|
|
||||||
if (!props->peer_) {
|
if (!props->peer_) {
|
||||||
|
|||||||
288
src/tools/file_transfer.cpp
Normal file
288
src/tools/file_transfer.cpp
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
#include "file_transfer.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
#include "rd_log.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::atomic<uint32_t> g_next_file_id{1};
|
||||||
|
constexpr uint32_t kFileChunkMagic = 0x4A4E544D; // 'JNTM'
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
uint32_t FileSender::NextFileId() { return g_next_file_id.fetch_add(1); }
|
||||||
|
|
||||||
|
int FileSender::SendFile(const std::filesystem::path& path,
|
||||||
|
const std::string& label, const SendFunc& send,
|
||||||
|
std::size_t chunk_size) {
|
||||||
|
if (!send) {
|
||||||
|
LOG_ERROR("FileSender::SendFile: send function is empty");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
if (!std::filesystem::exists(path, ec) ||
|
||||||
|
!std::filesystem::is_regular_file(path, ec)) {
|
||||||
|
LOG_ERROR("FileSender::SendFile: file [{}] not found or not regular",
|
||||||
|
path.string().c_str());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t total_size = std::filesystem::file_size(path, ec);
|
||||||
|
if (ec) {
|
||||||
|
LOG_ERROR("FileSender::SendFile: failed to get size of [{}]: {}",
|
||||||
|
path.string().c_str(), ec.message().c_str());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ifstream ifs(path, std::ios::binary);
|
||||||
|
if (!ifs.is_open()) {
|
||||||
|
LOG_ERROR("FileSender::SendFile: failed to open [{}]",
|
||||||
|
path.string().c_str());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
LOG_INFO("FileSender send file {}, total size {}", path.string().c_str(),
|
||||||
|
total_size);
|
||||||
|
|
||||||
|
const uint32_t file_id = NextFileId();
|
||||||
|
uint64_t offset = 0;
|
||||||
|
bool is_first = true;
|
||||||
|
std::string file_name = label.empty() ? path.filename().string() : label;
|
||||||
|
|
||||||
|
std::vector<char> buffer;
|
||||||
|
buffer.resize(chunk_size);
|
||||||
|
|
||||||
|
while (ifs && offset < total_size) {
|
||||||
|
uint64_t remaining = total_size - offset;
|
||||||
|
uint32_t to_read =
|
||||||
|
static_cast<uint32_t>(std::min<uint64_t>(remaining, chunk_size));
|
||||||
|
|
||||||
|
ifs.read(buffer.data(), static_cast<std::streamsize>(to_read));
|
||||||
|
std::streamsize bytes_read = ifs.gcount();
|
||||||
|
if (bytes_read <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_last = (offset + static_cast<uint64_t>(bytes_read) >= total_size);
|
||||||
|
const std::string* name_ptr = is_first ? &file_name : nullptr;
|
||||||
|
|
||||||
|
std::vector<char> chunk = BuildChunk(
|
||||||
|
file_id, offset, total_size, buffer.data(),
|
||||||
|
static_cast<uint32_t>(bytes_read), name_ptr, is_first, is_last);
|
||||||
|
|
||||||
|
int ret = send(chunk.data(), chunk.size());
|
||||||
|
if (ret != 0) {
|
||||||
|
LOG_ERROR("FileSender::SendFile: send failed for [{}], ret={}",
|
||||||
|
path.string().c_str(), ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += static_cast<uint64_t>(bytes_read);
|
||||||
|
is_first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<char> FileSender::BuildChunk(uint32_t file_id, uint64_t offset,
|
||||||
|
uint64_t total_size, const char* data,
|
||||||
|
uint32_t data_size,
|
||||||
|
const std::string* file_name,
|
||||||
|
bool is_first, bool is_last) {
|
||||||
|
FileChunkHeader header{};
|
||||||
|
header.magic = kFileChunkMagic;
|
||||||
|
header.file_id = file_id;
|
||||||
|
header.offset = offset;
|
||||||
|
header.total_size = total_size;
|
||||||
|
header.chunk_size = data_size;
|
||||||
|
header.name_len =
|
||||||
|
(file_name && is_first) ? static_cast<uint16_t>(file_name->size()) : 0;
|
||||||
|
header.flags = 0;
|
||||||
|
if (is_first) header.flags |= 0x01;
|
||||||
|
if (is_last) header.flags |= 0x02;
|
||||||
|
|
||||||
|
std::size_t total_size_bytes =
|
||||||
|
sizeof(FileChunkHeader) + header.name_len + header.chunk_size;
|
||||||
|
|
||||||
|
std::vector<char> buffer;
|
||||||
|
buffer.resize(total_size_bytes);
|
||||||
|
|
||||||
|
std::size_t offset_bytes = 0;
|
||||||
|
memcpy(buffer.data() + offset_bytes, &header, sizeof(FileChunkHeader));
|
||||||
|
offset_bytes += sizeof(FileChunkHeader);
|
||||||
|
|
||||||
|
if (header.name_len > 0 && file_name) {
|
||||||
|
memcpy(buffer.data() + offset_bytes, file_name->data(), header.name_len);
|
||||||
|
offset_bytes += header.name_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.chunk_size > 0 && data) {
|
||||||
|
memcpy(buffer.data() + offset_bytes, data, header.chunk_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- FileReceiver ----------
|
||||||
|
|
||||||
|
FileReceiver::FileReceiver() : output_dir_(GetDefaultDesktopPath()) {}
|
||||||
|
|
||||||
|
FileReceiver::FileReceiver(const std::filesystem::path& output_dir)
|
||||||
|
: output_dir_(output_dir) {
|
||||||
|
std::error_code ec;
|
||||||
|
if (!output_dir_.empty()) {
|
||||||
|
std::filesystem::create_directories(output_dir_, ec);
|
||||||
|
if (ec) {
|
||||||
|
LOG_ERROR("FileReceiver: failed to create output dir [{}]: {}",
|
||||||
|
output_dir_.string().c_str(), ec.message().c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path FileReceiver::GetDefaultDesktopPath() {
|
||||||
|
#ifdef _WIN32
|
||||||
|
const char* home_env = std::getenv("USERPROFILE");
|
||||||
|
#else
|
||||||
|
const char* home_env = std::getenv("HOME");
|
||||||
|
#endif
|
||||||
|
if (!home_env) {
|
||||||
|
return std::filesystem::path{};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path desktop_path =
|
||||||
|
std::filesystem::path(home_env) / "Desktop";
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::create_directories(desktop_path, ec);
|
||||||
|
if (ec) {
|
||||||
|
LOG_ERROR("FileReceiver: failed to create desktop directory [{}]: {}",
|
||||||
|
desktop_path.string().c_str(), ec.message().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
return desktop_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FileReceiver::OnData(const char* data, size_t size) {
|
||||||
|
if (!data || size < sizeof(FileChunkHeader)) {
|
||||||
|
LOG_ERROR("FileReceiver::OnData: invalid buffer");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileChunkHeader header{};
|
||||||
|
memcpy(&header, data, sizeof(FileChunkHeader));
|
||||||
|
|
||||||
|
if (header.magic != kFileChunkMagic) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t header_and_name =
|
||||||
|
sizeof(FileChunkHeader) + static_cast<std::size_t>(header.name_len);
|
||||||
|
if (size < header_and_name ||
|
||||||
|
size < header_and_name + static_cast<std::size_t>(header.chunk_size)) {
|
||||||
|
LOG_ERROR("FileReceiver::OnData: buffer too small for header + payload");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* name_ptr = data + sizeof(FileChunkHeader);
|
||||||
|
std::string file_name;
|
||||||
|
const std::string* file_name_ptr = nullptr;
|
||||||
|
if (header.name_len > 0) {
|
||||||
|
file_name.assign(name_ptr,
|
||||||
|
name_ptr + static_cast<std::size_t>(header.name_len));
|
||||||
|
file_name_ptr = &file_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* payload = data + header_and_name;
|
||||||
|
std::size_t payload_size =
|
||||||
|
static_cast<std::size_t>(header.chunk_size); // may be 0
|
||||||
|
|
||||||
|
return HandleChunk(header, payload, payload_size, file_name_ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FileReceiver::HandleChunk(const FileChunkHeader& header,
|
||||||
|
const char* payload, size_t payload_size,
|
||||||
|
const std::string* file_name) {
|
||||||
|
auto it = contexts_.find(header.file_id);
|
||||||
|
if (it == contexts_.end()) {
|
||||||
|
// new file context must start with first chunk.
|
||||||
|
if ((header.flags & 0x01) == 0) {
|
||||||
|
LOG_ERROR("FileReceiver: received non-first chunk for unknown file_id={}",
|
||||||
|
header.file_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileContext ctx;
|
||||||
|
ctx.total_size = header.total_size;
|
||||||
|
|
||||||
|
std::string filename;
|
||||||
|
if (file_name && !file_name->empty()) {
|
||||||
|
filename = *file_name;
|
||||||
|
} else {
|
||||||
|
filename = "received_" + std::to_string(header.file_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.file_name = filename;
|
||||||
|
|
||||||
|
std::filesystem::path save_path = output_dir_.empty()
|
||||||
|
? std::filesystem::path(filename)
|
||||||
|
: output_dir_ / filename;
|
||||||
|
|
||||||
|
// if file exists, append timestamp.
|
||||||
|
std::error_code ec;
|
||||||
|
if (std::filesystem::exists(save_path, ec)) {
|
||||||
|
auto now = std::chrono::system_clock::now();
|
||||||
|
auto ts = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
now.time_since_epoch())
|
||||||
|
.count();
|
||||||
|
save_path = save_path.parent_path() /
|
||||||
|
(save_path.stem().string() + "_" + std::to_string(ts) +
|
||||||
|
save_path.extension().string());
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.ofs.open(save_path, std::ios::binary | std::ios::trunc);
|
||||||
|
if (!ctx.ofs.is_open()) {
|
||||||
|
LOG_ERROR("FileReceiver: failed to open [{}] for writing",
|
||||||
|
save_path.string().c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
contexts_.emplace(header.file_id, std::move(ctx));
|
||||||
|
it = contexts_.find(header.file_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileContext& ctx = it->second;
|
||||||
|
|
||||||
|
if (payload_size > 0 && payload) {
|
||||||
|
ctx.ofs.seekp(static_cast<std::streamoff>(header.offset), std::ios::beg);
|
||||||
|
ctx.ofs.write(payload, static_cast<std::streamsize>(payload_size));
|
||||||
|
if (!ctx.ofs.good()) {
|
||||||
|
LOG_ERROR("FileReceiver: write failed for file_id={}", header.file_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ctx.received += static_cast<uint64_t>(payload_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_last = (header.flags & 0x02) != 0;
|
||||||
|
if (is_last || ctx.received >= ctx.total_size) {
|
||||||
|
ctx.ofs.close();
|
||||||
|
|
||||||
|
std::filesystem::path saved_path =
|
||||||
|
output_dir_.empty() ? std::filesystem::path(ctx.file_name)
|
||||||
|
: output_dir_ / ctx.file_name;
|
||||||
|
|
||||||
|
LOG_INFO("FileReceiver: file received complete, file_id={}, size={}",
|
||||||
|
header.file_id, ctx.received);
|
||||||
|
|
||||||
|
if (on_file_complete_) {
|
||||||
|
on_file_complete_(saved_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
contexts_.erase(header.file_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
99
src/tools/file_transfer.h
Normal file
99
src/tools/file_transfer.h
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* @Author: DI JUNKUN
|
||||||
|
* @Date: 2025-12-18
|
||||||
|
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _FILE_TRANSFER_H_
|
||||||
|
#define _FILE_TRANSFER_H_
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
struct FileChunkHeader {
|
||||||
|
uint32_t magic; // magic to identify file-transfer chunks
|
||||||
|
uint32_t file_id; // unique id per file transfer
|
||||||
|
uint64_t offset; // offset in file
|
||||||
|
uint64_t total_size; // total file size
|
||||||
|
uint32_t chunk_size; // payload size in this chunk
|
||||||
|
uint16_t name_len; // filename length (bytes), only set on first chunk
|
||||||
|
uint8_t flags; // bit0: is_first, bit1: is_last, others reserved
|
||||||
|
};
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
class FileSender {
|
||||||
|
public:
|
||||||
|
using SendFunc = std::function<int(const char* data, size_t size)>;
|
||||||
|
|
||||||
|
public:
|
||||||
|
FileSender() = default;
|
||||||
|
|
||||||
|
// generate a new file id
|
||||||
|
static uint32_t NextFileId();
|
||||||
|
|
||||||
|
// synchronously send a file using the provided send function.
|
||||||
|
// `path` : full path to the local file.
|
||||||
|
// `label` : logical filename to send (usually path.filename()).
|
||||||
|
// `send` : callback that pushes one encoded chunk into the data channel.
|
||||||
|
// Return 0 on success, <0 on error.
|
||||||
|
int SendFile(const std::filesystem::path& path, const std::string& label,
|
||||||
|
const SendFunc& send, std::size_t chunk_size = 64 * 1024);
|
||||||
|
|
||||||
|
// build a single encoded chunk buffer according to FileChunkHeader protocol.
|
||||||
|
static std::vector<char> BuildChunk(uint32_t file_id, uint64_t offset,
|
||||||
|
uint64_t total_size, const char* data,
|
||||||
|
uint32_t data_size,
|
||||||
|
const std::string* file_name,
|
||||||
|
bool is_first, bool is_last);
|
||||||
|
};
|
||||||
|
|
||||||
|
class FileReceiver {
|
||||||
|
public:
|
||||||
|
struct FileContext {
|
||||||
|
std::string file_name;
|
||||||
|
uint64_t total_size = 0;
|
||||||
|
uint64_t received = 0;
|
||||||
|
std::ofstream ofs;
|
||||||
|
};
|
||||||
|
|
||||||
|
using OnFileComplete =
|
||||||
|
std::function<void(const std::filesystem::path& saved_path)>;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// save to default desktop directory.
|
||||||
|
FileReceiver();
|
||||||
|
|
||||||
|
// save to a specified directory.
|
||||||
|
explicit FileReceiver(const std::filesystem::path& output_dir);
|
||||||
|
|
||||||
|
// process one received data buffer (one chunk).
|
||||||
|
// return true if parsed and processed successfully, false otherwise.
|
||||||
|
bool OnData(const char* data, size_t size);
|
||||||
|
|
||||||
|
void SetOnFileComplete(OnFileComplete cb) { on_file_complete_ = cb; }
|
||||||
|
|
||||||
|
const std::filesystem::path& OutputDir() const { return output_dir_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::filesystem::path GetDefaultDesktopPath();
|
||||||
|
|
||||||
|
bool HandleChunk(const FileChunkHeader& header, const char* payload,
|
||||||
|
size_t payload_size, const std::string* file_name);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::filesystem::path output_dir_;
|
||||||
|
std::unordered_map<uint32_t, FileContext> contexts_;
|
||||||
|
OnFileComplete on_file_complete_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif
|
||||||
Submodule submodules/minirtc updated: f9810444ee...235b86b5bf
11
xmake.lua
11
xmake.lua
@@ -33,6 +33,7 @@ add_requires("imgui v1.92.1-docking", {configs = {sdl3 = true, sdl3_renderer = t
|
|||||||
add_requires("openssl3 3.3.2", {system = false})
|
add_requires("openssl3 3.3.2", {system = false})
|
||||||
add_requires("nlohmann_json 3.11.3")
|
add_requires("nlohmann_json 3.11.3")
|
||||||
add_requires("cpp-httplib v0.26.0", {configs = {ssl = true}})
|
add_requires("cpp-httplib v0.26.0", {configs = {ssl = true}})
|
||||||
|
add_requires("tinyfiledialogs 3.15.1")
|
||||||
|
|
||||||
if is_os("windows") then
|
if is_os("windows") then
|
||||||
add_requires("libyuv", "miniaudio 0.11.21")
|
add_requires("libyuv", "miniaudio 0.11.21")
|
||||||
@@ -168,13 +169,19 @@ target("version_checker")
|
|||||||
add_files("src/version_checker/*.cpp")
|
add_files("src/version_checker/*.cpp")
|
||||||
add_includedirs("src/version_checker", {public = true})
|
add_includedirs("src/version_checker", {public = true})
|
||||||
|
|
||||||
|
target("tools")
|
||||||
|
set_kind("object")
|
||||||
|
add_deps("rd_log")
|
||||||
|
add_files("src/tools/*.cpp")
|
||||||
|
add_includedirs("src/tools", {public = true})
|
||||||
|
|
||||||
target("gui")
|
target("gui")
|
||||||
set_kind("object")
|
set_kind("object")
|
||||||
add_packages("libyuv")
|
add_packages("libyuv", "tinyfiledialogs")
|
||||||
add_defines("CROSSDESK_VERSION=\"" .. (get_config("CROSSDESK_VERSION") or "Unknown") .. "\"")
|
add_defines("CROSSDESK_VERSION=\"" .. (get_config("CROSSDESK_VERSION") or "Unknown") .. "\"")
|
||||||
add_deps("rd_log", "common", "assets", "config_center", "minirtc",
|
add_deps("rd_log", "common", "assets", "config_center", "minirtc",
|
||||||
"path_manager", "screen_capturer", "speaker_capturer",
|
"path_manager", "screen_capturer", "speaker_capturer",
|
||||||
"device_controller", "thumbnail", "version_checker")
|
"device_controller", "thumbnail", "version_checker", "tools")
|
||||||
add_files("src/gui/*.cpp", "src/gui/panels/*.cpp", "src/gui/toolbars/*.cpp",
|
add_files("src/gui/*.cpp", "src/gui/panels/*.cpp", "src/gui/toolbars/*.cpp",
|
||||||
"src/gui/windows/*.cpp")
|
"src/gui/windows/*.cpp")
|
||||||
add_includedirs("src/gui", "src/gui/panels", "src/gui/toolbars",
|
add_includedirs("src/gui", "src/gui/panels", "src/gui/toolbars",
|
||||||
|
|||||||
Reference in New Issue
Block a user