mirror of
https://github.com/kunkundi/crossdesk.git
synced 2025-12-19 05:36:32 +08:00
Compare commits
4 Commits
latest
...
file-trans
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a967dc72d7 | ||
|
|
5066fcda48 | ||
|
|
e7bdf42694 | ||
|
|
875fea88ee |
@@ -185,6 +185,8 @@ static std::vector<std::string> enable_daemon = {
|
||||
static std::vector<std::string> takes_effect_after_restart = {
|
||||
reinterpret_cast<const char*>(u8"重启后生效"),
|
||||
"Takes effect after restart"};
|
||||
static std::vector<std::string> select_file = {
|
||||
reinterpret_cast<const char*>(u8"选择文件"), "Select File"};
|
||||
#if _WIN32
|
||||
static std::vector<std::string> minimize_to_tray = {
|
||||
reinterpret_cast<const char*>(u8"退出时最小化到系统托盘:"),
|
||||
|
||||
@@ -30,6 +30,7 @@ int Render::LocalWindow() {
|
||||
ImGui::SetCursorPos(
|
||||
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.02f));
|
||||
|
||||
ImGui::SetWindowFontScale(0.9f);
|
||||
ImGui::TextColored(
|
||||
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
|
||||
localization::local_desktop[localization_language_index_].c_str());
|
||||
|
||||
@@ -26,6 +26,7 @@ int Render::RecentConnectionsWindow() {
|
||||
ImGui::SetCursorPos(
|
||||
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.02f));
|
||||
|
||||
ImGui::SetWindowFontScale(0.9f);
|
||||
ImGui::TextColored(
|
||||
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
|
||||
localization::recent_connections[localization_language_index_].c_str());
|
||||
|
||||
@@ -31,6 +31,7 @@ int Render::RemoteWindow() {
|
||||
ImGui::SetCursorPos(
|
||||
ImVec2(io.DisplaySize.x * 0.057f, io.DisplaySize.y * 0.02f));
|
||||
|
||||
ImGui::SetWindowFontScale(0.9f);
|
||||
ImGui::TextColored(
|
||||
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
|
||||
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->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_min_width_ = title_bar_height_ * 0.65f;
|
||||
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;
|
||||
|
||||
if (!props->peer_) {
|
||||
@@ -206,6 +207,7 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
|
||||
}
|
||||
AddAudioStream(props->peer_, props->audio_label_.c_str());
|
||||
AddDataStream(props->peer_, props->data_label_.c_str());
|
||||
AddDataStream(props->peer_, props->file_label_.c_str());
|
||||
|
||||
props->connection_status_ = ConnectionStatus::Connecting;
|
||||
|
||||
|
||||
@@ -714,6 +714,7 @@ int Render::CreateConnectionPeer() {
|
||||
|
||||
AddAudioStream(peer_, audio_label_.c_str());
|
||||
AddDataStream(peer_, data_label_.c_str());
|
||||
AddDataStream(peer_, file_label_.c_str());
|
||||
return 0;
|
||||
} else {
|
||||
return -1;
|
||||
@@ -1340,8 +1341,8 @@ void Render::MainLoop() {
|
||||
remote_action.i.host_name_size = host_name.size();
|
||||
|
||||
std::string msg = remote_action.to_json();
|
||||
int ret =
|
||||
SendDataFrame(peer_, msg.data(), msg.size(), data_label_.c_str());
|
||||
int ret = SendReliableDataFrame(peer_, msg.data(), msg.size(),
|
||||
data_label_.c_str());
|
||||
FreeRemoteAction(remote_action);
|
||||
if (0 == ret) {
|
||||
need_to_send_host_info_ = false;
|
||||
|
||||
@@ -45,6 +45,7 @@ class Render {
|
||||
PeerPtr* peer_ = nullptr;
|
||||
std::string audio_label_ = "control_audio";
|
||||
std::string data_label_ = "control_data";
|
||||
std::string file_label_ = "file";
|
||||
std::string local_id_ = "";
|
||||
std::string remote_id_ = "";
|
||||
bool exit_ = false;
|
||||
@@ -464,6 +465,7 @@ class Render {
|
||||
std::string video_secondary_label_ = "secondary_display";
|
||||
std::string audio_label_ = "audio";
|
||||
std::string data_label_ = "data";
|
||||
std::string file_label_ = "file";
|
||||
Params params_;
|
||||
SDL_AudioDeviceID input_dev_;
|
||||
SDL_AudioDeviceID output_dev_;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#include "device_controller.h"
|
||||
#include "file_transfer.h"
|
||||
#include "localization.h"
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
@@ -29,8 +33,8 @@ int Render::SendKeyCommand(int key_code, bool is_down) {
|
||||
if (props->connection_status_ == ConnectionStatus::Connected) {
|
||||
std::string msg = remote_action.to_json();
|
||||
if (props->peer_) {
|
||||
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->data_label_.c_str());
|
||||
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->data_label_.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,8 +104,8 @@ int Render::ProcessMouseEvent(const SDL_Event& event) {
|
||||
|
||||
std::string msg = remote_action.to_json();
|
||||
if (props->peer_) {
|
||||
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->data_label_.c_str());
|
||||
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->data_label_.c_str());
|
||||
}
|
||||
} else if (SDL_EVENT_MOUSE_WHEEL == event.type &&
|
||||
last_mouse_event.button.x >= props->stream_render_rect_.x &&
|
||||
@@ -147,8 +151,8 @@ int Render::ProcessMouseEvent(const SDL_Event& event) {
|
||||
|
||||
std::string msg = remote_action.to_json();
|
||||
if (props->peer_) {
|
||||
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->data_label_.c_str());
|
||||
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->data_label_.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,6 +308,12 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
return;
|
||||
}
|
||||
|
||||
// try to parse as file-transfer chunk first
|
||||
static FileReceiver receiver;
|
||||
if (receiver.OnData(data, size)) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string json_str(data, size);
|
||||
RemoteAction remote_action;
|
||||
|
||||
@@ -485,12 +495,12 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
render->need_to_send_host_info_ = true;
|
||||
render->start_screen_capturer_ = true;
|
||||
render->start_speaker_capturer_ = true;
|
||||
#ifdef CROSSDESK_DEBUG
|
||||
// #ifdef CROSSDESK_DEBUG
|
||||
render->start_mouse_controller_ = false;
|
||||
render->start_keyboard_capturer_ = false;
|
||||
#else
|
||||
// #else
|
||||
render->start_mouse_controller_ = true;
|
||||
#endif
|
||||
// #endif
|
||||
if (std::all_of(render->connection_status_.begin(),
|
||||
render->connection_status_.end(), [](const auto& kv) {
|
||||
return kv.first.find("web") != std::string::npos;
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "file_transfer.h"
|
||||
#include "layout.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
#include "tinyfiledialogs.h"
|
||||
|
||||
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) {
|
||||
if (number == 0) return 1;
|
||||
return (int)std::floor(std::log10(std::abs(number))) + 1;
|
||||
@@ -41,14 +60,14 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
if (props->control_bar_expand_) {
|
||||
ImGui::SetCursorPosX(props->is_control_bar_in_left_
|
||||
? props->control_window_width_ * 1.03f
|
||||
: props->control_window_width_ * 0.2f);
|
||||
: props->control_window_width_ * 0.17f);
|
||||
|
||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||
if (!props->is_control_bar_in_left_) {
|
||||
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),
|
||||
ImVec2(ImGui::GetCursorScreenPos().x - button_height * 0.56f,
|
||||
ImVec2(ImGui::GetCursorScreenPos().x - button_height * 0.5f,
|
||||
ImGui::GetCursorScreenPos().y + button_height * 0.8f),
|
||||
IM_COL32(178, 178, 178, 255), 2.0f);
|
||||
}
|
||||
@@ -73,8 +92,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
remote_action.d = i;
|
||||
if (props->connection_status_ == ConnectionStatus::Connected) {
|
||||
std::string msg = remote_action.to_json();
|
||||
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->data_label_.c_str());
|
||||
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->data_label_.c_str());
|
||||
}
|
||||
}
|
||||
props->display_selectable_hovered_ = ImGui::IsWindowHovered();
|
||||
@@ -154,8 +173,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
remote_action.type = ControlType::audio_capture;
|
||||
remote_action.a = props->audio_capture_button_pressed_;
|
||||
std::string msg = remote_action.to_json();
|
||||
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->data_label_.c_str());
|
||||
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->data_label_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +194,39 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
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_;
|
||||
std::filesystem::path file_path = std::filesystem::path(path);
|
||||
std::string file_label = file_label_;
|
||||
|
||||
std::thread([peer, file_path, file_label]() {
|
||||
FileSender sender;
|
||||
int ret = sender.SendFile(
|
||||
file_path, file_path.filename().string(),
|
||||
[peer, file_label](const char* buf, size_t sz) -> int {
|
||||
return SendReliableDataFrame(peer, buf, sz, file_label.c_str());
|
||||
});
|
||||
|
||||
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();
|
||||
// net traffic stats button
|
||||
bool button_color_style_pushed = false;
|
||||
@@ -238,9 +290,9 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
|
||||
if (props->is_control_bar_in_left_) {
|
||||
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),
|
||||
ImVec2(ImGui::GetCursorScreenPos().x + button_height * 0.2f,
|
||||
ImVec2(ImGui::GetCursorScreenPos().x + button_height * 0.13f,
|
||||
ImGui::GetCursorScreenPos().y + button_height * 0.8f),
|
||||
IM_COL32(178, 178, 178, 255), 2.0f);
|
||||
}
|
||||
@@ -250,7 +302,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
|
||||
float expand_button_pos_x =
|
||||
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->is_control_bar_in_left_
|
||||
? props->control_window_width_ * 1.02f
|
||||
|
||||
@@ -230,6 +230,7 @@ int Render::SelfHostedServerWindow() {
|
||||
|
||||
// ShowSimpleFileBrowser();
|
||||
// }
|
||||
|
||||
{
|
||||
ImGui::AlignTextToFramePadding();
|
||||
if (ImGui::Button(localization::reset_cert_fingerprint
|
||||
|
||||
286
src/tools/file_transfer.cpp
Normal file
286
src/tools/file_transfer.cpp
Normal file
@@ -0,0 +1,286 @@
|
||||
#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;
|
||||
}
|
||||
|
||||
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...c5622d7104
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("nlohmann_json 3.11.3")
|
||||
add_requires("cpp-httplib v0.26.0", {configs = {ssl = true}})
|
||||
add_requires("tinyfiledialogs 3.15.1")
|
||||
|
||||
if is_os("windows") then
|
||||
add_requires("libyuv", "miniaudio 0.11.21")
|
||||
@@ -168,13 +169,19 @@ target("version_checker")
|
||||
add_files("src/version_checker/*.cpp")
|
||||
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")
|
||||
set_kind("object")
|
||||
add_packages("libyuv")
|
||||
add_packages("libyuv", "tinyfiledialogs")
|
||||
add_defines("CROSSDESK_VERSION=\"" .. (get_config("CROSSDESK_VERSION") or "Unknown") .. "\"")
|
||||
add_deps("rd_log", "common", "assets", "config_center", "minirtc",
|
||||
"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",
|
||||
"src/gui/windows/*.cpp")
|
||||
add_includedirs("src/gui", "src/gui/panels", "src/gui/toolbars",
|
||||
|
||||
Reference in New Issue
Block a user