librats 0.7.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/lib/index.d.ts +9 -22
- package/native-src/CMakeLists.txt +132 -50
- package/native-src/cmake/ratsConfig.cmake.in +8 -0
- package/native-src/src/bt_network.cpp +296 -183
- package/native-src/src/bt_network.h +25 -5
- package/native-src/src/crypto/sha256.c +1 -1
- package/native-src/src/dht.cpp +297 -47
- package/native-src/src/dht.h +70 -6
- package/native-src/src/file_transfer.cpp +1185 -1578
- package/native-src/src/file_transfer.h +240 -521
- package/native-src/src/io_poller.cpp +917 -0
- package/native-src/src/io_poller.h +138 -0
- package/native-src/src/krpc.cpp +161 -96
- package/native-src/src/krpc.h +18 -4
- package/native-src/src/librats.cpp +812 -1236
- package/native-src/src/librats.h +207 -208
- package/native-src/src/librats_bittorrent.cpp +1 -5
- package/native-src/src/librats_c.cpp +22 -39
- package/native-src/src/librats_discovery.cpp +377 -0
- package/native-src/src/librats_encryption.cpp +130 -283
- package/native-src/src/librats_file_transfer.cpp +27 -109
- package/native-src/src/librats_gossipsub.cpp +1 -5
- package/native-src/src/librats_ice.cpp +5 -1
- package/native-src/src/librats_log_macros.h +36 -0
- package/native-src/src/librats_logging.cpp +1 -7
- package/native-src/src/librats_mdns.cpp +1 -11
- package/native-src/src/librats_persistence.cpp +1 -11
- package/native-src/src/librats_reconnection.cpp +2 -13
- package/native-src/src/librats_statistic.cpp +105 -0
- package/native-src/src/socket.cpp +15 -3
- package/package.json +1 -1
- package/scripts/build-librats.js +3 -0
- package/scripts/prepare-package.js +10 -0
- package/src/librats_node.cpp +53 -64
|
@@ -2,1807 +2,1414 @@
|
|
|
2
2
|
#include "librats.h"
|
|
3
3
|
#include "fs.h"
|
|
4
4
|
#include "logger.h"
|
|
5
|
-
#include "
|
|
5
|
+
#include "crypto/sha256.h"
|
|
6
6
|
|
|
7
|
-
// Define logging module for this file
|
|
8
|
-
#define LOG_FILE_TRANSFER_INFO(message) LOG_INFO("filetransfer", message)
|
|
9
|
-
#define LOG_FILE_TRANSFER_ERROR(message) LOG_ERROR("filetransfer", message)
|
|
10
|
-
#define LOG_FILE_TRANSFER_WARN(message) LOG_WARN("filetransfer", message)
|
|
11
|
-
#define LOG_FILE_TRANSFER_DEBUG(message) LOG_DEBUG("filetransfer", message)
|
|
12
7
|
#include <algorithm>
|
|
13
|
-
#include <random>
|
|
14
|
-
#include <iomanip>
|
|
15
|
-
#include <sstream>
|
|
16
8
|
#include <cstring>
|
|
17
|
-
#include <
|
|
18
|
-
|
|
19
|
-
// Optional compression support
|
|
20
|
-
#ifdef LIBRATS_ENABLE_ZLIB
|
|
21
|
-
#include <zlib.h>
|
|
22
|
-
#endif
|
|
9
|
+
#include <random>
|
|
23
10
|
|
|
24
|
-
|
|
25
|
-
#
|
|
26
|
-
#
|
|
11
|
+
// Logging shorthand for this module.
|
|
12
|
+
#define LOG_FT_INFO(msg) LOG_INFO("filetransfer", msg)
|
|
13
|
+
#define LOG_FT_WARN(msg) LOG_WARN("filetransfer", msg)
|
|
14
|
+
#define LOG_FT_ERROR(msg) LOG_ERROR("filetransfer", msg)
|
|
15
|
+
#define LOG_FT_DEBUG(msg) LOG_DEBUG("filetransfer", msg)
|
|
27
16
|
|
|
28
17
|
namespace librats {
|
|
29
18
|
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Wire constants
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
namespace {
|
|
24
|
+
|
|
25
|
+
// Named control-message types exchanged on the JSON channel.
|
|
26
|
+
constexpr const char* MSG_OFFER = "ft_offer";
|
|
27
|
+
constexpr const char* MSG_RESPONSE = "ft_response";
|
|
28
|
+
constexpr const char* MSG_FILE_END = "ft_file_end";
|
|
29
|
+
constexpr const char* MSG_PROGRESS = "ft_progress";
|
|
30
|
+
constexpr const char* MSG_COMPLETE = "ft_complete";
|
|
31
|
+
constexpr const char* MSG_CONTROL = "ft_control";
|
|
32
|
+
|
|
33
|
+
// Magic prefix of a binary chunk frame.
|
|
34
|
+
constexpr char CHUNK_MAGIC[4] = {'R', 'F', 'T', '1'};
|
|
35
|
+
|
|
36
|
+
// How long a finished transfer is kept queryable before being purged.
|
|
37
|
+
constexpr auto FINISHED_RETENTION = std::chrono::minutes(5);
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// Small helpers
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
// --- big-endian serialization ---
|
|
44
|
+
void put_u16(std::vector<uint8_t>& b, uint16_t v) {
|
|
45
|
+
b.push_back(uint8_t(v >> 8));
|
|
46
|
+
b.push_back(uint8_t(v));
|
|
47
|
+
}
|
|
48
|
+
void put_u32(std::vector<uint8_t>& b, uint32_t v) {
|
|
49
|
+
b.push_back(uint8_t(v >> 24));
|
|
50
|
+
b.push_back(uint8_t(v >> 16));
|
|
51
|
+
b.push_back(uint8_t(v >> 8));
|
|
52
|
+
b.push_back(uint8_t(v));
|
|
53
|
+
}
|
|
54
|
+
void put_u64(std::vector<uint8_t>& b, uint64_t v) {
|
|
55
|
+
for (int s = 56; s >= 0; s -= 8) b.push_back(uint8_t(v >> s));
|
|
56
|
+
}
|
|
57
|
+
uint16_t get_u16(const uint8_t* p) { return uint16_t(p[0]) << 8 | p[1]; }
|
|
58
|
+
uint32_t get_u32(const uint8_t* p) {
|
|
59
|
+
return uint32_t(p[0]) << 24 | uint32_t(p[1]) << 16 | uint32_t(p[2]) << 8 | p[3];
|
|
60
|
+
}
|
|
61
|
+
uint64_t get_u64(const uint8_t* p) {
|
|
62
|
+
uint64_t v = 0;
|
|
63
|
+
for (int i = 0; i < 8; ++i) v = (v << 8) | p[i];
|
|
64
|
+
return v;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- CRC32 (IEEE 802.3) for per-chunk integrity ---
|
|
68
|
+
struct Crc32Table {
|
|
69
|
+
uint32_t t[256];
|
|
70
|
+
Crc32Table() {
|
|
71
|
+
for (uint32_t i = 0; i < 256; ++i) {
|
|
72
|
+
uint32_t c = i;
|
|
73
|
+
for (int k = 0; k < 8; ++k) c = (c & 1) ? (0xEDB88320u ^ (c >> 1)) : (c >> 1);
|
|
74
|
+
t[i] = c;
|
|
75
|
+
}
|
|
45
76
|
}
|
|
46
|
-
|
|
47
|
-
|
|
77
|
+
};
|
|
78
|
+
uint32_t crc32(const uint8_t* data, size_t len) {
|
|
79
|
+
static const Crc32Table tbl;
|
|
80
|
+
uint32_t c = 0xFFFFFFFFu;
|
|
81
|
+
for (size_t i = 0; i < len; ++i) c = tbl.t[(c ^ data[i]) & 0xFF] ^ (c >> 8);
|
|
82
|
+
return c ^ 0xFFFFFFFFu;
|
|
48
83
|
}
|
|
49
84
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
for (
|
|
55
|
-
|
|
85
|
+
// --- hex encoding ---
|
|
86
|
+
std::string to_hex(const uint8_t* d, size_t n) {
|
|
87
|
+
static const char* h = "0123456789abcdef";
|
|
88
|
+
std::string s(n * 2, '0');
|
|
89
|
+
for (size_t i = 0; i < n; ++i) {
|
|
90
|
+
s[2 * i] = h[d[i] >> 4];
|
|
91
|
+
s[2 * i + 1] = h[d[i] & 0xF];
|
|
56
92
|
}
|
|
57
|
-
|
|
58
|
-
return count;
|
|
93
|
+
return s;
|
|
59
94
|
}
|
|
60
95
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
96
|
+
// SHA-256 of an empty input, used for zero-byte files.
|
|
97
|
+
std::string sha256_of_empty() {
|
|
98
|
+
uint8_t digest[SHA256_HASH_SIZE];
|
|
99
|
+
sha256_hash(digest, nullptr, 0);
|
|
100
|
+
return to_hex(digest, SHA256_HASH_SIZE);
|
|
101
|
+
}
|
|
64
102
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
|
|
103
|
+
// --- random transfer id ---
|
|
104
|
+
std::string random_transfer_id() {
|
|
105
|
+
static const char* h = "0123456789abcdef";
|
|
106
|
+
std::random_device rd;
|
|
107
|
+
std::mt19937 gen(rd() ^ uint32_t(std::chrono::steady_clock::now().time_since_epoch().count()));
|
|
108
|
+
std::uniform_int_distribution<int> dis(0, 15);
|
|
109
|
+
std::string id(32, '0');
|
|
110
|
+
for (char& c : id) c = h[dis(gen)];
|
|
111
|
+
return id;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// A relative path coming from a peer must not escape the destination directory.
|
|
115
|
+
bool is_safe_relative_path(const std::string& p) {
|
|
116
|
+
if (p.empty()) return false;
|
|
117
|
+
if (p.front() == '/' || p.front() == '\\') return false;
|
|
118
|
+
if (p.size() >= 2 && p[1] == ':') return false; // Windows drive letter
|
|
119
|
+
size_t start = 0;
|
|
120
|
+
for (size_t i = 0; i <= p.size(); ++i) {
|
|
121
|
+
if (i == p.size() || p[i] == '/' || p[i] == '\\') {
|
|
122
|
+
std::string comp = p.substr(start, i - start);
|
|
123
|
+
if (comp.empty() || comp == "." || comp == "..") return false;
|
|
124
|
+
start = i + 1;
|
|
87
125
|
}
|
|
88
126
|
}
|
|
89
|
-
|
|
90
|
-
bytes_transferred = new_bytes_transferred;
|
|
91
|
-
last_update = now;
|
|
127
|
+
return true;
|
|
92
128
|
}
|
|
93
129
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
130
|
+
} // namespace
|
|
131
|
+
|
|
132
|
+
// =============================================================================
|
|
133
|
+
// Internal per-transfer state
|
|
134
|
+
// =============================================================================
|
|
135
|
+
|
|
136
|
+
// One file within a transfer.
|
|
137
|
+
struct TransferFile {
|
|
138
|
+
std::string relative_path; // POSIX path relative to the transfer root
|
|
139
|
+
uint64_t size = 0;
|
|
140
|
+
|
|
141
|
+
// sender side
|
|
142
|
+
std::string source_path; // local path the data is read from
|
|
143
|
+
|
|
144
|
+
// receiver side
|
|
145
|
+
std::string temp_path; // chunks are written here first
|
|
146
|
+
std::string final_path; // destination once verified
|
|
147
|
+
uint64_t received = 0; // bytes written so far
|
|
148
|
+
bool temp_created = false;
|
|
149
|
+
std::string expected_sha; // from ft_file_end
|
|
150
|
+
std::string computed_sha; // hashed while receiving
|
|
151
|
+
bool sha_known = false;
|
|
152
|
+
bool finalized = false;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Full state of a single transfer. Always held through a shared_ptr; `mtx`
|
|
156
|
+
// guards every field below `total_bytes`.
|
|
157
|
+
struct Transfer {
|
|
158
|
+
// immutable after creation
|
|
159
|
+
std::string id;
|
|
160
|
+
std::string peer_id;
|
|
161
|
+
FileTransferDirection direction;
|
|
162
|
+
bool is_directory = false;
|
|
163
|
+
std::string name;
|
|
164
|
+
std::string local_root; // sender: source path; receiver: destination
|
|
165
|
+
std::vector<TransferFile> files;
|
|
166
|
+
uint64_t total_bytes = 0;
|
|
167
|
+
|
|
168
|
+
// mutable state
|
|
169
|
+
std::mutex mtx;
|
|
170
|
+
std::condition_variable cv;
|
|
171
|
+
FileTransferStatus status = FileTransferStatus::PENDING;
|
|
172
|
+
bool finished = false; // finish() has run (fired callbacks); distinct from status
|
|
173
|
+
std::string error;
|
|
174
|
+
uint64_t bytes_done = 0; // sender: streamed bytes; receiver: written bytes
|
|
175
|
+
uint64_t acked_bytes = 0; // sender: bytes confirmed by the peer
|
|
176
|
+
uint32_t files_done = 0;
|
|
177
|
+
|
|
178
|
+
// sender streaming cursor (survives pause)
|
|
179
|
+
size_t send_file = 0;
|
|
180
|
+
uint64_t send_offset = 0;
|
|
181
|
+
sha256_context_t send_hash;
|
|
182
|
+
bool worker_active = false;
|
|
183
|
+
|
|
184
|
+
// receiver cursor
|
|
185
|
+
size_t recv_file = 0;
|
|
186
|
+
sha256_context_t recv_hash;
|
|
187
|
+
uint64_t last_ack_sent = 0; // bytes_done at the last ft_progress emitted
|
|
188
|
+
|
|
189
|
+
// timing
|
|
190
|
+
std::chrono::steady_clock::time_point start_time;
|
|
191
|
+
std::chrono::steady_clock::time_point last_activity;
|
|
192
|
+
std::chrono::steady_clock::time_point last_progress_cb;
|
|
193
|
+
|
|
194
|
+
// throughput
|
|
195
|
+
double rate_bps = 0.0;
|
|
196
|
+
uint64_t rate_mark_bytes = 0;
|
|
197
|
+
std::chrono::steady_clock::time_point rate_mark_time;
|
|
198
|
+
|
|
199
|
+
bool is_terminal() const {
|
|
200
|
+
return status == FileTransferStatus::COMPLETED ||
|
|
201
|
+
status == FileTransferStatus::FAILED ||
|
|
202
|
+
status == FileTransferStatus::CANCELLED;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// =============================================================================
|
|
207
|
+
// Status name
|
|
208
|
+
// =============================================================================
|
|
209
|
+
|
|
210
|
+
const char* file_transfer_status_name(FileTransferStatus s) {
|
|
211
|
+
switch (s) {
|
|
212
|
+
case FileTransferStatus::PENDING: return "PENDING";
|
|
213
|
+
case FileTransferStatus::STARTING: return "STARTING";
|
|
214
|
+
case FileTransferStatus::IN_PROGRESS: return "IN_PROGRESS";
|
|
215
|
+
case FileTransferStatus::PAUSED: return "PAUSED";
|
|
216
|
+
case FileTransferStatus::COMPLETED: return "COMPLETED";
|
|
217
|
+
case FileTransferStatus::FAILED: return "FAILED";
|
|
218
|
+
case FileTransferStatus::CANCELLED: return "CANCELLED";
|
|
219
|
+
case FileTransferStatus::RESUMING: return "RESUMING";
|
|
220
|
+
}
|
|
221
|
+
return "UNKNOWN";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// =============================================================================
|
|
225
|
+
// Construction / destruction
|
|
226
|
+
// =============================================================================
|
|
97
227
|
|
|
98
228
|
FileTransferManager::FileTransferManager(RatsClient& client, const FileTransferConfig& config)
|
|
99
|
-
: client_(client), config_(config)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
start_time_ = std::chrono::steady_clock::now();
|
|
104
|
-
initialize();
|
|
105
|
-
}
|
|
229
|
+
: client_(client), config_(config) {
|
|
230
|
+
started_at_ = std::chrono::steady_clock::now();
|
|
231
|
+
create_directories(config_.temp_directory.c_str());
|
|
232
|
+
register_handlers();
|
|
106
233
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
234
|
+
uint32_t threads = std::max<uint32_t>(1, config_.worker_threads);
|
|
235
|
+
for (uint32_t i = 0; i < threads; ++i) {
|
|
236
|
+
workers_.emplace_back(&FileTransferManager::worker_loop, this);
|
|
237
|
+
}
|
|
238
|
+
maintenance_thread_ = std::thread(&FileTransferManager::maintenance_loop, this);
|
|
110
239
|
|
|
111
|
-
|
|
112
|
-
// Ensure temp directory exists
|
|
113
|
-
create_directories(config_.temp_directory.c_str());
|
|
114
|
-
|
|
115
|
-
// Register message handlers with RatsClient
|
|
116
|
-
client_.on("file_transfer_request", [this](const std::string& peer_id, const nlohmann::json& data) {
|
|
117
|
-
handle_transfer_request(peer_id, data);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
client_.on("file_transfer_response", [this](const std::string& peer_id, const nlohmann::json& data) {
|
|
121
|
-
handle_transfer_response(peer_id, data);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
client_.on("file_chunk_metadata", [this](const std::string& peer_id, const nlohmann::json& data) {
|
|
125
|
-
handle_chunk_metadata_message(peer_id, data);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// Note: Binary chunk data will be handled through the global binary_data_callback_
|
|
129
|
-
// The FileTransferManager will check for file chunk magic headers in the callback
|
|
130
|
-
|
|
131
|
-
client_.on("file_chunk_ack", [this](const std::string& peer_id, const nlohmann::json& data) {
|
|
132
|
-
handle_chunk_ack_message(peer_id, data);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
client_.on("file_transfer_control", [this](const std::string& peer_id, const nlohmann::json& data) {
|
|
136
|
-
handle_transfer_control(peer_id, data);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
client_.on("file_request", [this](const std::string& peer_id, const nlohmann::json& data) {
|
|
140
|
-
handle_file_request(peer_id, data);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
client_.on("directory_request", [this](const std::string& peer_id, const nlohmann::json& data) {
|
|
144
|
-
handle_directory_request(peer_id, data);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// Start worker threads
|
|
148
|
-
for (uint32_t i = 0; i < config_.max_concurrent_chunks; ++i) {
|
|
149
|
-
worker_threads_.emplace_back(&FileTransferManager::worker_thread_loop, this);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Start cleanup thread for pending chunks timeout
|
|
153
|
-
worker_threads_.emplace_back(&FileTransferManager::cleanup_thread_loop, this);
|
|
154
|
-
|
|
155
|
-
LOG_FILE_TRANSFER_INFO("FileTransferManager initialized with " << worker_threads_.size() << " worker threads");
|
|
240
|
+
LOG_FT_INFO("FileTransferManager started (" << threads << " workers)");
|
|
156
241
|
}
|
|
157
242
|
|
|
158
|
-
|
|
159
|
-
LOG_FILE_TRANSFER_INFO("FileTransferManager stopping...");
|
|
243
|
+
FileTransferManager::~FileTransferManager() {
|
|
160
244
|
running_.store(false);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
thread.join();
|
|
245
|
+
queue_cv_.notify_all();
|
|
246
|
+
maintenance_cv_.notify_all();
|
|
247
|
+
|
|
248
|
+
// Wake any worker blocked on a transfer's condition variable.
|
|
249
|
+
{
|
|
250
|
+
std::lock_guard<std::mutex> lk(transfers_mutex_);
|
|
251
|
+
for (auto& kv : transfers_) {
|
|
252
|
+
std::lock_guard<std::mutex> tk(kv.second->mtx);
|
|
253
|
+
kv.second->cv.notify_all();
|
|
171
254
|
}
|
|
172
255
|
}
|
|
173
|
-
|
|
174
|
-
worker_threads_.clear();
|
|
175
|
-
LOG_FILE_TRANSFER_INFO("FileTransferManager stopped");
|
|
176
|
-
}
|
|
177
256
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
std::unique_lock<std::mutex> lock(work_mutex_);
|
|
181
|
-
work_condition_.wait(lock, [this] { return !work_queue_.empty() || !running_.load(); });
|
|
182
|
-
|
|
183
|
-
if (!running_.load()) {
|
|
184
|
-
break;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (!work_queue_.empty()) {
|
|
188
|
-
std::string transfer_id = work_queue_.front();
|
|
189
|
-
work_queue_.pop();
|
|
190
|
-
lock.unlock();
|
|
191
|
-
|
|
192
|
-
process_transfer(transfer_id);
|
|
193
|
-
}
|
|
257
|
+
for (auto& w : workers_) {
|
|
258
|
+
if (w.joinable()) w.join();
|
|
194
259
|
}
|
|
260
|
+
if (maintenance_thread_.joinable()) maintenance_thread_.join();
|
|
261
|
+
LOG_FT_INFO("FileTransferManager stopped");
|
|
195
262
|
}
|
|
196
263
|
|
|
197
|
-
void FileTransferManager::
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
cleanup_condition_.wait_for(lock, cleanup_interval, [this] { return !running_.load(); });
|
|
205
|
-
|
|
206
|
-
if (!running_.load()) {
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Clean up expired pending chunks
|
|
211
|
-
auto now = std::chrono::steady_clock::now();
|
|
212
|
-
std::vector<std::string> expired_peers;
|
|
213
|
-
|
|
214
|
-
{
|
|
215
|
-
std::lock_guard<std::mutex> lock(chunks_mutex_);
|
|
216
|
-
for (auto it = pending_chunks_.begin(); it != pending_chunks_.end();) {
|
|
217
|
-
if (now - it->second.created_at > pending_timeout) {
|
|
218
|
-
LOG_FILE_TRANSFER_WARN("Pending chunk from peer " << it->first <<
|
|
219
|
-
" timed out (transfer: " << it->second.transfer_id <<
|
|
220
|
-
", chunk: " << it->second.chunk_index << ")");
|
|
221
|
-
expired_peers.push_back(it->first);
|
|
222
|
-
it = pending_chunks_.erase(it);
|
|
223
|
-
} else {
|
|
224
|
-
++it;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Send negative acknowledgments for expired chunks
|
|
230
|
-
for (const auto& peer_id : expired_peers) {
|
|
231
|
-
// We don't have direct access to the transfer info here, but the timeout logged above
|
|
232
|
-
// provides sufficient information for debugging
|
|
233
|
-
LOG_FILE_TRANSFER_DEBUG("Cleaned up " << expired_peers.size() << " expired pending chunks");
|
|
234
|
-
}
|
|
235
|
-
}
|
|
264
|
+
void FileTransferManager::register_handlers() {
|
|
265
|
+
client_.on(MSG_OFFER, [this](const std::string& p, const nlohmann::json& m) { on_offer(p, m); });
|
|
266
|
+
client_.on(MSG_RESPONSE, [this](const std::string& p, const nlohmann::json& m) { on_response(p, m); });
|
|
267
|
+
client_.on(MSG_FILE_END, [this](const std::string& p, const nlohmann::json& m) { on_file_end(p, m); });
|
|
268
|
+
client_.on(MSG_PROGRESS, [this](const std::string& p, const nlohmann::json& m) { on_progress(p, m); });
|
|
269
|
+
client_.on(MSG_COMPLETE, [this](const std::string& p, const nlohmann::json& m) { on_complete(p, m); });
|
|
270
|
+
client_.on(MSG_CONTROL, [this](const std::string& p, const nlohmann::json& m) { on_control(p, m); });
|
|
236
271
|
}
|
|
237
272
|
|
|
273
|
+
// =============================================================================
|
|
274
|
+
// Configuration
|
|
275
|
+
// =============================================================================
|
|
276
|
+
|
|
238
277
|
void FileTransferManager::set_config(const FileTransferConfig& config) {
|
|
239
|
-
std::lock_guard<std::mutex>
|
|
278
|
+
std::lock_guard<std::mutex> lk(config_mutex_);
|
|
240
279
|
config_ = config;
|
|
241
|
-
|
|
242
|
-
// Ensure temp directory exists
|
|
243
280
|
create_directories(config_.temp_directory.c_str());
|
|
244
281
|
}
|
|
245
282
|
|
|
246
|
-
|
|
283
|
+
FileTransferConfig FileTransferManager::get_config() const {
|
|
284
|
+
std::lock_guard<std::mutex> lk(config_mutex_);
|
|
247
285
|
return config_;
|
|
248
286
|
}
|
|
249
287
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (binary_data.size() >= magic.length() &&
|
|
255
|
-
std::memcmp(binary_data.data(), magic.c_str(), magic.length()) == 0) {
|
|
256
|
-
|
|
257
|
-
// This is a file transfer chunk - handle it
|
|
258
|
-
handle_chunk_binary_message(peer_id, binary_data);
|
|
259
|
-
return true;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Not a file transfer chunk
|
|
263
|
-
return false;
|
|
264
|
-
}
|
|
288
|
+
// =============================================================================
|
|
289
|
+
// Callbacks
|
|
290
|
+
// =============================================================================
|
|
265
291
|
|
|
266
|
-
void FileTransferManager::
|
|
267
|
-
|
|
292
|
+
void FileTransferManager::set_offer_callback(TransferOfferCallback cb) {
|
|
293
|
+
std::lock_guard<std::mutex> lk(callbacks_mutex_);
|
|
294
|
+
offer_callback_ = std::move(cb);
|
|
268
295
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
296
|
+
void FileTransferManager::set_progress_callback(TransferProgressCallback cb) {
|
|
297
|
+
std::lock_guard<std::mutex> lk(callbacks_mutex_);
|
|
298
|
+
progress_callback_ = std::move(cb);
|
|
272
299
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
300
|
+
void FileTransferManager::set_completed_callback(TransferCompletedCallback cb) {
|
|
301
|
+
std::lock_guard<std::mutex> lk(callbacks_mutex_);
|
|
302
|
+
completed_callback_ = std::move(cb);
|
|
276
303
|
}
|
|
277
304
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
305
|
+
// =============================================================================
|
|
306
|
+
// Lookup / progress snapshot
|
|
307
|
+
// =============================================================================
|
|
281
308
|
|
|
282
|
-
|
|
283
|
-
|
|
309
|
+
std::shared_ptr<Transfer> FileTransferManager::find(const std::string& id) const {
|
|
310
|
+
std::lock_guard<std::mutex> lk(transfers_mutex_);
|
|
311
|
+
auto it = transfers_.find(id);
|
|
312
|
+
return it == transfers_.end() ? nullptr : it->second;
|
|
284
313
|
}
|
|
285
314
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
315
|
+
// Builds a progress snapshot. Caller must hold t->mtx.
|
|
316
|
+
FileTransferProgress FileTransferManager::snapshot(const std::shared_ptr<Transfer>& t) const {
|
|
317
|
+
FileTransferProgress p;
|
|
318
|
+
p.transfer_id = t->id;
|
|
319
|
+
p.peer_id = t->peer_id;
|
|
320
|
+
p.direction = t->direction;
|
|
321
|
+
p.status = t->status;
|
|
322
|
+
p.filename = t->name;
|
|
323
|
+
p.local_path = t->local_root;
|
|
324
|
+
p.is_directory = t->is_directory;
|
|
325
|
+
p.bytes_transferred = t->bytes_done;
|
|
326
|
+
p.total_bytes = t->total_bytes;
|
|
327
|
+
p.files_completed = t->files_done;
|
|
328
|
+
p.total_files = static_cast<uint32_t>(t->files.size());
|
|
329
|
+
p.transfer_rate_bps = t->rate_bps;
|
|
330
|
+
p.error_message = t->error;
|
|
289
331
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (
|
|
294
|
-
|
|
295
|
-
return "";
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Get file metadata
|
|
299
|
-
FileMetadata metadata = get_file_metadata(file_path);
|
|
300
|
-
if (metadata.file_size == 0) {
|
|
301
|
-
LOG_FILE_TRANSFER_ERROR("Failed to get metadata for file: " << file_path);
|
|
302
|
-
return "";
|
|
332
|
+
auto now = std::chrono::steady_clock::now();
|
|
333
|
+
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - t->start_time);
|
|
334
|
+
p.elapsed_time = elapsed;
|
|
335
|
+
if (elapsed.count() > 0) {
|
|
336
|
+
p.average_rate_bps = static_cast<double>(t->bytes_done) * 1000.0 / elapsed.count();
|
|
303
337
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
metadata.filename = remote_filename;
|
|
338
|
+
if (t->rate_bps > 1.0 && t->total_bytes > t->bytes_done) {
|
|
339
|
+
double secs = static_cast<double>(t->total_bytes - t->bytes_done) / t->rate_bps;
|
|
340
|
+
p.estimated_time_remaining = std::chrono::milliseconds(static_cast<int64_t>(secs * 1000.0));
|
|
308
341
|
}
|
|
309
|
-
|
|
310
|
-
return send_file_with_metadata(peer_id, file_path, metadata);
|
|
342
|
+
return p;
|
|
311
343
|
}
|
|
312
344
|
|
|
313
|
-
std::
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
progress->transfer_id = transfer_id;
|
|
320
|
-
progress->peer_id = peer_id;
|
|
321
|
-
progress->direction = FileTransferDirection::SENDING;
|
|
322
|
-
progress->status = FileTransferStatus::STARTING;
|
|
323
|
-
progress->filename = metadata.filename;
|
|
324
|
-
progress->local_path = file_path;
|
|
325
|
-
progress->file_size = metadata.file_size;
|
|
326
|
-
progress->total_bytes = metadata.file_size;
|
|
327
|
-
progress->total_chunks = (metadata.file_size + config_.chunk_size - 1) / config_.chunk_size;
|
|
328
|
-
|
|
329
|
-
{
|
|
330
|
-
std::lock_guard<std::mutex> lock(transfers_mutex_);
|
|
331
|
-
active_transfers_[transfer_id] = progress;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Send transfer request to peer
|
|
335
|
-
nlohmann::json request_msg = create_transfer_request_message(metadata, transfer_id);
|
|
336
|
-
client_.send(peer_id, "file_transfer_request", request_msg);
|
|
337
|
-
|
|
338
|
-
LOG_FILE_TRANSFER_INFO("Initiated file transfer request: " << transfer_id << " (" << metadata.filename << " -> " << peer_id << ")");
|
|
339
|
-
return transfer_id;
|
|
345
|
+
std::shared_ptr<FileTransferProgress>
|
|
346
|
+
FileTransferManager::get_progress(const std::string& id) const {
|
|
347
|
+
auto t = find(id);
|
|
348
|
+
if (!t) return nullptr;
|
|
349
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
350
|
+
return std::make_shared<FileTransferProgress>(snapshot(t));
|
|
340
351
|
}
|
|
341
352
|
|
|
342
|
-
std::
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
353
|
+
std::vector<std::shared_ptr<FileTransferProgress>>
|
|
354
|
+
FileTransferManager::get_active_transfers() const {
|
|
355
|
+
std::vector<std::shared_ptr<Transfer>> all;
|
|
356
|
+
{
|
|
357
|
+
std::lock_guard<std::mutex> lk(transfers_mutex_);
|
|
358
|
+
for (auto& kv : transfers_) all.push_back(kv.second);
|
|
359
|
+
}
|
|
360
|
+
std::vector<std::shared_ptr<FileTransferProgress>> out;
|
|
361
|
+
for (auto& t : all) {
|
|
362
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
363
|
+
if (!t->is_terminal()) {
|
|
364
|
+
out.push_back(std::make_shared<FileTransferProgress>(snapshot(t)));
|
|
365
|
+
}
|
|
355
366
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
std::string transfer_id = generate_transfer_id();
|
|
363
|
-
|
|
364
|
-
// Create transfer progress tracking for directory
|
|
365
|
-
auto progress = std::make_shared<FileTransferProgress>();
|
|
366
|
-
progress->transfer_id = transfer_id;
|
|
367
|
-
progress->peer_id = peer_id;
|
|
368
|
-
progress->direction = FileTransferDirection::SENDING;
|
|
369
|
-
progress->status = FileTransferStatus::STARTING;
|
|
370
|
-
progress->filename = dir_metadata.directory_name;
|
|
371
|
-
progress->local_path = directory_path;
|
|
372
|
-
progress->total_bytes = dir_metadata.get_total_size();
|
|
373
|
-
progress->file_size = progress->total_bytes;
|
|
374
|
-
|
|
367
|
+
return out;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
nlohmann::json FileTransferManager::get_statistics() const {
|
|
371
|
+
nlohmann::json j;
|
|
375
372
|
{
|
|
376
|
-
std::lock_guard<std::mutex>
|
|
377
|
-
|
|
373
|
+
std::lock_guard<std::mutex> lk(stats_mutex_);
|
|
374
|
+
auto uptime = std::chrono::duration_cast<std::chrono::seconds>(
|
|
375
|
+
std::chrono::steady_clock::now() - started_at_);
|
|
376
|
+
j["uptime_seconds"] = uptime.count();
|
|
377
|
+
j["total_bytes_sent"] = stat_bytes_sent_;
|
|
378
|
+
j["total_bytes_received"] = stat_bytes_received_;
|
|
379
|
+
j["total_files_sent"] = stat_files_sent_;
|
|
380
|
+
j["total_files_received"] = stat_files_received_;
|
|
381
|
+
j["completed_transfers"] = stat_completed_;
|
|
382
|
+
j["failed_transfers"] = stat_failed_;
|
|
383
|
+
uint64_t total = stat_completed_ + stat_failed_;
|
|
384
|
+
j["success_rate"] = total ? static_cast<double>(stat_completed_) / total : 0.0;
|
|
378
385
|
}
|
|
379
|
-
|
|
380
|
-
// Store directory metadata for processing
|
|
381
386
|
{
|
|
382
|
-
std::lock_guard<std::mutex>
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
// Serialize directory metadata with full file information
|
|
392
|
-
nlohmann::json dir_metadata_json;
|
|
393
|
-
dir_metadata_json["directory_name"] = dir_metadata.directory_name;
|
|
394
|
-
dir_metadata_json["relative_path"] = dir_metadata.relative_path;
|
|
395
|
-
dir_metadata_json["total_size"] = dir_metadata.get_total_size();
|
|
396
|
-
dir_metadata_json["total_files"] = dir_metadata.get_total_file_count();
|
|
397
|
-
|
|
398
|
-
// Serialize file metadata
|
|
399
|
-
nlohmann::json files_json = nlohmann::json::array();
|
|
400
|
-
for (const auto& file_meta : dir_metadata.files) {
|
|
401
|
-
nlohmann::json file_json;
|
|
402
|
-
file_json["filename"] = file_meta.filename;
|
|
403
|
-
file_json["relative_path"] = file_meta.relative_path;
|
|
404
|
-
file_json["file_size"] = file_meta.file_size;
|
|
405
|
-
file_json["last_modified"] = file_meta.last_modified;
|
|
406
|
-
file_json["mime_type"] = file_meta.mime_type;
|
|
407
|
-
file_json["checksum"] = file_meta.checksum;
|
|
408
|
-
files_json.push_back(file_json);
|
|
409
|
-
}
|
|
410
|
-
dir_metadata_json["files"] = files_json;
|
|
411
|
-
|
|
412
|
-
// For now, we'll handle single-level directories (can be extended for nested later)
|
|
413
|
-
dir_metadata_json["subdirectories"] = nlohmann::json::array();
|
|
414
|
-
|
|
415
|
-
request_msg["directory_metadata"] = dir_metadata_json;
|
|
416
|
-
|
|
417
|
-
client_.send(peer_id, "file_transfer_request", request_msg);
|
|
418
|
-
|
|
419
|
-
LOG_FILE_TRANSFER_INFO("Initiated directory transfer request: " << transfer_id << " (" << dir_metadata.directory_name << " -> " << peer_id << ")");
|
|
420
|
-
return transfer_id;
|
|
387
|
+
std::lock_guard<std::mutex> lk(transfers_mutex_);
|
|
388
|
+
size_t active = 0;
|
|
389
|
+
for (auto& kv : transfers_) {
|
|
390
|
+
std::lock_guard<std::mutex> tk(kv.second->mtx);
|
|
391
|
+
if (!kv.second->is_terminal()) ++active;
|
|
392
|
+
}
|
|
393
|
+
j["active_transfers"] = active;
|
|
394
|
+
}
|
|
395
|
+
return j;
|
|
421
396
|
}
|
|
422
397
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
// Create file request message
|
|
428
|
-
nlohmann::json request_msg;
|
|
429
|
-
request_msg["transfer_id"] = transfer_id;
|
|
430
|
-
request_msg["type"] = "file_request";
|
|
431
|
-
request_msg["remote_path"] = remote_file_path;
|
|
432
|
-
request_msg["local_path"] = local_path;
|
|
433
|
-
|
|
434
|
-
client_.send(peer_id, "file_request", request_msg);
|
|
435
|
-
|
|
436
|
-
LOG_FILE_TRANSFER_INFO("Sent file request: " << transfer_id << " (" << remote_file_path << " from " << peer_id << ")");
|
|
437
|
-
return transfer_id;
|
|
438
|
-
}
|
|
398
|
+
// =============================================================================
|
|
399
|
+
// Progress / completion notification
|
|
400
|
+
// =============================================================================
|
|
439
401
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
std::string transfer_id = generate_transfer_id();
|
|
443
|
-
|
|
444
|
-
// Create directory request message
|
|
445
|
-
nlohmann::json request_msg;
|
|
446
|
-
request_msg["transfer_id"] = transfer_id;
|
|
447
|
-
request_msg["type"] = "directory_request";
|
|
448
|
-
request_msg["remote_path"] = remote_directory_path;
|
|
449
|
-
request_msg["local_path"] = local_directory_path;
|
|
450
|
-
request_msg["recursive"] = recursive;
|
|
451
|
-
|
|
452
|
-
client_.send(peer_id, "directory_request", request_msg);
|
|
453
|
-
|
|
454
|
-
LOG_FILE_TRANSFER_INFO("Sent directory request: " << transfer_id << " (" << remote_directory_path << " from " << peer_id << ")");
|
|
455
|
-
return transfer_id;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
bool FileTransferManager::accept_file_transfer(const std::string& transfer_id, const std::string& local_path) {
|
|
459
|
-
std::lock_guard<std::mutex> lock(pending_mutex_);
|
|
460
|
-
|
|
461
|
-
auto it = pending_transfers_.find(transfer_id);
|
|
462
|
-
if (it == pending_transfers_.end()) {
|
|
463
|
-
LOG_FILE_TRANSFER_ERROR("Transfer not found in pending transfers: " << transfer_id);
|
|
464
|
-
return false;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
PendingFileTransfer pending_transfer = it->second;
|
|
468
|
-
pending_transfers_.erase(it);
|
|
469
|
-
|
|
470
|
-
// Create transfer progress tracking
|
|
471
|
-
auto progress = std::make_shared<FileTransferProgress>();
|
|
472
|
-
progress->transfer_id = transfer_id;
|
|
473
|
-
progress->peer_id = pending_transfer.peer_id;
|
|
474
|
-
progress->direction = FileTransferDirection::RECEIVING;
|
|
475
|
-
progress->status = FileTransferStatus::STARTING;
|
|
476
|
-
progress->filename = pending_transfer.metadata.filename;
|
|
477
|
-
progress->local_path = local_path;
|
|
478
|
-
progress->file_size = pending_transfer.metadata.file_size;
|
|
479
|
-
progress->total_bytes = pending_transfer.metadata.file_size;
|
|
480
|
-
progress->total_chunks = (pending_transfer.metadata.file_size + config_.chunk_size - 1) / config_.chunk_size;
|
|
481
|
-
|
|
402
|
+
void FileTransferManager::emit_progress(const std::shared_ptr<Transfer>& t) {
|
|
403
|
+
FileTransferProgress snap;
|
|
482
404
|
{
|
|
483
|
-
std::lock_guard<std::mutex>
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
405
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
406
|
+
auto now = std::chrono::steady_clock::now();
|
|
407
|
+
|
|
408
|
+
// Refresh the throughput estimate roughly twice a second.
|
|
409
|
+
auto dt = std::chrono::duration_cast<std::chrono::milliseconds>(now - t->rate_mark_time);
|
|
410
|
+
if (dt.count() >= 500) {
|
|
411
|
+
t->rate_bps = static_cast<double>(t->bytes_done - t->rate_mark_bytes) * 1000.0 / dt.count();
|
|
412
|
+
t->rate_mark_bytes = t->bytes_done;
|
|
413
|
+
t->rate_mark_time = now;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Throttle callbacks to ~10/s unless the transfer just finished.
|
|
417
|
+
auto since_cb = std::chrono::duration_cast<std::chrono::milliseconds>(now - t->last_progress_cb);
|
|
418
|
+
if (!t->is_terminal() && since_cb.count() < 100) return;
|
|
419
|
+
t->last_progress_cb = now;
|
|
420
|
+
|
|
421
|
+
snap = snapshot(t);
|
|
422
|
+
}
|
|
423
|
+
TransferProgressCallback cb;
|
|
492
424
|
{
|
|
493
|
-
std::lock_guard<std::mutex>
|
|
494
|
-
|
|
425
|
+
std::lock_guard<std::mutex> lk(callbacks_mutex_);
|
|
426
|
+
cb = progress_callback_;
|
|
495
427
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
LOG_FILE_TRANSFER_INFO("Accepted file transfer: " << transfer_id << " (" << pending_transfer.metadata.filename << " from " << pending_transfer.peer_id << " -> " << local_path << ")");
|
|
499
|
-
return true;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
bool FileTransferManager::reject_file_transfer(const std::string& transfer_id, const std::string& reason) {
|
|
503
|
-
std::lock_guard<std::mutex> lock(pending_mutex_);
|
|
504
|
-
|
|
505
|
-
auto it = pending_transfers_.find(transfer_id);
|
|
506
|
-
if (it == pending_transfers_.end()) {
|
|
507
|
-
LOG_FILE_TRANSFER_ERROR("Transfer not found in pending transfers: " << transfer_id);
|
|
508
|
-
return false;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
PendingFileTransfer pending_transfer = it->second;
|
|
512
|
-
pending_transfers_.erase(it);
|
|
513
|
-
|
|
514
|
-
// Send rejection response
|
|
515
|
-
nlohmann::json response_msg = create_transfer_response_message(transfer_id, false, reason);
|
|
516
|
-
client_.send(pending_transfer.peer_id, "file_transfer_response", response_msg);
|
|
517
|
-
|
|
518
|
-
LOG_FILE_TRANSFER_INFO("Rejected file transfer: " << transfer_id << " (" << pending_transfer.metadata.filename << " from " << pending_transfer.peer_id << ", reason: " << reason << ")");
|
|
519
|
-
return true;
|
|
428
|
+
if (cb) cb(snap);
|
|
520
429
|
}
|
|
521
430
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
LOG_FILE_TRANSFER_ERROR("Directory transfer not found in pending transfers: " << transfer_id);
|
|
528
|
-
return false;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
PendingDirectoryTransfer pending_transfer = it->second;
|
|
532
|
-
pending_directory_transfers_.erase(it);
|
|
533
|
-
|
|
534
|
-
// Create transfer progress tracking
|
|
535
|
-
auto progress = std::make_shared<FileTransferProgress>();
|
|
536
|
-
progress->transfer_id = transfer_id;
|
|
537
|
-
progress->peer_id = pending_transfer.peer_id;
|
|
538
|
-
progress->direction = FileTransferDirection::RECEIVING;
|
|
539
|
-
progress->status = FileTransferStatus::STARTING;
|
|
540
|
-
progress->filename = pending_transfer.metadata.directory_name;
|
|
541
|
-
progress->local_path = local_path;
|
|
542
|
-
progress->total_bytes = pending_transfer.metadata.get_total_size();
|
|
543
|
-
progress->file_size = progress->total_bytes;
|
|
544
|
-
progress->total_chunks = 0; // Will be calculated per file
|
|
545
|
-
|
|
431
|
+
void FileTransferManager::finish(const std::shared_ptr<Transfer>& t, bool success,
|
|
432
|
+
const std::string& error) {
|
|
433
|
+
FileTransferProgress snap;
|
|
434
|
+
bool is_receiver;
|
|
435
|
+
size_t file_count;
|
|
546
436
|
{
|
|
547
|
-
std::lock_guard<std::mutex>
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
437
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
438
|
+
if (t->finished) return; // finish() already ran for this transfer
|
|
439
|
+
t->finished = true;
|
|
440
|
+
// Preserve an explicit CANCELLED status set by the cancel path.
|
|
441
|
+
if (!success && t->status == FileTransferStatus::CANCELLED) {
|
|
442
|
+
// keep CANCELLED
|
|
443
|
+
} else {
|
|
444
|
+
t->status = success ? FileTransferStatus::COMPLETED : FileTransferStatus::FAILED;
|
|
445
|
+
}
|
|
446
|
+
t->error = error;
|
|
447
|
+
t->last_activity = std::chrono::steady_clock::now();
|
|
448
|
+
t->last_progress_cb = {}; // force the final progress callback through
|
|
449
|
+
is_receiver = (t->direction == FileTransferDirection::RECEIVING);
|
|
450
|
+
file_count = t->files.size();
|
|
451
|
+
t->cv.notify_all();
|
|
452
|
+
snap = snapshot(t);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (!success && is_receiver) {
|
|
456
|
+
cleanup_temp_files(t);
|
|
457
|
+
}
|
|
458
|
+
|
|
556
459
|
{
|
|
557
|
-
std::lock_guard<std::mutex>
|
|
558
|
-
|
|
460
|
+
std::lock_guard<std::mutex> lk(stats_mutex_);
|
|
461
|
+
if (success) {
|
|
462
|
+
++stat_completed_;
|
|
463
|
+
if (is_receiver) stat_files_received_ += file_count;
|
|
464
|
+
else stat_files_sent_ += file_count;
|
|
465
|
+
} else {
|
|
466
|
+
++stat_failed_;
|
|
467
|
+
}
|
|
559
468
|
}
|
|
560
|
-
|
|
561
|
-
|
|
469
|
+
|
|
470
|
+
TransferProgressCallback pcb;
|
|
471
|
+
TransferCompletedCallback ccb;
|
|
562
472
|
{
|
|
563
|
-
std::lock_guard<std::mutex>
|
|
564
|
-
|
|
473
|
+
std::lock_guard<std::mutex> lk(callbacks_mutex_);
|
|
474
|
+
pcb = progress_callback_;
|
|
475
|
+
ccb = completed_callback_;
|
|
565
476
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
LOG_FILE_TRANSFER_INFO("Accepted directory transfer: " << transfer_id << " (" << pending_transfer.metadata.directory_name << " from " << pending_transfer.peer_id << " -> " << local_path << ")");
|
|
569
|
-
return true;
|
|
570
|
-
}
|
|
477
|
+
if (pcb) pcb(snap);
|
|
478
|
+
if (ccb) ccb(t->id, success, error);
|
|
571
479
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
auto it = pending_directory_transfers_.find(transfer_id);
|
|
576
|
-
if (it == pending_directory_transfers_.end()) {
|
|
577
|
-
LOG_FILE_TRANSFER_ERROR("Directory transfer not found in pending transfers: " << transfer_id);
|
|
578
|
-
return false;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
PendingDirectoryTransfer pending_transfer = it->second;
|
|
582
|
-
pending_directory_transfers_.erase(it);
|
|
583
|
-
|
|
584
|
-
// Send rejection response
|
|
585
|
-
nlohmann::json response_msg = create_transfer_response_message(transfer_id, false, reason);
|
|
586
|
-
client_.send(pending_transfer.peer_id, "file_transfer_response", response_msg);
|
|
587
|
-
|
|
588
|
-
LOG_FILE_TRANSFER_INFO("Rejected directory transfer: " << transfer_id << " (" << pending_transfer.metadata.directory_name << " from " << pending_transfer.peer_id << ", reason: " << reason << ")");
|
|
589
|
-
return true;
|
|
480
|
+
LOG_FT_INFO("Transfer " << t->id << " " << (success ? "completed" : "ended")
|
|
481
|
+
<< (error.empty() ? "" : (": " + error)));
|
|
590
482
|
}
|
|
591
483
|
|
|
592
|
-
|
|
593
|
-
std::
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
auto&
|
|
601
|
-
|
|
602
|
-
progress->status = FileTransferStatus::PAUSED;
|
|
603
|
-
|
|
604
|
-
// Send pause control message
|
|
605
|
-
nlohmann::json control_msg = create_control_message(transfer_id, "pause");
|
|
606
|
-
client_.send(progress->peer_id, "file_transfer_control", control_msg);
|
|
607
|
-
|
|
608
|
-
LOG_FILE_TRANSFER_INFO("Paused transfer: " << transfer_id);
|
|
609
|
-
return true;
|
|
484
|
+
void FileTransferManager::cleanup_temp_files(const std::shared_ptr<Transfer>& t) {
|
|
485
|
+
std::vector<std::string> temps;
|
|
486
|
+
{
|
|
487
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
488
|
+
for (auto& f : t->files) {
|
|
489
|
+
if (f.temp_created && !f.finalized) temps.push_back(f.temp_path);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
for (auto& p : temps) {
|
|
493
|
+
if (file_exists(p)) delete_file(p.c_str());
|
|
610
494
|
}
|
|
611
|
-
|
|
612
|
-
return false;
|
|
613
495
|
}
|
|
614
496
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
if (
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
497
|
+
// =============================================================================
|
|
498
|
+
// Sending: send_file / send_directory
|
|
499
|
+
// =============================================================================
|
|
500
|
+
|
|
501
|
+
namespace {
|
|
502
|
+
// Recursively collects regular files under `abs_dir` into `out`.
|
|
503
|
+
void scan_directory(const std::string& abs_dir, const std::string& rel_prefix,
|
|
504
|
+
std::vector<TransferFile>& out) {
|
|
505
|
+
std::vector<DirectoryEntry> entries;
|
|
506
|
+
if (!list_directory(abs_dir.c_str(), entries)) return;
|
|
507
|
+
for (const auto& e : entries) {
|
|
508
|
+
std::string rel = rel_prefix.empty() ? e.name : rel_prefix + "/" + e.name;
|
|
509
|
+
if (e.is_directory) {
|
|
510
|
+
scan_directory(e.path, rel, out);
|
|
511
|
+
} else {
|
|
512
|
+
TransferFile f;
|
|
513
|
+
f.relative_path = rel;
|
|
514
|
+
int64_t sz = get_file_size(e.path.c_str());
|
|
515
|
+
f.size = sz > 0 ? static_cast<uint64_t>(sz) : 0;
|
|
516
|
+
f.source_path = e.path;
|
|
517
|
+
out.push_back(std::move(f));
|
|
631
518
|
}
|
|
632
|
-
work_condition_.notify_one();
|
|
633
|
-
|
|
634
|
-
// Send resume control message
|
|
635
|
-
nlohmann::json control_msg = create_control_message(transfer_id, "resume");
|
|
636
|
-
client_.send(progress->peer_id, "file_transfer_control", control_msg);
|
|
637
|
-
|
|
638
|
-
LOG_FILE_TRANSFER_INFO("Resumed transfer: " << transfer_id);
|
|
639
|
-
return true;
|
|
640
519
|
}
|
|
641
|
-
|
|
642
|
-
return false;
|
|
643
520
|
}
|
|
521
|
+
} // namespace
|
|
644
522
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
523
|
+
std::string FileTransferManager::send_file(const std::string& peer_id, const std::string& file_path,
|
|
524
|
+
const std::string& remote_name) {
|
|
525
|
+
if (!file_exists(file_path) || is_directory(file_path.c_str())) {
|
|
526
|
+
LOG_FT_ERROR("send_file: not a readable file: " << file_path);
|
|
527
|
+
return "";
|
|
528
|
+
}
|
|
529
|
+
int64_t sz = get_file_size(file_path.c_str());
|
|
530
|
+
if (sz < 0) {
|
|
531
|
+
LOG_FT_ERROR("send_file: cannot stat: " << file_path);
|
|
532
|
+
return "";
|
|
533
|
+
}
|
|
534
|
+
std::string name = remote_name.empty() ? get_filename_from_path(file_path) : remote_name;
|
|
535
|
+
|
|
536
|
+
auto t = std::make_shared<Transfer>();
|
|
537
|
+
t->id = random_transfer_id();
|
|
538
|
+
t->peer_id = peer_id;
|
|
539
|
+
t->direction = FileTransferDirection::SENDING;
|
|
540
|
+
t->is_directory = false;
|
|
541
|
+
t->name = name;
|
|
542
|
+
t->local_root = file_path;
|
|
543
|
+
t->total_bytes = static_cast<uint64_t>(sz);
|
|
544
|
+
|
|
545
|
+
TransferFile f;
|
|
546
|
+
f.relative_path = name;
|
|
547
|
+
f.size = t->total_bytes;
|
|
548
|
+
f.source_path = file_path;
|
|
549
|
+
t->files.push_back(std::move(f));
|
|
550
|
+
|
|
551
|
+
return start_send(peer_id, t, name);
|
|
665
552
|
}
|
|
666
553
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
return false;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
auto progress = it->second;
|
|
677
|
-
if (progress->status != FileTransferStatus::FAILED) {
|
|
678
|
-
return false;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// Reset progress and move back to active
|
|
682
|
-
progress->status = FileTransferStatus::STARTING;
|
|
683
|
-
progress->bytes_transferred = 0;
|
|
684
|
-
progress->chunks_completed = 0;
|
|
685
|
-
progress->retry_count++;
|
|
686
|
-
progress->error_message.clear();
|
|
687
|
-
progress->start_time = std::chrono::steady_clock::now();
|
|
688
|
-
|
|
689
|
-
active_transfers_[transfer_id] = progress;
|
|
690
|
-
completed_transfers_.erase(it);
|
|
691
|
-
|
|
692
|
-
// Add to work queue
|
|
693
|
-
{
|
|
694
|
-
std::lock_guard<std::mutex> work_lock(work_mutex_);
|
|
695
|
-
work_queue_.push(transfer_id);
|
|
554
|
+
std::string FileTransferManager::send_directory(const std::string& peer_id,
|
|
555
|
+
const std::string& directory_path,
|
|
556
|
+
const std::string& remote_name) {
|
|
557
|
+
if (!directory_exists(directory_path)) {
|
|
558
|
+
LOG_FT_ERROR("send_directory: not a directory: " << directory_path);
|
|
559
|
+
return "";
|
|
696
560
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
561
|
+
std::string name = remote_name.empty() ? get_filename_from_path(directory_path) : remote_name;
|
|
562
|
+
|
|
563
|
+
auto t = std::make_shared<Transfer>();
|
|
564
|
+
t->id = random_transfer_id();
|
|
565
|
+
t->peer_id = peer_id;
|
|
566
|
+
t->direction = FileTransferDirection::SENDING;
|
|
567
|
+
t->is_directory = true;
|
|
568
|
+
t->name = name;
|
|
569
|
+
t->local_root = directory_path;
|
|
570
|
+
|
|
571
|
+
scan_directory(directory_path, "", t->files);
|
|
572
|
+
for (auto& f : t->files) t->total_bytes += f.size;
|
|
573
|
+
|
|
574
|
+
return start_send(peer_id, t, name);
|
|
701
575
|
}
|
|
702
576
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
577
|
+
// Registers a freshly-built sending transfer and sends its offer.
|
|
578
|
+
std::string FileTransferManager::start_send(const std::string& peer_id,
|
|
579
|
+
const std::shared_ptr<Transfer>& t,
|
|
580
|
+
const std::string& /*name*/) {
|
|
581
|
+
t->status = FileTransferStatus::STARTING;
|
|
582
|
+
t->start_time = std::chrono::steady_clock::now();
|
|
583
|
+
t->last_activity = t->start_time;
|
|
584
|
+
t->rate_mark_time = t->start_time;
|
|
585
|
+
|
|
586
|
+
{
|
|
587
|
+
std::lock_guard<std::mutex> lk(transfers_mutex_);
|
|
588
|
+
transfers_[t->id] = t;
|
|
709
589
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
590
|
+
|
|
591
|
+
nlohmann::json offer;
|
|
592
|
+
offer["transfer_id"] = t->id;
|
|
593
|
+
offer["name"] = t->name;
|
|
594
|
+
offer["is_directory"] = t->is_directory;
|
|
595
|
+
offer["total_size"] = t->total_bytes;
|
|
596
|
+
nlohmann::json files = nlohmann::json::array();
|
|
597
|
+
for (const auto& f : t->files) {
|
|
598
|
+
files.push_back({{"path", f.relative_path}, {"size", f.size}});
|
|
714
599
|
}
|
|
715
|
-
|
|
716
|
-
return nullptr;
|
|
717
|
-
}
|
|
600
|
+
offer["files"] = files;
|
|
718
601
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
for (const auto& pair : active_transfers_) {
|
|
726
|
-
transfers.push_back(pair.second);
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
return transfers;
|
|
602
|
+
client_.send(peer_id, MSG_OFFER, offer);
|
|
603
|
+
LOG_FT_INFO("Offering " << (t->is_directory ? "directory '" : "file '") << t->name
|
|
604
|
+
<< "' (" << t->files.size() << " file(s), " << t->total_bytes
|
|
605
|
+
<< " bytes) to " << peer_id << " [" << t->id << "]");
|
|
606
|
+
return t->id;
|
|
730
607
|
}
|
|
731
608
|
|
|
732
|
-
std::vector<std::shared_ptr<FileTransferProgress>> FileTransferManager::get_transfer_history(size_t limit) const {
|
|
733
|
-
std::lock_guard<std::mutex> lock(transfers_mutex_);
|
|
734
|
-
|
|
735
|
-
std::vector<std::shared_ptr<FileTransferProgress>> transfers;
|
|
736
|
-
|
|
737
|
-
for (const auto& pair : completed_transfers_) {
|
|
738
|
-
transfers.push_back(pair.second);
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// Sort by completion time (most recent first)
|
|
742
|
-
std::sort(transfers.begin(), transfers.end(),
|
|
743
|
-
[](const auto& a, const auto& b) {
|
|
744
|
-
return a->last_update > b->last_update;
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
if (limit > 0 && transfers.size() > limit) {
|
|
748
|
-
transfers.resize(limit);
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
return transfers;
|
|
752
|
-
}
|
|
753
609
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
LOG_FILE_TRANSFER_INFO("Cleared transfer history");
|
|
758
|
-
}
|
|
610
|
+
// =============================================================================
|
|
611
|
+
// Sending: streaming worker
|
|
612
|
+
// =============================================================================
|
|
759
613
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
auto now = std::chrono::steady_clock::now();
|
|
765
|
-
auto uptime = std::chrono::duration_cast<std::chrono::seconds>(now - start_time_);
|
|
766
|
-
|
|
767
|
-
nlohmann::json stats;
|
|
768
|
-
stats["uptime_seconds"] = uptime.count();
|
|
769
|
-
stats["total_bytes_sent"] = total_bytes_sent_;
|
|
770
|
-
stats["total_bytes_received"] = total_bytes_received_;
|
|
771
|
-
stats["total_files_sent"] = total_files_sent_;
|
|
772
|
-
stats["total_files_received"] = total_files_received_;
|
|
773
|
-
stats["active_transfers"] = active_transfers_.size();
|
|
774
|
-
stats["completed_transfers"] = completed_transfers_.size();
|
|
775
|
-
|
|
776
|
-
// Calculate success rate
|
|
777
|
-
size_t successful_transfers = 0;
|
|
778
|
-
for (const auto& pair : completed_transfers_) {
|
|
779
|
-
if (pair.second->status == FileTransferStatus::COMPLETED) {
|
|
780
|
-
successful_transfers++;
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
if (!completed_transfers_.empty()) {
|
|
785
|
-
stats["success_rate"] = static_cast<double>(successful_transfers) / completed_transfers_.size();
|
|
786
|
-
} else {
|
|
787
|
-
stats["success_rate"] = 0.0;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
// Average transfer rate
|
|
791
|
-
if (uptime.count() > 0) {
|
|
792
|
-
stats["average_send_rate_bps"] = static_cast<double>(total_bytes_sent_) / uptime.count();
|
|
793
|
-
stats["average_receive_rate_bps"] = static_cast<double>(total_bytes_received_) / uptime.count();
|
|
794
|
-
} else {
|
|
795
|
-
stats["average_send_rate_bps"] = 0.0;
|
|
796
|
-
stats["average_receive_rate_bps"] = 0.0;
|
|
614
|
+
void FileTransferManager::queue_send(const std::string& id) {
|
|
615
|
+
{
|
|
616
|
+
std::lock_guard<std::mutex> lk(queue_mutex_);
|
|
617
|
+
send_queue_.push(id);
|
|
797
618
|
}
|
|
798
|
-
|
|
799
|
-
return stats;
|
|
619
|
+
queue_cv_.notify_one();
|
|
800
620
|
}
|
|
801
621
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
622
|
+
void FileTransferManager::worker_loop() {
|
|
623
|
+
while (running_.load()) {
|
|
624
|
+
std::string id;
|
|
625
|
+
{
|
|
626
|
+
std::unique_lock<std::mutex> lk(queue_mutex_);
|
|
627
|
+
queue_cv_.wait(lk, [this] { return !running_.load() || !send_queue_.empty(); });
|
|
628
|
+
if (!running_.load()) return;
|
|
629
|
+
id = send_queue_.front();
|
|
630
|
+
send_queue_.pop();
|
|
810
631
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
sha1.update(reinterpret_cast<const uint8_t*>(file_data), file_size);
|
|
814
|
-
std::string result = sha1.finalize();
|
|
815
|
-
|
|
816
|
-
free_file_buffer(file_data);
|
|
817
|
-
return result;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
// For MD5 or other algorithms, we would need additional implementations
|
|
821
|
-
return "";
|
|
822
|
-
}
|
|
632
|
+
auto t = find(id);
|
|
633
|
+
if (!t) continue;
|
|
823
634
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
if (!file_or_directory_exists(file_path.c_str())) {
|
|
829
|
-
return metadata;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
metadata.filename = get_filename_from_path(file_path.c_str());
|
|
833
|
-
int64_t file_size = get_file_size(file_path.c_str());
|
|
834
|
-
if (file_size >= 0) {
|
|
835
|
-
metadata.file_size = static_cast<uint64_t>(file_size);
|
|
635
|
+
{
|
|
636
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
637
|
+
if (t->worker_active) continue; // another worker already owns it
|
|
638
|
+
t->worker_active = true;
|
|
836
639
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
// Calculate checksum (optional, can be expensive for large files)
|
|
842
|
-
if (metadata.file_size < 100 * 1024 * 1024) { // Only for files < 100MB
|
|
843
|
-
metadata.checksum = calculate_file_checksum(file_path, "sha256");
|
|
640
|
+
run_send(t);
|
|
641
|
+
{
|
|
642
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
643
|
+
t->worker_active = false;
|
|
844
644
|
}
|
|
845
|
-
|
|
846
|
-
// Determine MIME type based on extension
|
|
847
|
-
metadata.mime_type = get_mime_type(file_path);
|
|
848
|
-
|
|
849
|
-
} catch (const std::exception& e) {
|
|
850
|
-
LOG_FILE_TRANSFER_ERROR("Failed to get file metadata for " << file_path << ": " << e.what());
|
|
851
645
|
}
|
|
852
|
-
|
|
853
|
-
return metadata;
|
|
854
646
|
}
|
|
855
647
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
}
|
|
870
|
-
else if (recursive) {
|
|
871
|
-
DirectoryMetadata subdir_meta = get_directory_metadata(entry.path, true);
|
|
872
|
-
subdir_meta.relative_path = entry.name;
|
|
873
|
-
metadata.subdirectories.push_back(subdir_meta);
|
|
874
|
-
}
|
|
648
|
+
void FileTransferManager::run_send(const std::shared_ptr<Transfer>& t) {
|
|
649
|
+
FileTransferConfig cfg = get_config();
|
|
650
|
+
std::vector<uint8_t> buf(cfg.chunk_size);
|
|
651
|
+
|
|
652
|
+
while (running_.load()) {
|
|
653
|
+
size_t file_index;
|
|
654
|
+
uint64_t offset;
|
|
655
|
+
uint64_t file_size;
|
|
656
|
+
std::string source_path;
|
|
657
|
+
{
|
|
658
|
+
std::unique_lock<std::mutex> lk(t->mtx);
|
|
659
|
+
if (t->status == FileTransferStatus::RESUMING) {
|
|
660
|
+
t->status = FileTransferStatus::IN_PROGRESS;
|
|
875
661
|
}
|
|
662
|
+
if (t->status == FileTransferStatus::PAUSED) return; // cursor preserved
|
|
663
|
+
if (t->status != FileTransferStatus::IN_PROGRESS) return; // cancelled / failed
|
|
664
|
+
if (t->send_file >= t->files.size()) break; // all data sent
|
|
665
|
+
|
|
666
|
+
file_index = t->send_file;
|
|
667
|
+
offset = t->send_offset;
|
|
668
|
+
file_size = t->files[file_index].size;
|
|
669
|
+
source_path = t->files[file_index].source_path;
|
|
670
|
+
if (offset == 0) sha256_reset(&t->send_hash);
|
|
876
671
|
}
|
|
877
|
-
|
|
878
|
-
} catch (const std::exception& e) {
|
|
879
|
-
LOG_FILE_TRANSFER_ERROR("Failed to get directory metadata for " << directory_path << ": " << e.what());
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
return metadata;
|
|
883
|
-
}
|
|
884
672
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
}
|
|
673
|
+
// Empty file: no chunks, just the end marker.
|
|
674
|
+
if (file_size == 0) {
|
|
675
|
+
nlohmann::json end{{"transfer_id", t->id}, {"file_index", file_index},
|
|
676
|
+
{"sha256", sha256_of_empty()}};
|
|
677
|
+
client_.send(t->peer_id, MSG_FILE_END, end);
|
|
678
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
679
|
+
t->send_file++;
|
|
680
|
+
t->send_offset = 0;
|
|
681
|
+
t->files_done++;
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
uint32_t want = static_cast<uint32_t>(std::min<uint64_t>(cfg.chunk_size, file_size - offset));
|
|
686
|
+
if (!read_file_chunk(source_path, offset, buf.data(), want)) {
|
|
687
|
+
nlohmann::json done{{"transfer_id", t->id}, {"success", false},
|
|
688
|
+
{"error", "sender failed to read file"}};
|
|
689
|
+
client_.send(t->peer_id, MSG_COMPLETE, done);
|
|
690
|
+
finish(t, false, "failed to read " + source_path);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
888
693
|
|
|
889
|
-
|
|
694
|
+
uint32_t crc = crc32(buf.data(), want);
|
|
695
|
+
|
|
696
|
+
// Build the chunk frame: magic | id | file_index | offset | len | crc | data.
|
|
697
|
+
std::vector<uint8_t> frame;
|
|
698
|
+
frame.reserve(4 + 2 + t->id.size() + 4 + 8 + 4 + 4 + want);
|
|
699
|
+
frame.insert(frame.end(), CHUNK_MAGIC, CHUNK_MAGIC + 4);
|
|
700
|
+
put_u16(frame, static_cast<uint16_t>(t->id.size()));
|
|
701
|
+
frame.insert(frame.end(), t->id.begin(), t->id.end());
|
|
702
|
+
put_u32(frame, static_cast<uint32_t>(file_index));
|
|
703
|
+
put_u64(frame, offset);
|
|
704
|
+
put_u32(frame, want);
|
|
705
|
+
put_u32(frame, crc);
|
|
706
|
+
frame.insert(frame.end(), buf.data(), buf.data() + want);
|
|
707
|
+
|
|
708
|
+
bool sent = client_.send_binary_to_peer_id(t->peer_id, frame, MessageDataType::BINARY);
|
|
709
|
+
if (!sent) {
|
|
710
|
+
finish(t, false, "connection lost while sending");
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
890
713
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
714
|
+
bool file_done = false;
|
|
715
|
+
std::string sha_hex;
|
|
716
|
+
{
|
|
717
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
718
|
+
sha256_update(&t->send_hash, buf.data(), want);
|
|
719
|
+
t->send_offset += want;
|
|
720
|
+
t->bytes_done += want;
|
|
721
|
+
t->last_activity = std::chrono::steady_clock::now();
|
|
722
|
+
if (t->send_offset >= file_size) {
|
|
723
|
+
uint8_t digest[SHA256_HASH_SIZE];
|
|
724
|
+
sha256_finish(&t->send_hash, digest);
|
|
725
|
+
sha_hex = to_hex(digest, SHA256_HASH_SIZE);
|
|
726
|
+
t->send_file++;
|
|
727
|
+
t->send_offset = 0;
|
|
728
|
+
t->files_done++;
|
|
729
|
+
file_done = true;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
{
|
|
733
|
+
std::lock_guard<std::mutex> lk(stats_mutex_);
|
|
734
|
+
stat_bytes_sent_ += want;
|
|
735
|
+
}
|
|
736
|
+
if (file_done) {
|
|
737
|
+
nlohmann::json end{{"transfer_id", t->id}, {"file_index", file_index},
|
|
738
|
+
{"sha256", sha_hex}};
|
|
739
|
+
client_.send(t->peer_id, MSG_FILE_END, end);
|
|
740
|
+
}
|
|
904
741
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
// Check again before processing (in case shutdown occurred during progress retrieval)
|
|
917
|
-
if (!running_.load()) {
|
|
918
|
-
return;
|
|
742
|
+
emit_progress(t);
|
|
743
|
+
|
|
744
|
+
// Backpressure: do not get more than `window_bytes` ahead of the peer.
|
|
745
|
+
{
|
|
746
|
+
std::unique_lock<std::mutex> lk(t->mtx);
|
|
747
|
+
while (running_.load() && t->status == FileTransferStatus::IN_PROGRESS &&
|
|
748
|
+
t->bytes_done - t->acked_bytes >= cfg.window_bytes) {
|
|
749
|
+
t->cv.wait_for(lk, std::chrono::milliseconds(200));
|
|
750
|
+
}
|
|
751
|
+
}
|
|
919
752
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
753
|
+
|
|
754
|
+
if (!running_.load()) return;
|
|
755
|
+
|
|
756
|
+
// All data streamed; wait for the receiver's ft_complete.
|
|
923
757
|
{
|
|
924
|
-
std::
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
if (is_directory_transfer) {
|
|
929
|
-
if (progress->direction == FileTransferDirection::SENDING) {
|
|
930
|
-
start_directory_send(transfer_id);
|
|
931
|
-
} else {
|
|
932
|
-
start_directory_receive(transfer_id);
|
|
933
|
-
}
|
|
934
|
-
} else {
|
|
935
|
-
if (progress->direction == FileTransferDirection::SENDING) {
|
|
936
|
-
start_file_send(transfer_id);
|
|
937
|
-
} else {
|
|
938
|
-
start_file_receive(transfer_id);
|
|
758
|
+
std::unique_lock<std::mutex> lk(t->mtx);
|
|
759
|
+
while (running_.load() && t->status == FileTransferStatus::IN_PROGRESS) {
|
|
760
|
+
t->cv.wait_for(lk, std::chrono::milliseconds(200));
|
|
939
761
|
}
|
|
940
762
|
}
|
|
941
763
|
}
|
|
942
764
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
765
|
+
// =============================================================================
|
|
766
|
+
// Receiving: offer handling
|
|
767
|
+
// =============================================================================
|
|
768
|
+
|
|
769
|
+
void FileTransferManager::on_offer(const std::string& peer_id, const nlohmann::json& msg) {
|
|
770
|
+
try {
|
|
771
|
+
std::string id = msg.at("transfer_id").get<std::string>();
|
|
772
|
+
if (find(id)) return; // duplicate offer
|
|
773
|
+
|
|
774
|
+
auto t = std::make_shared<Transfer>();
|
|
775
|
+
t->id = id;
|
|
776
|
+
t->peer_id = peer_id;
|
|
777
|
+
t->direction = FileTransferDirection::RECEIVING;
|
|
778
|
+
t->is_directory = msg.value("is_directory", false);
|
|
779
|
+
t->name = msg.value("name", std::string("transfer"));
|
|
780
|
+
t->total_bytes = msg.value("total_size", uint64_t(0));
|
|
781
|
+
t->status = FileTransferStatus::PENDING;
|
|
782
|
+
t->start_time = std::chrono::steady_clock::now();
|
|
783
|
+
t->last_activity = t->start_time;
|
|
784
|
+
t->rate_mark_time = t->start_time;
|
|
785
|
+
|
|
786
|
+
bool unsafe = false;
|
|
787
|
+
for (const auto& fj : msg.at("files")) {
|
|
788
|
+
TransferFile f;
|
|
789
|
+
f.relative_path = fj.at("path").get<std::string>();
|
|
790
|
+
f.size = fj.value("size", uint64_t(0));
|
|
791
|
+
if (!is_safe_relative_path(f.relative_path)) unsafe = true;
|
|
792
|
+
t->files.push_back(std::move(f));
|
|
962
793
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
chunk.chunk_index = chunk_index;
|
|
970
|
-
chunk.total_chunks = progress->total_chunks;
|
|
971
|
-
chunk.chunk_size = chunk_size;
|
|
972
|
-
chunk.file_offset = file_offset;
|
|
973
|
-
chunk.data.resize(chunk_size);
|
|
974
|
-
|
|
975
|
-
// Read chunk data
|
|
976
|
-
if (!read_file_chunk(progress->local_path.c_str(), file_offset, chunk.data.data(), chunk_size)) {
|
|
977
|
-
complete_transfer(transfer_id, false, "Failed to read complete chunk from file");
|
|
794
|
+
|
|
795
|
+
if (unsafe) {
|
|
796
|
+
LOG_FT_WARN("Rejecting offer " << id << " from " << peer_id << ": unsafe path in manifest");
|
|
797
|
+
nlohmann::json resp{{"transfer_id", id}, {"accepted", false},
|
|
798
|
+
{"reason", "unsafe path in manifest"}};
|
|
799
|
+
client_.send(peer_id, MSG_RESPONSE, resp);
|
|
978
800
|
return;
|
|
979
801
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
802
|
+
|
|
803
|
+
{
|
|
804
|
+
std::lock_guard<std::mutex> lk(transfers_mutex_);
|
|
805
|
+
transfers_[id] = t;
|
|
984
806
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
if (
|
|
1004
|
-
|
|
1005
|
-
|
|
807
|
+
|
|
808
|
+
IncomingTransferOffer offer;
|
|
809
|
+
offer.transfer_id = id;
|
|
810
|
+
offer.peer_id = peer_id;
|
|
811
|
+
offer.name = t->name;
|
|
812
|
+
offer.is_directory = t->is_directory;
|
|
813
|
+
offer.total_size = t->total_bytes;
|
|
814
|
+
for (const auto& f : t->files) offer.files.push_back({f.relative_path, f.size});
|
|
815
|
+
|
|
816
|
+
LOG_FT_INFO("Incoming offer [" << id << "] '" << t->name << "' from " << peer_id
|
|
817
|
+
<< " (" << t->files.size() << " file(s), "
|
|
818
|
+
<< t->total_bytes << " bytes)");
|
|
819
|
+
|
|
820
|
+
TransferOfferCallback cb;
|
|
821
|
+
{
|
|
822
|
+
std::lock_guard<std::mutex> lk(callbacks_mutex_);
|
|
823
|
+
cb = offer_callback_;
|
|
824
|
+
}
|
|
825
|
+
if (cb) {
|
|
826
|
+
cb(offer);
|
|
827
|
+
} else {
|
|
828
|
+
reject(id, "no offer handler configured");
|
|
1006
829
|
}
|
|
830
|
+
} catch (const std::exception& e) {
|
|
831
|
+
LOG_FT_ERROR("Malformed ft_offer from " << peer_id << ": " << e.what());
|
|
1007
832
|
}
|
|
1008
|
-
|
|
1009
|
-
LOG_FILE_TRANSFER_INFO("Completed sending file chunks for transfer: " << transfer_id);
|
|
1010
833
|
}
|
|
1011
834
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
if (!
|
|
1015
|
-
|
|
835
|
+
bool FileTransferManager::accept(const std::string& transfer_id, const std::string& local_path) {
|
|
836
|
+
auto t = find(transfer_id);
|
|
837
|
+
if (!t || t->direction != FileTransferDirection::RECEIVING) return false;
|
|
838
|
+
|
|
839
|
+
FileTransferConfig cfg = get_config();
|
|
840
|
+
bool empty_transfer = false;
|
|
841
|
+
{
|
|
842
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
843
|
+
if (t->status != FileTransferStatus::PENDING) return false;
|
|
844
|
+
t->local_root = local_path;
|
|
845
|
+
for (size_t i = 0; i < t->files.size(); ++i) {
|
|
846
|
+
TransferFile& f = t->files[i];
|
|
847
|
+
f.final_path = t->is_directory ? combine_paths(local_path, f.relative_path)
|
|
848
|
+
: local_path;
|
|
849
|
+
f.temp_path = combine_paths(cfg.temp_directory,
|
|
850
|
+
transfer_id + "." + std::to_string(i) + ".part");
|
|
851
|
+
}
|
|
852
|
+
t->status = FileTransferStatus::IN_PROGRESS;
|
|
853
|
+
t->start_time = std::chrono::steady_clock::now();
|
|
854
|
+
t->last_activity = t->start_time;
|
|
855
|
+
t->rate_mark_time = t->start_time;
|
|
856
|
+
empty_transfer = t->files.empty();
|
|
1016
857
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
858
|
+
|
|
859
|
+
create_directories(cfg.temp_directory.c_str());
|
|
860
|
+
nlohmann::json resp{{"transfer_id", transfer_id}, {"accepted", true}};
|
|
861
|
+
client_.send(t->peer_id, MSG_RESPONSE, resp);
|
|
862
|
+
LOG_FT_INFO("Accepted transfer [" << transfer_id << "] -> " << local_path);
|
|
863
|
+
|
|
864
|
+
if (t->is_directory) create_directories(local_path.c_str());
|
|
865
|
+
|
|
866
|
+
emit_progress(t);
|
|
867
|
+
|
|
868
|
+
// A transfer with no files (e.g. an empty directory) is done immediately.
|
|
869
|
+
if (empty_transfer) {
|
|
870
|
+
nlohmann::json done{{"transfer_id", transfer_id}, {"success", true}};
|
|
871
|
+
client_.send(t->peer_id, MSG_COMPLETE, done);
|
|
872
|
+
finish(t, true, "");
|
|
1021
873
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
bool FileTransferManager::reject(const std::string& transfer_id, const std::string& reason) {
|
|
878
|
+
auto t = find(transfer_id);
|
|
879
|
+
if (!t || t->direction != FileTransferDirection::RECEIVING) return false;
|
|
880
|
+
{
|
|
881
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
882
|
+
if (t->status != FileTransferStatus::PENDING) return false;
|
|
883
|
+
t->status = FileTransferStatus::CANCELLED;
|
|
1029
884
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
885
|
+
nlohmann::json resp{{"transfer_id", transfer_id}, {"accepted", false}, {"reason", reason}};
|
|
886
|
+
client_.send(t->peer_id, MSG_RESPONSE, resp);
|
|
887
|
+
finish(t, false, reason.empty() ? "rejected" : ("rejected: " + reason));
|
|
888
|
+
return true;
|
|
1033
889
|
}
|
|
1034
890
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
891
|
+
// =============================================================================
|
|
892
|
+
// Receiving: chunk handling
|
|
893
|
+
// =============================================================================
|
|
894
|
+
|
|
895
|
+
bool FileTransferManager::handle_binary_data(const std::string& peer_id,
|
|
896
|
+
const std::vector<uint8_t>& data) {
|
|
897
|
+
if (data.size() < 4 || std::memcmp(data.data(), CHUNK_MAGIC, 4) != 0) {
|
|
898
|
+
return false; // not a file-transfer frame
|
|
899
|
+
}
|
|
900
|
+
const uint8_t* p = data.data();
|
|
901
|
+
size_t n = data.size();
|
|
902
|
+
size_t pos = 4;
|
|
903
|
+
|
|
904
|
+
if (pos + 2 > n) return true;
|
|
905
|
+
uint16_t id_len = get_u16(p + pos);
|
|
906
|
+
pos += 2;
|
|
907
|
+
if (pos + id_len > n) return true;
|
|
908
|
+
std::string id(reinterpret_cast<const char*>(p + pos), id_len);
|
|
909
|
+
pos += id_len;
|
|
910
|
+
if (pos + 4 + 8 + 4 + 4 > n) return true;
|
|
911
|
+
uint32_t file_index = get_u32(p + pos); pos += 4;
|
|
912
|
+
uint64_t offset = get_u64(p + pos); pos += 8;
|
|
913
|
+
uint32_t data_len = get_u32(p + pos); pos += 4;
|
|
914
|
+
uint32_t crc = get_u32(p + pos); pos += 4;
|
|
915
|
+
if (pos + data_len != n) {
|
|
916
|
+
LOG_FT_WARN("Chunk frame from " << peer_id << " has inconsistent length");
|
|
917
|
+
return true;
|
|
1039
918
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
919
|
+
on_chunk(id, peer_id, file_index, offset, p + pos, data_len, crc);
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
void FileTransferManager::on_chunk(const std::string& transfer_id, const std::string& peer_id,
|
|
924
|
+
uint32_t file_index, uint64_t offset,
|
|
925
|
+
const uint8_t* data, uint32_t len, uint32_t crc) {
|
|
926
|
+
auto t = find(transfer_id);
|
|
927
|
+
if (!t || t->direction != FileTransferDirection::RECEIVING) return;
|
|
928
|
+
FileTransferConfig cfg = get_config();
|
|
929
|
+
|
|
930
|
+
// Acknowledge at least twice per window so the sender never stalls waiting
|
|
931
|
+
// for a progress update it will not receive.
|
|
932
|
+
uint64_t ack_interval = std::min<uint64_t>(
|
|
933
|
+
cfg.progress_interval, std::max<uint32_t>(1, cfg.window_bytes / 2));
|
|
934
|
+
|
|
935
|
+
std::string temp_path;
|
|
936
|
+
uint64_t file_size = 0;
|
|
937
|
+
std::string fail_error;
|
|
1042
938
|
{
|
|
1043
|
-
std::lock_guard<std::mutex>
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
939
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
940
|
+
// Only accept chunks while the transfer is live (PAUSED still drains
|
|
941
|
+
// chunks already in flight).
|
|
942
|
+
if (t->status != FileTransferStatus::IN_PROGRESS &&
|
|
943
|
+
t->status != FileTransferStatus::PAUSED) {
|
|
1047
944
|
return;
|
|
1048
945
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
auto current_progress = get_transfer_progress(transfer_id);
|
|
1063
|
-
if (!current_progress || current_progress->status == FileTransferStatus::CANCELLED ||
|
|
1064
|
-
current_progress->status == FileTransferStatus::PAUSED) {
|
|
1065
|
-
return;
|
|
946
|
+
if (file_index >= t->files.size() || file_index != t->recv_file) {
|
|
947
|
+
// Strict ordering is guaranteed by the reliable transport.
|
|
948
|
+
fail_error = "out-of-order file index";
|
|
949
|
+
} else {
|
|
950
|
+
TransferFile& f = t->files[file_index];
|
|
951
|
+
if (offset != f.received) {
|
|
952
|
+
fail_error = "out-of-order chunk offset";
|
|
953
|
+
} else if (offset + len > f.size) {
|
|
954
|
+
fail_error = "chunk exceeds declared file size";
|
|
955
|
+
} else {
|
|
956
|
+
temp_path = f.temp_path;
|
|
957
|
+
file_size = f.size;
|
|
958
|
+
}
|
|
1066
959
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
std::string file_transfer_id = send_file_with_metadata(progress->peer_id, full_file_path, file_metadata);
|
|
1072
|
-
|
|
1073
|
-
if (file_transfer_id.empty()) {
|
|
1074
|
-
complete_transfer(transfer_id, false, "Failed to send file: " + file_metadata.filename);
|
|
1075
|
-
return;
|
|
960
|
+
if (!fail_error.empty()) {
|
|
961
|
+
t->status = FileTransferStatus::FAILED;
|
|
962
|
+
t->error = fail_error;
|
|
963
|
+
t->cv.notify_all();
|
|
1076
964
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
}
|
|
965
|
+
}
|
|
966
|
+
if (!fail_error.empty()) {
|
|
967
|
+
nlohmann::json done{{"transfer_id", transfer_id}, {"success", false},
|
|
968
|
+
{"error", fail_error}};
|
|
969
|
+
client_.send(peer_id, MSG_COMPLETE, done);
|
|
970
|
+
finish(t, false, fail_error);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
1086
973
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
974
|
+
if (cfg.verify_integrity && crc32(data, len) != crc) {
|
|
975
|
+
nlohmann::json done{{"transfer_id", transfer_id}, {"success", false},
|
|
976
|
+
{"error", "chunk CRC mismatch"}};
|
|
977
|
+
client_.send(peer_id, MSG_COMPLETE, done);
|
|
978
|
+
finish(t, false, "chunk CRC mismatch");
|
|
1090
979
|
return;
|
|
1091
980
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
981
|
+
|
|
982
|
+
// Create the temp file on first contact, pre-sized to the declared length.
|
|
1094
983
|
{
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
984
|
+
bool need_create;
|
|
985
|
+
{
|
|
986
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
987
|
+
need_create = !t->files[file_index].temp_created;
|
|
988
|
+
}
|
|
989
|
+
if (need_create) {
|
|
990
|
+
if (!create_file_with_size(temp_path.c_str(), file_size)) {
|
|
991
|
+
nlohmann::json done{{"transfer_id", transfer_id}, {"success", false},
|
|
992
|
+
{"error", "cannot create destination temp file"}};
|
|
993
|
+
client_.send(peer_id, MSG_COMPLETE, done);
|
|
994
|
+
finish(t, false, "cannot create temp file");
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
998
|
+
t->files[file_index].temp_created = true;
|
|
1100
999
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
if (!ensure_directory_exists(base_path)) {
|
|
1109
|
-
complete_transfer(transfer_id, false, "Failed to create local directory structure");
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (!write_file_chunk(temp_path, offset, data, len)) {
|
|
1003
|
+
nlohmann::json done{{"transfer_id", transfer_id}, {"success", false},
|
|
1004
|
+
{"error", "receiver failed to write to disk"}};
|
|
1005
|
+
client_.send(peer_id, MSG_COMPLETE, done);
|
|
1006
|
+
finish(t, false, "failed to write chunk to disk");
|
|
1110
1007
|
return;
|
|
1111
1008
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1009
|
+
|
|
1010
|
+
bool data_complete = false;
|
|
1011
|
+
bool send_ack = false;
|
|
1012
|
+
uint64_t ack_bytes = 0;
|
|
1013
|
+
{
|
|
1014
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
1015
|
+
TransferFile& f = t->files[file_index];
|
|
1016
|
+
if (f.received == 0) sha256_reset(&t->recv_hash);
|
|
1017
|
+
sha256_update(&t->recv_hash, data, len);
|
|
1018
|
+
f.received += len;
|
|
1019
|
+
t->bytes_done += len;
|
|
1020
|
+
t->last_activity = std::chrono::steady_clock::now();
|
|
1021
|
+
|
|
1022
|
+
if (f.received >= f.size) {
|
|
1023
|
+
uint8_t digest[SHA256_HASH_SIZE];
|
|
1024
|
+
sha256_finish(&t->recv_hash, digest);
|
|
1025
|
+
f.computed_sha = to_hex(digest, SHA256_HASH_SIZE);
|
|
1026
|
+
data_complete = true;
|
|
1027
|
+
}
|
|
1028
|
+
if (data_complete || t->bytes_done - t->last_ack_sent >= ack_interval) {
|
|
1029
|
+
t->last_ack_sent = t->bytes_done;
|
|
1030
|
+
ack_bytes = t->bytes_done;
|
|
1031
|
+
send_ack = true;
|
|
1119
1032
|
}
|
|
1120
1033
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1034
|
+
{
|
|
1035
|
+
std::lock_guard<std::mutex> lk(stats_mutex_);
|
|
1036
|
+
stat_bytes_received_ += len;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (send_ack) {
|
|
1040
|
+
nlohmann::json prog{{"transfer_id", transfer_id}, {"bytes_received", ack_bytes}};
|
|
1041
|
+
client_.send(peer_id, MSG_PROGRESS, prog);
|
|
1042
|
+
}
|
|
1043
|
+
emit_progress(t);
|
|
1044
|
+
|
|
1045
|
+
if (data_complete) try_finalize_file(t, file_index);
|
|
1127
1046
|
}
|
|
1128
1047
|
|
|
1129
|
-
|
|
1130
|
-
std::string temp_path = get_temp_file_path(transfer_id, config_.temp_directory);
|
|
1131
|
-
|
|
1048
|
+
void FileTransferManager::on_file_end(const std::string& /*peer_id*/, const nlohmann::json& msg) {
|
|
1132
1049
|
try {
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1050
|
+
std::string id = msg.at("transfer_id").get<std::string>();
|
|
1051
|
+
auto t = find(id);
|
|
1052
|
+
if (!t || t->direction != FileTransferDirection::RECEIVING) return;
|
|
1053
|
+
size_t file_index = msg.at("file_index").get<size_t>();
|
|
1054
|
+
std::string sha = msg.value("sha256", std::string());
|
|
1055
|
+
{
|
|
1056
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
1057
|
+
if (t->is_terminal() || file_index >= t->files.size()) return;
|
|
1058
|
+
TransferFile& f = t->files[file_index];
|
|
1059
|
+
if (f.finalized) return;
|
|
1060
|
+
f.expected_sha = sha;
|
|
1061
|
+
f.sha_known = true;
|
|
1062
|
+
// A zero-byte file produces no chunks, so hash it here.
|
|
1063
|
+
if (f.size == 0 && f.computed_sha.empty()) f.computed_sha = sha256_of_empty();
|
|
1139
1064
|
}
|
|
1140
|
-
|
|
1141
|
-
return true;
|
|
1065
|
+
try_finalize_file(t, file_index);
|
|
1142
1066
|
} catch (const std::exception& e) {
|
|
1143
|
-
|
|
1144
|
-
return false;
|
|
1067
|
+
LOG_FT_ERROR("Malformed ft_file_end: " << e.what());
|
|
1145
1068
|
}
|
|
1146
1069
|
}
|
|
1147
1070
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1071
|
+
// Moves a fully-received, verified file to its destination. When the last file
|
|
1072
|
+
// of the transfer is finalized, completes the whole transfer.
|
|
1073
|
+
void FileTransferManager::try_finalize_file(const std::shared_ptr<Transfer>& t, size_t file_index) {
|
|
1074
|
+
FileTransferConfig cfg = get_config();
|
|
1075
|
+
std::string final_path, temp_path, rel_path;
|
|
1076
|
+
bool do_finalize = false;
|
|
1077
|
+
bool sha_mismatch = false;
|
|
1078
|
+
bool zero_byte = false;
|
|
1079
|
+
{
|
|
1080
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
1081
|
+
if (t->is_terminal() || file_index >= t->files.size()) return;
|
|
1082
|
+
TransferFile& f = t->files[file_index];
|
|
1083
|
+
if (f.finalized) return;
|
|
1084
|
+
if (f.received < f.size) return; // data not complete yet
|
|
1085
|
+
if (!f.sha_known) return; // ft_file_end not received yet
|
|
1086
|
+
|
|
1087
|
+
if (cfg.verify_integrity && f.computed_sha != f.expected_sha) {
|
|
1088
|
+
sha_mismatch = true;
|
|
1089
|
+
} else {
|
|
1090
|
+
do_finalize = true;
|
|
1091
|
+
final_path = f.final_path;
|
|
1092
|
+
temp_path = f.temp_path;
|
|
1093
|
+
rel_path = f.relative_path;
|
|
1094
|
+
zero_byte = (f.size == 0);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1159
1097
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
{".xml", "application/xml"},
|
|
1169
|
-
{".html", "text/html"},
|
|
1170
|
-
{".css", "text/css"},
|
|
1171
|
-
{".js", "application/javascript"},
|
|
1172
|
-
{".pdf", "application/pdf"},
|
|
1173
|
-
{".png", "image/png"},
|
|
1174
|
-
{".jpg", "image/jpeg"},
|
|
1175
|
-
{".jpeg", "image/jpeg"},
|
|
1176
|
-
{".gif", "image/gif"},
|
|
1177
|
-
{".bmp", "image/bmp"},
|
|
1178
|
-
{".svg", "image/svg+xml"},
|
|
1179
|
-
{".mp3", "audio/mpeg"},
|
|
1180
|
-
{".wav", "audio/wav"},
|
|
1181
|
-
{".mp4", "video/mp4"},
|
|
1182
|
-
{".avi", "video/x-msvideo"},
|
|
1183
|
-
{".zip", "application/zip"},
|
|
1184
|
-
{".tar", "application/x-tar"},
|
|
1185
|
-
{".gz", "application/gzip"}
|
|
1186
|
-
};
|
|
1187
|
-
|
|
1188
|
-
auto it = mime_types.find(extension);
|
|
1189
|
-
return (it != mime_types.end()) ? it->second : "application/octet-stream";
|
|
1190
|
-
}
|
|
1098
|
+
if (sha_mismatch) {
|
|
1099
|
+
nlohmann::json done{{"transfer_id", t->id}, {"success", false},
|
|
1100
|
+
{"error", "SHA-256 mismatch"}};
|
|
1101
|
+
client_.send(t->peer_id, MSG_COMPLETE, done);
|
|
1102
|
+
finish(t, false, "SHA-256 mismatch for " + rel_path);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (!do_finalize) return;
|
|
1191
1106
|
|
|
1192
|
-
//
|
|
1107
|
+
// Place the file at its destination (parent directories created as needed).
|
|
1108
|
+
std::string parent = get_parent_directory(final_path.c_str());
|
|
1109
|
+
if (!parent.empty()) create_directories(parent.c_str());
|
|
1110
|
+
if (file_exists(final_path)) delete_file(final_path.c_str());
|
|
1193
1111
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
// Parse files and subdirectories from the metadata
|
|
1206
|
-
if (dir_info.contains("files")) {
|
|
1207
|
-
for (const auto& file_json : dir_info["files"]) {
|
|
1208
|
-
FileMetadata file_meta;
|
|
1209
|
-
file_meta.filename = file_json["filename"];
|
|
1210
|
-
file_meta.relative_path = file_json["relative_path"];
|
|
1211
|
-
file_meta.file_size = file_json["file_size"];
|
|
1212
|
-
file_meta.mime_type = file_json.value("mime_type", "application/octet-stream");
|
|
1213
|
-
file_meta.checksum = file_json.value("checksum", "");
|
|
1214
|
-
file_meta.last_modified = file_json.value("last_modified", 0);
|
|
1215
|
-
dir_metadata.files.push_back(file_meta);
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
// For now, handle as single directory level (can be expanded for nested later)
|
|
1220
|
-
|
|
1221
|
-
{
|
|
1222
|
-
std::lock_guard<std::mutex> lock(pending_mutex_);
|
|
1223
|
-
PendingDirectoryTransfer pending_transfer;
|
|
1224
|
-
pending_transfer.metadata = dir_metadata;
|
|
1225
|
-
pending_transfer.peer_id = peer_id;
|
|
1226
|
-
pending_directory_transfers_[transfer_id] = pending_transfer;
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
// Call user callback to approve/reject
|
|
1230
|
-
if (request_callback_) {
|
|
1231
|
-
// Create a dummy file metadata for compatibility with existing callback
|
|
1232
|
-
FileMetadata dummy_metadata;
|
|
1233
|
-
dummy_metadata.filename = dir_metadata.directory_name + " (directory)";
|
|
1234
|
-
dummy_metadata.file_size = dir_info.value("total_size", 0);
|
|
1235
|
-
|
|
1236
|
-
bool accepted = request_callback_(peer_id, dummy_metadata, transfer_id);
|
|
1237
|
-
if (accepted) {
|
|
1238
|
-
std::string local_path = "./" + dir_metadata.directory_name;
|
|
1239
|
-
accept_directory_transfer(transfer_id, local_path);
|
|
1240
|
-
} else {
|
|
1241
|
-
reject_directory_transfer(transfer_id, "Rejected by user");
|
|
1242
|
-
}
|
|
1243
|
-
} else {
|
|
1244
|
-
// Auto-reject if no callback is set
|
|
1245
|
-
reject_directory_transfer(transfer_id, "No request handler configured");
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
LOG_FILE_TRANSFER_INFO("Received directory transfer request from " << peer_id << " for " << dir_metadata.directory_name);
|
|
1249
|
-
return;
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
// Single file transfer request
|
|
1253
|
-
FileMetadata metadata;
|
|
1254
|
-
auto file_info = message["file_metadata"];
|
|
1255
|
-
metadata.filename = file_info["filename"];
|
|
1256
|
-
metadata.file_size = file_info["file_size"];
|
|
1257
|
-
metadata.mime_type = file_info.value("mime_type", "application/octet-stream");
|
|
1258
|
-
metadata.checksum = file_info.value("checksum", "");
|
|
1259
|
-
|
|
1260
|
-
{
|
|
1261
|
-
std::lock_guard<std::mutex> lock(pending_mutex_);
|
|
1262
|
-
PendingFileTransfer pending_transfer;
|
|
1263
|
-
pending_transfer.metadata = metadata;
|
|
1264
|
-
pending_transfer.peer_id = peer_id;
|
|
1265
|
-
pending_transfers_[transfer_id] = pending_transfer;
|
|
1112
|
+
bool ok;
|
|
1113
|
+
if (zero_byte) {
|
|
1114
|
+
ok = create_file_with_size(final_path.c_str(), 0);
|
|
1115
|
+
if (file_exists(temp_path)) delete_file(temp_path.c_str());
|
|
1116
|
+
} else {
|
|
1117
|
+
ok = rename_file(temp_path, final_path);
|
|
1118
|
+
if (!ok) {
|
|
1119
|
+
// rename fails across volumes; fall back to copy + delete.
|
|
1120
|
+
ok = copy_file(temp_path.c_str(), final_path.c_str());
|
|
1121
|
+
if (ok) delete_file(temp_path.c_str());
|
|
1266
1122
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1123
|
+
}
|
|
1124
|
+
if (!ok) {
|
|
1125
|
+
nlohmann::json done{{"transfer_id", t->id}, {"success", false},
|
|
1126
|
+
{"error", "cannot write destination file"}};
|
|
1127
|
+
client_.send(t->peer_id, MSG_COMPLETE, done);
|
|
1128
|
+
finish(t, false, "cannot move file to " + final_path);
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
bool all_done = false;
|
|
1133
|
+
{
|
|
1134
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
1135
|
+
TransferFile& f = t->files[file_index];
|
|
1136
|
+
f.finalized = true;
|
|
1137
|
+
t->files_done++;
|
|
1138
|
+
if (file_index == t->recv_file) t->recv_file++;
|
|
1139
|
+
all_done = true;
|
|
1140
|
+
for (const auto& ff : t->files) {
|
|
1141
|
+
if (!ff.finalized) { all_done = false; break; }
|
|
1282
1142
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1143
|
+
}
|
|
1144
|
+
emit_progress(t);
|
|
1145
|
+
|
|
1146
|
+
if (all_done) {
|
|
1147
|
+
nlohmann::json done{{"transfer_id", t->id}, {"success", true}};
|
|
1148
|
+
client_.send(t->peer_id, MSG_COMPLETE, done);
|
|
1149
|
+
finish(t, true, "");
|
|
1286
1150
|
}
|
|
1287
1151
|
}
|
|
1288
1152
|
|
|
1289
|
-
|
|
1153
|
+
// =============================================================================
|
|
1154
|
+
// Control message handlers
|
|
1155
|
+
// =============================================================================
|
|
1156
|
+
|
|
1157
|
+
void FileTransferManager::on_response(const std::string& /*peer_id*/, const nlohmann::json& msg) {
|
|
1290
1158
|
try {
|
|
1291
|
-
std::string
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
LOG_FILE_TRANSFER_ERROR("Received response for unknown transfer: " << transfer_id);
|
|
1297
|
-
return;
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1159
|
+
std::string id = msg.at("transfer_id").get<std::string>();
|
|
1160
|
+
auto t = find(id);
|
|
1161
|
+
if (!t || t->direction != FileTransferDirection::SENDING) return;
|
|
1162
|
+
bool accepted = msg.value("accepted", false);
|
|
1163
|
+
|
|
1300
1164
|
if (accepted) {
|
|
1301
|
-
// Start sending file
|
|
1302
1165
|
{
|
|
1303
|
-
std::lock_guard<std::mutex>
|
|
1304
|
-
|
|
1166
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
1167
|
+
if (t->status != FileTransferStatus::STARTING) return;
|
|
1168
|
+
t->status = FileTransferStatus::IN_PROGRESS;
|
|
1169
|
+
t->start_time = std::chrono::steady_clock::now();
|
|
1170
|
+
t->last_activity = t->start_time;
|
|
1171
|
+
t->rate_mark_time = t->start_time;
|
|
1305
1172
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
LOG_FILE_TRANSFER_INFO("Transfer accepted by peer: " << transfer_id);
|
|
1173
|
+
LOG_FT_INFO("Transfer " << id << " accepted by peer");
|
|
1174
|
+
queue_send(id);
|
|
1309
1175
|
} else {
|
|
1310
|
-
std::string reason =
|
|
1311
|
-
|
|
1176
|
+
std::string reason = msg.value("reason", std::string("rejected by peer"));
|
|
1177
|
+
finish(t, false, reason);
|
|
1312
1178
|
}
|
|
1313
|
-
|
|
1314
1179
|
} catch (const std::exception& e) {
|
|
1315
|
-
|
|
1180
|
+
LOG_FT_ERROR("Malformed ft_response: " << e.what());
|
|
1316
1181
|
}
|
|
1317
1182
|
}
|
|
1318
1183
|
|
|
1319
|
-
void FileTransferManager::
|
|
1184
|
+
void FileTransferManager::on_progress(const std::string& /*peer_id*/, const nlohmann::json& msg) {
|
|
1320
1185
|
try {
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
pending.chunk_size = message["chunk_size"];
|
|
1326
|
-
pending.file_offset = message["file_offset"];
|
|
1327
|
-
pending.checksum = message.value("checksum", "");
|
|
1328
|
-
pending.created_at = std::chrono::steady_clock::now();
|
|
1329
|
-
|
|
1330
|
-
// Store pending chunk metadata waiting for binary data
|
|
1186
|
+
std::string id = msg.at("transfer_id").get<std::string>();
|
|
1187
|
+
auto t = find(id);
|
|
1188
|
+
if (!t || t->direction != FileTransferDirection::SENDING) return;
|
|
1189
|
+
uint64_t acked = msg.value("bytes_received", uint64_t(0));
|
|
1331
1190
|
{
|
|
1332
|
-
std::lock_guard<std::mutex>
|
|
1333
|
-
|
|
1191
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
1192
|
+
if (acked > t->acked_bytes) t->acked_bytes = acked;
|
|
1193
|
+
t->last_activity = std::chrono::steady_clock::now();
|
|
1194
|
+
t->cv.notify_all(); // wake the streaming worker if it was throttled
|
|
1334
1195
|
}
|
|
1335
|
-
|
|
1336
|
-
LOG_FILE_TRANSFER_DEBUG("Stored chunk metadata for transfer " << pending.transfer_id <<
|
|
1337
|
-
", chunk " << pending.chunk_index << " from peer " << peer_id);
|
|
1338
|
-
|
|
1339
1196
|
} catch (const std::exception& e) {
|
|
1340
|
-
|
|
1197
|
+
LOG_FT_ERROR("Malformed ft_progress: " << e.what());
|
|
1341
1198
|
}
|
|
1342
1199
|
}
|
|
1343
1200
|
|
|
1344
|
-
void FileTransferManager::
|
|
1201
|
+
void FileTransferManager::on_complete(const std::string& /*peer_id*/, const nlohmann::json& msg) {
|
|
1345
1202
|
try {
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
if (!
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
// Get pending chunk metadata
|
|
1354
|
-
PendingChunk pending;
|
|
1355
|
-
bool found_pending = false;
|
|
1356
|
-
{
|
|
1357
|
-
std::lock_guard<std::mutex> lock(chunks_mutex_);
|
|
1358
|
-
auto it = pending_chunks_.find(peer_id);
|
|
1359
|
-
if (it != pending_chunks_.end()) {
|
|
1360
|
-
pending = it->second;
|
|
1361
|
-
pending_chunks_.erase(it);
|
|
1362
|
-
found_pending = true;
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
if (!found_pending) {
|
|
1367
|
-
LOG_FILE_TRANSFER_ERROR("No pending chunk metadata found for peer " << peer_id);
|
|
1368
|
-
return;
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
// Combine metadata with binary data
|
|
1372
|
-
chunk.transfer_id = pending.transfer_id;
|
|
1373
|
-
chunk.chunk_index = pending.chunk_index;
|
|
1374
|
-
chunk.total_chunks = pending.total_chunks;
|
|
1375
|
-
chunk.chunk_size = pending.chunk_size;
|
|
1376
|
-
chunk.file_offset = pending.file_offset;
|
|
1377
|
-
chunk.checksum = pending.checksum;
|
|
1378
|
-
|
|
1379
|
-
// Verify chunk size matches
|
|
1380
|
-
if (chunk.data.size() != pending.chunk_size) {
|
|
1381
|
-
LOG_FILE_TRANSFER_ERROR("Chunk size mismatch: expected " << pending.chunk_size <<
|
|
1382
|
-
", got " << chunk.data.size() << " for transfer " << pending.transfer_id);
|
|
1383
|
-
|
|
1384
|
-
// Send negative acknowledgment
|
|
1385
|
-
nlohmann::json ack_msg = create_chunk_ack_message(chunk.transfer_id, chunk.chunk_index, false, "Size mismatch");
|
|
1386
|
-
client_.send(peer_id, "file_chunk_ack", ack_msg);
|
|
1387
|
-
return;
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
// Verify checksum if enabled
|
|
1391
|
-
if (config_.verify_checksums && !chunk.checksum.empty()) {
|
|
1392
|
-
if (!verify_chunk_checksum(chunk)) {
|
|
1393
|
-
LOG_FILE_TRANSFER_ERROR("Chunk checksum verification failed for transfer " << chunk.transfer_id <<
|
|
1394
|
-
", chunk " << chunk.chunk_index);
|
|
1395
|
-
|
|
1396
|
-
// Send negative acknowledgment
|
|
1397
|
-
nlohmann::json ack_msg = create_chunk_ack_message(chunk.transfer_id, chunk.chunk_index, false, "Checksum mismatch");
|
|
1398
|
-
client_.send(peer_id, "file_chunk_ack", ack_msg);
|
|
1399
|
-
return;
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
// Process the received chunk
|
|
1404
|
-
handle_chunk_received(chunk);
|
|
1405
|
-
|
|
1406
|
-
// Send positive acknowledgment
|
|
1407
|
-
nlohmann::json ack_msg = create_chunk_ack_message(chunk.transfer_id, chunk.chunk_index, true);
|
|
1408
|
-
client_.send(peer_id, "file_chunk_ack", ack_msg);
|
|
1409
|
-
|
|
1410
|
-
LOG_FILE_TRANSFER_DEBUG("Successfully processed chunk " << chunk.chunk_index <<
|
|
1411
|
-
" for transfer " << chunk.transfer_id << " from peer " << peer_id);
|
|
1412
|
-
|
|
1203
|
+
std::string id = msg.at("transfer_id").get<std::string>();
|
|
1204
|
+
auto t = find(id);
|
|
1205
|
+
if (!t) return;
|
|
1206
|
+
bool success = msg.value("success", false);
|
|
1207
|
+
std::string error = msg.value("error", std::string());
|
|
1208
|
+
finish(t, success, success ? "" : (error.empty() ? "peer reported failure" : error));
|
|
1413
1209
|
} catch (const std::exception& e) {
|
|
1414
|
-
|
|
1210
|
+
LOG_FT_ERROR("Malformed ft_complete: " << e.what());
|
|
1415
1211
|
}
|
|
1416
1212
|
}
|
|
1417
1213
|
|
|
1418
|
-
void FileTransferManager::
|
|
1214
|
+
void FileTransferManager::on_control(const std::string& /*peer_id*/, const nlohmann::json& msg) {
|
|
1419
1215
|
try {
|
|
1420
|
-
std::string
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
handle_chunk_ack(transfer_id, chunk_index, success);
|
|
1425
|
-
|
|
1426
|
-
} catch (const std::exception& e) {
|
|
1427
|
-
LOG_FILE_TRANSFER_ERROR("Error handling chunk ack message: " << e.what());
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1216
|
+
std::string id = msg.at("transfer_id").get<std::string>();
|
|
1217
|
+
std::string action = msg.at("action").get<std::string>();
|
|
1218
|
+
auto t = find(id);
|
|
1219
|
+
if (!t) return;
|
|
1430
1220
|
|
|
1431
|
-
void FileTransferManager::handle_transfer_control(const std::string& peer_id, const nlohmann::json& message) {
|
|
1432
|
-
try {
|
|
1433
|
-
std::string transfer_id = message["transfer_id"];
|
|
1434
|
-
std::string action = message["action"];
|
|
1435
|
-
|
|
1436
1221
|
if (action == "pause") {
|
|
1437
|
-
|
|
1222
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
1223
|
+
if (t->status == FileTransferStatus::IN_PROGRESS) {
|
|
1224
|
+
t->status = FileTransferStatus::PAUSED;
|
|
1225
|
+
t->cv.notify_all();
|
|
1226
|
+
}
|
|
1438
1227
|
} else if (action == "resume") {
|
|
1439
|
-
|
|
1228
|
+
bool requeue = false;
|
|
1229
|
+
{
|
|
1230
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
1231
|
+
if (t->status == FileTransferStatus::PAUSED) {
|
|
1232
|
+
if (t->direction == FileTransferDirection::SENDING) {
|
|
1233
|
+
t->status = FileTransferStatus::RESUMING;
|
|
1234
|
+
requeue = true;
|
|
1235
|
+
} else {
|
|
1236
|
+
t->status = FileTransferStatus::IN_PROGRESS;
|
|
1237
|
+
}
|
|
1238
|
+
t->cv.notify_all();
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
if (requeue) queue_send(id);
|
|
1440
1242
|
} else if (action == "cancel") {
|
|
1441
|
-
|
|
1243
|
+
{
|
|
1244
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
1245
|
+
if (t->is_terminal()) return;
|
|
1246
|
+
t->status = FileTransferStatus::CANCELLED;
|
|
1247
|
+
t->cv.notify_all();
|
|
1248
|
+
}
|
|
1249
|
+
finish(t, false, "cancelled by peer");
|
|
1442
1250
|
}
|
|
1443
|
-
|
|
1444
1251
|
} catch (const std::exception& e) {
|
|
1445
|
-
|
|
1252
|
+
LOG_FT_ERROR("Malformed ft_control: " << e.what());
|
|
1446
1253
|
}
|
|
1447
1254
|
}
|
|
1448
1255
|
|
|
1449
|
-
//
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
nlohmann::json message;
|
|
1453
|
-
message["transfer_id"] = transfer_id;
|
|
1454
|
-
message["type"] = "file";
|
|
1455
|
-
message["file_metadata"] = {
|
|
1456
|
-
{"filename", metadata.filename},
|
|
1457
|
-
{"file_size", metadata.file_size},
|
|
1458
|
-
{"mime_type", metadata.mime_type},
|
|
1459
|
-
{"checksum", metadata.checksum},
|
|
1460
|
-
{"last_modified", metadata.last_modified}
|
|
1461
|
-
};
|
|
1462
|
-
return message;
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
nlohmann::json FileTransferManager::create_transfer_response_message(const std::string& transfer_id, bool accepted, const std::string& reason) {
|
|
1466
|
-
nlohmann::json message;
|
|
1467
|
-
message["transfer_id"] = transfer_id;
|
|
1468
|
-
message["accepted"] = accepted;
|
|
1469
|
-
if (!reason.empty()) {
|
|
1470
|
-
message["reason"] = reason;
|
|
1471
|
-
}
|
|
1472
|
-
return message;
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
nlohmann::json FileTransferManager::create_chunk_metadata_message(const FileChunk& chunk) {
|
|
1476
|
-
nlohmann::json message;
|
|
1477
|
-
message["transfer_id"] = chunk.transfer_id;
|
|
1478
|
-
message["chunk_index"] = chunk.chunk_index;
|
|
1479
|
-
message["total_chunks"] = chunk.total_chunks;
|
|
1480
|
-
message["chunk_size"] = chunk.chunk_size;
|
|
1481
|
-
message["file_offset"] = chunk.file_offset;
|
|
1482
|
-
message["checksum"] = chunk.checksum;
|
|
1483
|
-
|
|
1484
|
-
return message;
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
std::vector<uint8_t> FileTransferManager::create_chunk_binary_message(const FileChunk& chunk) {
|
|
1488
|
-
// Create binary message with header: "FTCHUNK" + chunk data
|
|
1489
|
-
const std::string magic = "FTCHUNK";
|
|
1490
|
-
std::vector<uint8_t> message;
|
|
1491
|
-
|
|
1492
|
-
// Reserve space for the entire message
|
|
1493
|
-
message.reserve(magic.length() + chunk.data.size());
|
|
1494
|
-
|
|
1495
|
-
// Add magic header
|
|
1496
|
-
message.insert(message.end(), magic.begin(), magic.end());
|
|
1497
|
-
|
|
1498
|
-
// Add chunk data
|
|
1499
|
-
message.insert(message.end(), chunk.data.begin(), chunk.data.end());
|
|
1500
|
-
|
|
1501
|
-
return message;
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
nlohmann::json FileTransferManager::create_chunk_ack_message(const std::string& transfer_id, uint64_t chunk_index, bool success, const std::string& error) {
|
|
1505
|
-
nlohmann::json message;
|
|
1506
|
-
message["transfer_id"] = transfer_id;
|
|
1507
|
-
message["chunk_index"] = chunk_index;
|
|
1508
|
-
message["success"] = success;
|
|
1509
|
-
if (!error.empty()) {
|
|
1510
|
-
message["error"] = error;
|
|
1511
|
-
}
|
|
1512
|
-
return message;
|
|
1513
|
-
}
|
|
1256
|
+
// =============================================================================
|
|
1257
|
+
// Transfer control (local side)
|
|
1258
|
+
// =============================================================================
|
|
1514
1259
|
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
if (!data.empty()) {
|
|
1520
|
-
message["data"] = data;
|
|
1521
|
-
}
|
|
1522
|
-
return message;
|
|
1260
|
+
void FileTransferManager::send_control(const std::shared_ptr<Transfer>& t,
|
|
1261
|
+
const std::string& action) {
|
|
1262
|
+
nlohmann::json ctl{{"transfer_id", t->id}, {"action", action}};
|
|
1263
|
+
client_.send(t->peer_id, MSG_CONTROL, ctl);
|
|
1523
1264
|
}
|
|
1524
1265
|
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
return;
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
std::lock_guard<std::mutex> stats_lock(stats_mutex_);
|
|
1539
|
-
if (progress->direction == FileTransferDirection::SENDING) {
|
|
1540
|
-
total_bytes_sent_ += bytes_delta;
|
|
1541
|
-
} else {
|
|
1542
|
-
total_bytes_received_ += bytes_delta;
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
// Call progress callback
|
|
1548
|
-
if (progress_callback_) {
|
|
1549
|
-
progress_callback_(*progress);
|
|
1550
|
-
}
|
|
1266
|
+
bool FileTransferManager::pause(const std::string& transfer_id) {
|
|
1267
|
+
auto t = find(transfer_id);
|
|
1268
|
+
if (!t) return false;
|
|
1269
|
+
{
|
|
1270
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
1271
|
+
if (t->status != FileTransferStatus::IN_PROGRESS) return false;
|
|
1272
|
+
t->status = FileTransferStatus::PAUSED;
|
|
1273
|
+
t->cv.notify_all();
|
|
1274
|
+
}
|
|
1275
|
+
send_control(t, "pause");
|
|
1276
|
+
emit_progress(t);
|
|
1277
|
+
LOG_FT_INFO("Transfer " << transfer_id << " paused");
|
|
1278
|
+
return true;
|
|
1551
1279
|
}
|
|
1552
1280
|
|
|
1553
|
-
|
|
1554
|
-
auto
|
|
1555
|
-
if (!
|
|
1556
|
-
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
progress->status = success ? FileTransferStatus::COMPLETED : FileTransferStatus::FAILED;
|
|
1560
|
-
progress->error_message = error_message;
|
|
1561
|
-
|
|
1562
|
-
// Update statistics
|
|
1281
|
+
bool FileTransferManager::resume(const std::string& transfer_id) {
|
|
1282
|
+
auto t = find(transfer_id);
|
|
1283
|
+
if (!t) return false;
|
|
1284
|
+
bool requeue = false;
|
|
1563
1285
|
{
|
|
1564
|
-
std::lock_guard<std::mutex>
|
|
1565
|
-
if (
|
|
1566
|
-
|
|
1286
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
1287
|
+
if (t->status != FileTransferStatus::PAUSED) return false;
|
|
1288
|
+
if (t->direction == FileTransferDirection::SENDING) {
|
|
1289
|
+
t->status = FileTransferStatus::RESUMING;
|
|
1290
|
+
requeue = true;
|
|
1567
1291
|
} else {
|
|
1568
|
-
|
|
1292
|
+
t->status = FileTransferStatus::IN_PROGRESS;
|
|
1569
1293
|
}
|
|
1294
|
+
t->last_activity = std::chrono::steady_clock::now();
|
|
1295
|
+
t->cv.notify_all();
|
|
1570
1296
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
if (completion_callback_) {
|
|
1577
|
-
completion_callback_(transfer_id, success, error_message);
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
LOG_FILE_TRANSFER_INFO("Transfer " << (success ? "completed" : "failed") << ": " << transfer_id);
|
|
1297
|
+
send_control(t, "resume");
|
|
1298
|
+
if (requeue) queue_send(transfer_id);
|
|
1299
|
+
emit_progress(t);
|
|
1300
|
+
LOG_FT_INFO("Transfer " << transfer_id << " resumed");
|
|
1301
|
+
return true;
|
|
1581
1302
|
}
|
|
1582
1303
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1304
|
+
bool FileTransferManager::cancel(const std::string& transfer_id) {
|
|
1305
|
+
auto t = find(transfer_id);
|
|
1306
|
+
if (!t) return false;
|
|
1307
|
+
{
|
|
1308
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
1309
|
+
if (t->is_terminal()) return false;
|
|
1310
|
+
t->status = FileTransferStatus::CANCELLED;
|
|
1311
|
+
t->cv.notify_all();
|
|
1312
|
+
}
|
|
1313
|
+
send_control(t, "cancel");
|
|
1314
|
+
finish(t, false, "cancelled");
|
|
1315
|
+
LOG_FT_INFO("Transfer " << transfer_id << " cancelled");
|
|
1316
|
+
return true;
|
|
1591
1317
|
}
|
|
1592
1318
|
|
|
1593
|
-
//
|
|
1594
|
-
//
|
|
1595
|
-
//
|
|
1596
|
-
// - Replaced JSON + base64 encoding with direct binary transmission
|
|
1597
|
-
// - Eliminates 33% encoding overhead from base64
|
|
1598
|
-
// - Reduces CPU usage for encoding/decoding
|
|
1599
|
-
// - Minimizes memory allocations for string conversions
|
|
1600
|
-
// - Uses two-phase protocol: metadata (JSON) + binary data (raw)
|
|
1601
|
-
// - Added cleanup thread for timeout handling of split messages
|
|
1602
|
-
//
|
|
1603
|
-
// PROTOCOL CHANGES:
|
|
1604
|
-
// 1. Send file_chunk_metadata (JSON) with transfer info
|
|
1605
|
-
// 2. Send binary data with "FTCHUNK" magic header + raw file data
|
|
1606
|
-
// 3. Receive acknowledgments as before
|
|
1607
|
-
//
|
|
1608
|
-
// This approach provides maximum performance while maintaining protocol robustness.
|
|
1609
|
-
|
|
1610
|
-
void FileTransferManager::handle_chunk_received(const FileChunk& chunk) {
|
|
1611
|
-
// Write chunk to temporary file
|
|
1612
|
-
std::string temp_path = get_temp_file_path(chunk.transfer_id, config_.temp_directory);
|
|
1613
|
-
|
|
1614
|
-
if (!write_file_chunk(temp_path.c_str(), chunk.file_offset, chunk.data.data(), chunk.chunk_size)) {
|
|
1615
|
-
LOG_FILE_TRANSFER_ERROR("Failed to write chunk to temp file: " << temp_path);
|
|
1616
|
-
return;
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1619
|
-
// Update progress
|
|
1620
|
-
update_transfer_progress(chunk.transfer_id, chunk.chunk_size);
|
|
1621
|
-
|
|
1622
|
-
// Check if transfer is complete
|
|
1623
|
-
auto progress = get_transfer_progress(chunk.transfer_id);
|
|
1624
|
-
if (progress) {
|
|
1625
|
-
progress->chunks_completed++;
|
|
1626
|
-
if (progress->chunks_completed == progress->total_chunks) {
|
|
1627
|
-
// Transfer complete - move temp file to final location
|
|
1628
|
-
if (finalize_received_file(chunk.transfer_id, progress->local_path)) {
|
|
1629
|
-
complete_transfer(chunk.transfer_id, true);
|
|
1630
|
-
} else {
|
|
1631
|
-
complete_transfer(chunk.transfer_id, false, "Failed to finalize received file");
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1319
|
+
// =============================================================================
|
|
1320
|
+
// Peer disconnect
|
|
1321
|
+
// =============================================================================
|
|
1636
1322
|
|
|
1637
|
-
void FileTransferManager::
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
if (success) {
|
|
1644
|
-
progress->chunks_completed++;
|
|
1645
|
-
if (progress->chunks_completed == progress->total_chunks) {
|
|
1646
|
-
complete_transfer(transfer_id, true);
|
|
1323
|
+
void FileTransferManager::on_peer_disconnected(const std::string& peer_id) {
|
|
1324
|
+
std::vector<std::shared_ptr<Transfer>> affected;
|
|
1325
|
+
{
|
|
1326
|
+
std::lock_guard<std::mutex> lk(transfers_mutex_);
|
|
1327
|
+
for (auto& kv : transfers_) {
|
|
1328
|
+
if (kv.second->peer_id == peer_id) affected.push_back(kv.second);
|
|
1647
1329
|
}
|
|
1648
|
-
} else {
|
|
1649
|
-
// Handle chunk failure - could retry or fail the transfer
|
|
1650
|
-
LOG_FILE_TRANSFER_ERROR("Chunk " << chunk_index << " failed for transfer " << transfer_id);
|
|
1651
1330
|
}
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
// Ensure destination directory exists
|
|
1659
|
-
std::string dest_dir = get_parent_directory(final_path.c_str());
|
|
1660
|
-
if (!dest_dir.empty()) {
|
|
1661
|
-
ensure_directory_exists(dest_dir);
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
// Move temp file to final location
|
|
1665
|
-
if (!rename_file(temp_path.c_str(), final_path.c_str())) {
|
|
1666
|
-
LOG_FILE_TRANSFER_ERROR("Failed to rename temp file to final location: " << temp_path << " -> " << final_path);
|
|
1667
|
-
return false;
|
|
1331
|
+
for (auto& t : affected) {
|
|
1332
|
+
bool active;
|
|
1333
|
+
{
|
|
1334
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
1335
|
+
active = !t->is_terminal();
|
|
1336
|
+
if (active) t->cv.notify_all();
|
|
1668
1337
|
}
|
|
1669
|
-
|
|
1670
|
-
return true;
|
|
1671
|
-
} catch (const std::exception& e) {
|
|
1672
|
-
LOG_FILE_TRANSFER_ERROR("Failed to finalize file " << final_path << ": " << e.what());
|
|
1673
|
-
return false;
|
|
1338
|
+
if (active) finish(t, false, "peer disconnected");
|
|
1674
1339
|
}
|
|
1675
1340
|
}
|
|
1676
1341
|
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
return sha1.finalize();
|
|
1681
|
-
}
|
|
1342
|
+
// =============================================================================
|
|
1343
|
+
// Maintenance: timeouts and cleanup
|
|
1344
|
+
// =============================================================================
|
|
1682
1345
|
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
// Verify magic header
|
|
1692
|
-
if (std::memcmp(binary_data.data(), magic.c_str(), magic.length()) != 0) {
|
|
1693
|
-
return false;
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
// Extract chunk data (everything after the magic header)
|
|
1697
|
-
size_t data_start = magic.length();
|
|
1698
|
-
size_t data_size = binary_data.size() - data_start;
|
|
1699
|
-
|
|
1700
|
-
chunk.data.resize(data_size);
|
|
1701
|
-
std::memcpy(chunk.data.data(), binary_data.data() + data_start, data_size);
|
|
1702
|
-
|
|
1703
|
-
return true;
|
|
1704
|
-
}
|
|
1346
|
+
void FileTransferManager::maintenance_loop() {
|
|
1347
|
+
while (running_.load()) {
|
|
1348
|
+
{
|
|
1349
|
+
std::unique_lock<std::mutex> lk(maintenance_mutex_);
|
|
1350
|
+
maintenance_cv_.wait_for(lk, std::chrono::seconds(2),
|
|
1351
|
+
[this] { return !running_.load(); });
|
|
1352
|
+
}
|
|
1353
|
+
if (!running_.load()) return;
|
|
1705
1354
|
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
return true;
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
std::string calculated = calculate_chunk_checksum(chunk.data);
|
|
1712
|
-
return calculated == chunk.checksum;
|
|
1713
|
-
}
|
|
1355
|
+
auto now = std::chrono::steady_clock::now();
|
|
1356
|
+
uint32_t timeout_secs = get_config().transfer_timeout_secs;
|
|
1714
1357
|
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
// Check if file exists and is accessible
|
|
1721
|
-
if (!file_or_directory_exists(remote_path)) {
|
|
1722
|
-
LOG_FILE_TRANSFER_WARN("File request denied - file not found: " << remote_path);
|
|
1723
|
-
nlohmann::json response;
|
|
1724
|
-
response["transfer_id"] = transfer_id;
|
|
1725
|
-
response["accepted"] = false;
|
|
1726
|
-
response["reason"] = "File not found or not accessible";
|
|
1727
|
-
client_.send(peer_id, "file_transfer_response", response);
|
|
1728
|
-
return;
|
|
1358
|
+
std::vector<std::shared_ptr<Transfer>> all;
|
|
1359
|
+
{
|
|
1360
|
+
std::lock_guard<std::mutex> lk(transfers_mutex_);
|
|
1361
|
+
for (auto& kv : transfers_) all.push_back(kv.second);
|
|
1729
1362
|
}
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
} else
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
response["accepted"] = false;
|
|
1742
|
-
response["reason"] = "Request rejected by user";
|
|
1743
|
-
client_.send(peer_id, "file_transfer_response", response);
|
|
1744
|
-
LOG_FILE_TRANSFER_INFO("Rejected file request: " << remote_path << " for " << peer_id);
|
|
1363
|
+
|
|
1364
|
+
std::vector<std::shared_ptr<Transfer>> timed_out;
|
|
1365
|
+
std::vector<std::string> to_purge;
|
|
1366
|
+
for (auto& t : all) {
|
|
1367
|
+
std::lock_guard<std::mutex> lk(t->mtx);
|
|
1368
|
+
auto idle = std::chrono::duration_cast<std::chrono::seconds>(now - t->last_activity);
|
|
1369
|
+
if (t->is_terminal()) {
|
|
1370
|
+
if (idle > FINISHED_RETENTION) to_purge.push_back(t->id);
|
|
1371
|
+
} else if (t->status != FileTransferStatus::PAUSED &&
|
|
1372
|
+
idle.count() > timeout_secs) {
|
|
1373
|
+
timed_out.push_back(t);
|
|
1745
1374
|
}
|
|
1746
|
-
} else {
|
|
1747
|
-
// Auto-reject if no callback is set
|
|
1748
|
-
nlohmann::json response;
|
|
1749
|
-
response["transfer_id"] = transfer_id;
|
|
1750
|
-
response["accepted"] = false;
|
|
1751
|
-
response["reason"] = "No file request handler configured";
|
|
1752
|
-
client_.send(peer_id, "file_transfer_response", response);
|
|
1753
|
-
LOG_FILE_TRANSFER_WARN("Auto-rejected file request - no handler: " << remote_path);
|
|
1754
1375
|
}
|
|
1755
|
-
|
|
1756
|
-
} catch (const std::exception& e) {
|
|
1757
|
-
LOG_FILE_TRANSFER_ERROR("Error handling file request: " << e.what());
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
1376
|
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
// Check if directory exists and is accessible
|
|
1768
|
-
if (!directory_exists(remote_path)) {
|
|
1769
|
-
LOG_FILE_TRANSFER_WARN("Directory request denied - directory not found: " << remote_path);
|
|
1770
|
-
nlohmann::json response;
|
|
1771
|
-
response["transfer_id"] = transfer_id;
|
|
1772
|
-
response["accepted"] = false;
|
|
1773
|
-
response["reason"] = "Directory not found or not accessible";
|
|
1774
|
-
client_.send(peer_id, "file_transfer_response", response);
|
|
1775
|
-
return;
|
|
1377
|
+
for (auto& t : timed_out) {
|
|
1378
|
+
LOG_FT_WARN("Transfer " << t->id << " timed out");
|
|
1379
|
+
nlohmann::json done{{"transfer_id", t->id}, {"success", false},
|
|
1380
|
+
{"error", "timed out"}};
|
|
1381
|
+
client_.send(t->peer_id, MSG_COMPLETE, done);
|
|
1382
|
+
finish(t, false, "timed out");
|
|
1776
1383
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
bool accepted = directory_request_callback_(peer_id, remote_path, recursive, transfer_id);
|
|
1781
|
-
if (accepted) {
|
|
1782
|
-
// Start directory transfer
|
|
1783
|
-
send_directory(peer_id, remote_path, "", recursive);
|
|
1784
|
-
LOG_FILE_TRANSFER_INFO("Accepted directory request: " << remote_path << " for " << peer_id);
|
|
1785
|
-
} else {
|
|
1786
|
-
nlohmann::json response;
|
|
1787
|
-
response["transfer_id"] = transfer_id;
|
|
1788
|
-
response["accepted"] = false;
|
|
1789
|
-
response["reason"] = "Request rejected by user";
|
|
1790
|
-
client_.send(peer_id, "file_transfer_response", response);
|
|
1791
|
-
LOG_FILE_TRANSFER_INFO("Rejected directory request: " << remote_path << " for " << peer_id);
|
|
1792
|
-
}
|
|
1793
|
-
} else {
|
|
1794
|
-
// Auto-reject if no callback is set
|
|
1795
|
-
nlohmann::json response;
|
|
1796
|
-
response["transfer_id"] = transfer_id;
|
|
1797
|
-
response["accepted"] = false;
|
|
1798
|
-
response["reason"] = "No directory request handler configured";
|
|
1799
|
-
client_.send(peer_id, "file_transfer_response", response);
|
|
1800
|
-
LOG_FILE_TRANSFER_WARN("Auto-rejected directory request - no handler: " << remote_path);
|
|
1384
|
+
if (!to_purge.empty()) {
|
|
1385
|
+
std::lock_guard<std::mutex> lk(transfers_mutex_);
|
|
1386
|
+
for (auto& id : to_purge) transfers_.erase(id);
|
|
1801
1387
|
}
|
|
1802
|
-
|
|
1803
|
-
} catch (const std::exception& e) {
|
|
1804
|
-
LOG_FILE_TRANSFER_ERROR("Error handling directory request: " << e.what());
|
|
1805
1388
|
}
|
|
1806
1389
|
}
|
|
1807
1390
|
|
|
1391
|
+
// =============================================================================
|
|
1392
|
+
// Utilities
|
|
1393
|
+
// =============================================================================
|
|
1394
|
+
|
|
1395
|
+
std::string FileTransferManager::compute_file_sha256(const std::string& path) {
|
|
1396
|
+
int64_t size = get_file_size(path.c_str());
|
|
1397
|
+
if (size < 0) return "";
|
|
1398
|
+
sha256_context_t ctx;
|
|
1399
|
+
sha256_reset(&ctx);
|
|
1400
|
+
std::vector<uint8_t> buf(64 * 1024);
|
|
1401
|
+
uint64_t offset = 0;
|
|
1402
|
+
uint64_t remaining = static_cast<uint64_t>(size);
|
|
1403
|
+
while (remaining > 0) {
|
|
1404
|
+
uint32_t want = static_cast<uint32_t>(std::min<uint64_t>(buf.size(), remaining));
|
|
1405
|
+
if (!read_file_chunk(path, offset, buf.data(), want)) return "";
|
|
1406
|
+
sha256_update(&ctx, buf.data(), want);
|
|
1407
|
+
offset += want;
|
|
1408
|
+
remaining -= want;
|
|
1409
|
+
}
|
|
1410
|
+
uint8_t digest[SHA256_HASH_SIZE];
|
|
1411
|
+
sha256_finish(&ctx, digest);
|
|
1412
|
+
return to_hex(digest, SHA256_HASH_SIZE);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1808
1415
|
} // namespace librats
|