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.
Files changed (35) hide show
  1. package/README.md +9 -4
  2. package/lib/index.d.ts +9 -22
  3. package/native-src/CMakeLists.txt +132 -50
  4. package/native-src/cmake/ratsConfig.cmake.in +8 -0
  5. package/native-src/src/bt_network.cpp +296 -183
  6. package/native-src/src/bt_network.h +25 -5
  7. package/native-src/src/crypto/sha256.c +1 -1
  8. package/native-src/src/dht.cpp +297 -47
  9. package/native-src/src/dht.h +70 -6
  10. package/native-src/src/file_transfer.cpp +1185 -1578
  11. package/native-src/src/file_transfer.h +240 -521
  12. package/native-src/src/io_poller.cpp +917 -0
  13. package/native-src/src/io_poller.h +138 -0
  14. package/native-src/src/krpc.cpp +161 -96
  15. package/native-src/src/krpc.h +18 -4
  16. package/native-src/src/librats.cpp +812 -1236
  17. package/native-src/src/librats.h +207 -208
  18. package/native-src/src/librats_bittorrent.cpp +1 -5
  19. package/native-src/src/librats_c.cpp +22 -39
  20. package/native-src/src/librats_discovery.cpp +377 -0
  21. package/native-src/src/librats_encryption.cpp +130 -283
  22. package/native-src/src/librats_file_transfer.cpp +27 -109
  23. package/native-src/src/librats_gossipsub.cpp +1 -5
  24. package/native-src/src/librats_ice.cpp +5 -1
  25. package/native-src/src/librats_log_macros.h +36 -0
  26. package/native-src/src/librats_logging.cpp +1 -7
  27. package/native-src/src/librats_mdns.cpp +1 -11
  28. package/native-src/src/librats_persistence.cpp +1 -11
  29. package/native-src/src/librats_reconnection.cpp +2 -13
  30. package/native-src/src/librats_statistic.cpp +105 -0
  31. package/native-src/src/socket.cpp +15 -3
  32. package/package.json +1 -1
  33. package/scripts/build-librats.js +3 -0
  34. package/scripts/prepare-package.js +10 -0
  35. 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 "sha1.h"
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 <cstdlib>
18
-
19
- // Optional compression support
20
- #ifdef LIBRATS_ENABLE_ZLIB
21
- #include <zlib.h>
22
- #endif
9
+ #include <random>
23
10
 
24
- #ifdef LIBRATS_ENABLE_LZ4
25
- #include <lz4.h>
26
- #endif
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
- // DirectoryMetadata Implementation
32
- //=============================================================================
33
-
34
- uint64_t DirectoryMetadata::get_total_size() const {
35
- uint64_t total = 0;
36
-
37
- // Add size of files in this directory
38
- for (const auto& file : files) {
39
- total += file.file_size;
40
- }
41
-
42
- // Add size of subdirectories recursively
43
- for (const auto& subdir : subdirectories) {
44
- total += subdir.get_total_size();
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
- return total;
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
- size_t DirectoryMetadata::get_total_file_count() const {
51
- size_t count = files.size();
52
-
53
- // Add files from subdirectories recursively
54
- for (const auto& subdir : subdirectories) {
55
- count += subdir.get_total_file_count();
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
- // FileTransferProgress Implementation
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
- void FileTransferProgress::update_transfer_rates(uint64_t new_bytes_transferred) {
66
- auto now = std::chrono::steady_clock::now();
67
- auto time_diff = std::chrono::duration_cast<std::chrono::milliseconds>(now - last_update);
68
-
69
- if (time_diff.count() > 0) {
70
- uint64_t bytes_diff = new_bytes_transferred - bytes_transferred;
71
- transfer_rate_bps = (static_cast<double>(bytes_diff) * 1000.0) / time_diff.count();
72
-
73
- // Calculate average rate since start
74
- auto total_time = std::chrono::duration_cast<std::chrono::milliseconds>(now - start_time);
75
- if (total_time.count() > 0) {
76
- average_rate_bps = (static_cast<double>(new_bytes_transferred) * 1000.0) / total_time.count();
77
- }
78
-
79
- // Estimate time remaining
80
- if (transfer_rate_bps > 0 && total_bytes > new_bytes_transferred) {
81
- uint64_t remaining_bytes = total_bytes - new_bytes_transferred;
82
- estimated_time_remaining = std::chrono::milliseconds(
83
- static_cast<int64_t>((remaining_bytes * 1000.0) / transfer_rate_bps)
84
- );
85
- } else {
86
- estimated_time_remaining = std::chrono::milliseconds(0);
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
- // FileTransferManager Implementation
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), running_(true),
100
- total_bytes_sent_(0), total_bytes_received_(0),
101
- total_files_sent_(0), total_files_received_(0) {
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
- FileTransferManager::~FileTransferManager() {
108
- shutdown();
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
- void FileTransferManager::initialize() {
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
- void FileTransferManager::shutdown() {
159
- LOG_FILE_TRANSFER_INFO("FileTransferManager stopping...");
243
+ FileTransferManager::~FileTransferManager() {
160
244
  running_.store(false);
161
-
162
- // Notify all condition variables to wake up waiting threads immediately
163
- work_condition_.notify_all();
164
- cleanup_condition_.notify_all();
165
- throttle_condition_.notify_all();
166
-
167
- // Join all worker threads
168
- for (auto& thread : worker_threads_) {
169
- if (thread.joinable()) {
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
- void FileTransferManager::worker_thread_loop() {
179
- while (running_.load()) {
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::cleanup_thread_loop() {
198
- const auto cleanup_interval = std::chrono::seconds(5);
199
- const auto pending_timeout = std::chrono::seconds(30);
200
-
201
- while (running_.load()) {
202
- // Use condition variable with timeout instead of sleep
203
- std::unique_lock<std::mutex> lock(cleanup_mutex_);
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> lock(transfers_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
- const FileTransferConfig& FileTransferManager::get_config() const {
283
+ FileTransferConfig FileTransferManager::get_config() const {
284
+ std::lock_guard<std::mutex> lk(config_mutex_);
247
285
  return config_;
248
286
  }
249
287
 
250
- bool FileTransferManager::handle_binary_data(const std::string& peer_id, const std::vector<uint8_t>& binary_data) {
251
- // Check if this is a file chunk binary message
252
- const std::string magic = "FTCHUNK";
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::set_progress_callback(FileTransferProgressCallback callback) {
267
- progress_callback_ = callback;
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
- void FileTransferManager::set_completion_callback(FileTransferCompletedCallback callback) {
271
- completion_callback_ = callback;
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
- void FileTransferManager::set_request_callback(FileTransferRequestCallback callback) {
275
- request_callback_ = callback;
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
- void FileTransferManager::set_directory_progress_callback(DirectoryTransferProgressCallback callback) {
279
- directory_progress_callback_ = callback;
280
- }
305
+ // =============================================================================
306
+ // Lookup / progress snapshot
307
+ // =============================================================================
281
308
 
282
- void FileTransferManager::set_file_request_callback(FileRequestCallback callback) {
283
- file_request_callback_ = callback;
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
- void FileTransferManager::set_directory_request_callback(DirectoryRequestCallback callback) {
287
- directory_request_callback_ = callback;
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
- std::string FileTransferManager::send_file(const std::string& peer_id, const std::string& file_path,
291
- const std::string& remote_filename) {
292
- // Validate file
293
- if (!validate_file_path(file_path, false)) {
294
- LOG_FILE_TRANSFER_ERROR("Invalid file path: " << file_path);
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
- // Use custom filename if provided
306
- if (!remote_filename.empty()) {
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::string FileTransferManager::send_file_with_metadata(const std::string& peer_id, const std::string& file_path,
314
- const FileMetadata& metadata) {
315
- std::string transfer_id = generate_transfer_id();
316
-
317
- // Create transfer progress tracking
318
- auto progress = std::make_shared<FileTransferProgress>();
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::string FileTransferManager::send_directory(const std::string& peer_id, const std::string& directory_path,
343
- const std::string& remote_directory_name, bool recursive) {
344
- // Validate directory
345
- if (!directory_exists(directory_path)) {
346
- LOG_FILE_TRANSFER_ERROR("Invalid directory path: " << directory_path);
347
- return "";
348
- }
349
-
350
- // Get directory metadata
351
- DirectoryMetadata dir_metadata = get_directory_metadata(directory_path, recursive);
352
- if (dir_metadata.files.empty() && dir_metadata.subdirectories.empty()) {
353
- LOG_FILE_TRANSFER_ERROR("Directory is empty: " << directory_path);
354
- return "";
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
- // Use custom directory name if provided
358
- if (!remote_directory_name.empty()) {
359
- dir_metadata.directory_name = remote_directory_name;
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> lock(transfers_mutex_);
377
- active_transfers_[transfer_id] = progress;
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> dir_lock(directory_transfers_mutex_);
383
- active_directory_transfers_[transfer_id] = dir_metadata;
384
- }
385
-
386
- // Create directory transfer request message
387
- nlohmann::json request_msg;
388
- request_msg["transfer_id"] = transfer_id;
389
- request_msg["type"] = "directory";
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
- std::string FileTransferManager::request_file(const std::string& peer_id, const std::string& remote_file_path,
424
- const std::string& local_path) {
425
- std::string transfer_id = generate_transfer_id();
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
- std::string FileTransferManager::request_directory(const std::string& peer_id, const std::string& remote_directory_path,
441
- const std::string& local_directory_path, bool recursive) {
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> transfers_lock(transfers_mutex_);
484
- active_transfers_[transfer_id] = progress;
485
- }
486
-
487
- // Send acceptance response
488
- nlohmann::json response_msg = create_transfer_response_message(transfer_id, true);
489
- client_.send(pending_transfer.peer_id, "file_transfer_response", response_msg);
490
-
491
- // Add to work queue for processing
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> work_lock(work_mutex_);
494
- work_queue_.push(transfer_id);
425
+ std::lock_guard<std::mutex> lk(callbacks_mutex_);
426
+ cb = progress_callback_;
495
427
  }
496
- work_condition_.notify_one();
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
- bool FileTransferManager::accept_directory_transfer(const std::string& transfer_id, const std::string& local_path) {
523
- std::lock_guard<std::mutex> lock(pending_mutex_);
524
-
525
- auto it = pending_directory_transfers_.find(transfer_id);
526
- if (it == pending_directory_transfers_.end()) {
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> transfers_lock(transfers_mutex_);
548
- active_transfers_[transfer_id] = progress;
549
- }
550
-
551
- // Send acceptance response
552
- nlohmann::json response_msg = create_transfer_response_message(transfer_id, true);
553
- client_.send(pending_transfer.peer_id, "file_transfer_response", response_msg);
554
-
555
- // Store directory metadata for processing
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> dir_lock(directory_transfers_mutex_);
558
- active_directory_transfers_[transfer_id] = pending_transfer.metadata;
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
- // Add to work queue for processing
469
+
470
+ TransferProgressCallback pcb;
471
+ TransferCompletedCallback ccb;
562
472
  {
563
- std::lock_guard<std::mutex> work_lock(work_mutex_);
564
- work_queue_.push(transfer_id);
473
+ std::lock_guard<std::mutex> lk(callbacks_mutex_);
474
+ pcb = progress_callback_;
475
+ ccb = completed_callback_;
565
476
  }
566
- work_condition_.notify_one();
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
- bool FileTransferManager::reject_directory_transfer(const std::string& transfer_id, const std::string& reason) {
573
- std::lock_guard<std::mutex> lock(pending_mutex_);
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
- bool FileTransferManager::pause_transfer(const std::string& transfer_id) {
593
- std::lock_guard<std::mutex> lock(transfers_mutex_);
594
-
595
- auto it = active_transfers_.find(transfer_id);
596
- if (it == active_transfers_.end()) {
597
- return false;
598
- }
599
-
600
- auto& progress = it->second;
601
- if (progress->status == FileTransferStatus::IN_PROGRESS) {
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
- bool FileTransferManager::resume_transfer(const std::string& transfer_id) {
616
- std::lock_guard<std::mutex> lock(transfers_mutex_);
617
-
618
- auto it = active_transfers_.find(transfer_id);
619
- if (it == active_transfers_.end()) {
620
- return false;
621
- }
622
-
623
- auto& progress = it->second;
624
- if (progress->status == FileTransferStatus::PAUSED) {
625
- progress->status = FileTransferStatus::RESUMING;
626
-
627
- // Add to work queue for processing
628
- {
629
- std::lock_guard<std::mutex> work_lock(work_mutex_);
630
- work_queue_.push(transfer_id);
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
- bool FileTransferManager::cancel_transfer(const std::string& transfer_id) {
646
- std::lock_guard<std::mutex> lock(transfers_mutex_);
647
-
648
- auto it = active_transfers_.find(transfer_id);
649
- if (it == active_transfers_.end()) {
650
- return false;
651
- }
652
-
653
- auto& progress = it->second;
654
- progress->status = FileTransferStatus::CANCELLED;
655
-
656
- // Send cancel control message
657
- nlohmann::json control_msg = create_control_message(transfer_id, "cancel");
658
- client_.send(progress->peer_id, "file_transfer_control", control_msg);
659
-
660
- // Move to completed transfers
661
- move_to_completed(transfer_id);
662
-
663
- LOG_FILE_TRANSFER_INFO("Cancelled transfer: " << transfer_id);
664
- return true;
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
- bool FileTransferManager::retry_transfer(const std::string& transfer_id) {
668
- // Look in completed transfers for failed ones
669
- std::lock_guard<std::mutex> lock(transfers_mutex_);
670
-
671
- auto it = completed_transfers_.find(transfer_id);
672
- if (it == completed_transfers_.end()) {
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
- work_condition_.notify_one();
698
-
699
- LOG_FILE_TRANSFER_INFO("Retrying transfer: " << transfer_id);
700
- return true;
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
- std::shared_ptr<FileTransferProgress> FileTransferManager::get_transfer_progress(const std::string& transfer_id) const {
704
- std::lock_guard<std::mutex> lock(transfers_mutex_);
705
-
706
- auto it = active_transfers_.find(transfer_id);
707
- if (it != active_transfers_.end()) {
708
- return it->second;
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
- auto completed_it = completed_transfers_.find(transfer_id);
712
- if (completed_it != completed_transfers_.end()) {
713
- return completed_it->second;
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
- std::vector<std::shared_ptr<FileTransferProgress>> FileTransferManager::get_active_transfers() const {
720
- std::lock_guard<std::mutex> lock(transfers_mutex_);
721
-
722
- std::vector<std::shared_ptr<FileTransferProgress>> transfers;
723
- transfers.reserve(active_transfers_.size());
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
- void FileTransferManager::clear_transfer_history() {
755
- std::lock_guard<std::mutex> lock(transfers_mutex_);
756
- completed_transfers_.clear();
757
- LOG_FILE_TRANSFER_INFO("Cleared transfer history");
758
- }
610
+ // =============================================================================
611
+ // Sending: streaming worker
612
+ // =============================================================================
759
613
 
760
- nlohmann::json FileTransferManager::get_transfer_statistics() const {
761
- std::lock_guard<std::mutex> stats_lock(stats_mutex_);
762
- std::lock_guard<std::mutex> transfers_lock(transfers_mutex_);
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
- // Static utility functions
803
- std::string FileTransferManager::calculate_file_checksum(const std::string& file_path, const std::string& algorithm) {
804
- if (algorithm == "sha256") {
805
- // Use a simple SHA1 implementation for now (we can extend this)
806
- size_t file_size;
807
- void* file_data = read_file_binary(file_path.c_str(), &file_size);
808
- if (!file_data) {
809
- return "";
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
- SHA1 sha1;
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
- FileMetadata FileTransferManager::get_file_metadata(const std::string& file_path) {
825
- FileMetadata metadata;
826
-
827
- try {
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
- // Get last modification time
839
- metadata.last_modified = get_file_modified_time(file_path.c_str());
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
- DirectoryMetadata FileTransferManager::get_directory_metadata(const std::string& directory_path, bool recursive) {
857
- DirectoryMetadata metadata;
858
-
859
- try {
860
- metadata.directory_name = get_filename_from_path(directory_path.c_str());
861
-
862
- std::vector<DirectoryEntry> entries;
863
- if (list_directory(directory_path.c_str(), entries)) {
864
- for (const auto& entry : entries) {
865
- if (!entry.is_directory) {
866
- FileMetadata file_meta = get_file_metadata(entry.path);
867
- file_meta.relative_path = entry.name;
868
- metadata.files.push_back(file_meta);
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
- bool FileTransferManager::validate_file_path(const std::string& file_path, bool check_write) {
886
- return validate_path(file_path.c_str(), check_write);
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
- // Private implementation methods
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
- std::string FileTransferManager::generate_transfer_id() const {
892
- std::random_device rd;
893
- std::mt19937 gen(rd());
894
- std::uniform_int_distribution<> dis(0, 15);
895
-
896
- std::stringstream ss;
897
- ss << std::hex;
898
- for (int i = 0; i < 32; ++i) {
899
- ss << dis(gen);
900
- }
901
-
902
- return ss.str();
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
- void FileTransferManager::process_transfer(const std::string& transfer_id) {
906
- // Exit immediately if shutting down
907
- if (!running_.load()) {
908
- return;
909
- }
910
-
911
- auto progress = get_transfer_progress(transfer_id);
912
- if (!progress) {
913
- return;
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
- // Check if this is a directory transfer
922
- bool is_directory_transfer = false;
753
+
754
+ if (!running_.load()) return;
755
+
756
+ // All data streamed; wait for the receiver's ft_complete.
923
757
  {
924
- std::lock_guard<std::mutex> dir_lock(directory_transfers_mutex_);
925
- is_directory_transfer = active_directory_transfers_.find(transfer_id) != active_directory_transfers_.end();
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
- void FileTransferManager::start_file_send(const std::string& transfer_id) {
944
- auto progress = get_transfer_progress(transfer_id);
945
- if (!progress) {
946
- return;
947
- }
948
-
949
- progress->status = FileTransferStatus::IN_PROGRESS;
950
- update_transfer_progress(transfer_id);
951
-
952
- // Read file and create chunks
953
- uint64_t chunk_index = 0;
954
- uint64_t file_offset = 0;
955
-
956
- while (file_offset < progress->file_size && running_.load()) {
957
- // Check if transfer was cancelled or paused
958
- auto current_progress = get_transfer_progress(transfer_id);
959
- if (!current_progress || current_progress->status == FileTransferStatus::CANCELLED ||
960
- current_progress->status == FileTransferStatus::PAUSED) {
961
- return;
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
- uint32_t chunk_size = (std::min)(static_cast<uint64_t>(config_.chunk_size),
965
- progress->file_size - file_offset);
966
-
967
- FileChunk chunk;
968
- chunk.transfer_id = transfer_id;
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
- // Calculate checksum
982
- if (config_.verify_checksums) {
983
- chunk.checksum = calculate_chunk_checksum(chunk.data);
802
+
803
+ {
804
+ std::lock_guard<std::mutex> lk(transfers_mutex_);
805
+ transfers_[id] = t;
984
806
  }
985
-
986
- // Note: Compression removed as requested - only binary chunks needed
987
-
988
- // Send chunk metadata first
989
- nlohmann::json metadata_msg = create_chunk_metadata_message(chunk);
990
- client_.send(progress->peer_id, "file_chunk_metadata", metadata_msg);
991
-
992
- // Send chunk binary data
993
- std::vector<uint8_t> binary_msg = create_chunk_binary_message(chunk);
994
- client_.send_binary_to_peer_id(progress->peer_id, binary_msg, MessageDataType::BINARY);
995
-
996
- chunk_index++;
997
- file_offset += chunk_size;
998
-
999
- // Update progress
1000
- update_transfer_progress(transfer_id, chunk_size);
1001
-
1002
- // Throttle sending to avoid overwhelming the peer - use condition variable for fast shutdown
1003
- if (running_.load()) {
1004
- std::unique_lock<std::mutex> lock(throttle_mutex_);
1005
- throttle_condition_.wait_for(lock, std::chrono::milliseconds(10), [this] { return !running_.load(); });
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
- void FileTransferManager::start_file_receive(const std::string& transfer_id) {
1013
- // Exit immediately if shutting down
1014
- if (!running_.load()) {
1015
- return;
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
- auto progress = get_transfer_progress(transfer_id);
1019
- if (!progress) {
1020
- return;
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
- progress->status = FileTransferStatus::IN_PROGRESS;
1024
-
1025
- // Create temporary file for receiving
1026
- if (!create_temp_file(transfer_id, progress->file_size)) {
1027
- complete_transfer(transfer_id, false, "Failed to create temporary file");
1028
- return;
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
- update_transfer_progress(transfer_id);
1032
- LOG_FILE_TRANSFER_INFO("Started receiving file transfer: " << transfer_id);
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
- void FileTransferManager::start_directory_send(const std::string& transfer_id) {
1036
- auto progress = get_transfer_progress(transfer_id);
1037
- if (!progress) {
1038
- return;
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
- DirectoryMetadata dir_metadata;
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> dir_lock(directory_transfers_mutex_);
1044
- auto it = active_directory_transfers_.find(transfer_id);
1045
- if (it == active_directory_transfers_.end()) {
1046
- complete_transfer(transfer_id, false, "Directory metadata not found");
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
- dir_metadata = it->second;
1050
- }
1051
-
1052
- progress->status = FileTransferStatus::IN_PROGRESS;
1053
- update_transfer_progress(transfer_id);
1054
-
1055
- // Send each file in the directory
1056
- for (const auto& file_metadata : dir_metadata.files) {
1057
- if (!running_.load()) {
1058
- return;
1059
- }
1060
-
1061
- // Check if transfer was cancelled or paused
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
- std::string full_file_path = combine_paths(progress->local_path, file_metadata.relative_path);
1069
-
1070
- // Send individual file using existing file transfer logic
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
- // Wait for individual file transfer to complete
1079
- // Note: In a real implementation, we might want to track multiple concurrent file transfers
1080
- // For now, we'll send files sequentially
1081
- }
1082
-
1083
- LOG_FILE_TRANSFER_INFO("Completed sending directory transfer: " << transfer_id);
1084
- complete_transfer(transfer_id, true);
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
- void FileTransferManager::start_directory_receive(const std::string& transfer_id) {
1088
- auto progress = get_transfer_progress(transfer_id);
1089
- if (!progress) {
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
- DirectoryMetadata dir_metadata;
981
+
982
+ // Create the temp file on first contact, pre-sized to the declared length.
1094
983
  {
1095
- std::lock_guard<std::mutex> dir_lock(directory_transfers_mutex_);
1096
- auto it = active_directory_transfers_.find(transfer_id);
1097
- if (it == active_directory_transfers_.end()) {
1098
- complete_transfer(transfer_id, false, "Directory metadata not found");
1099
- return;
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
- dir_metadata = it->second;
1102
- }
1103
-
1104
- progress->status = FileTransferStatus::IN_PROGRESS;
1105
-
1106
- // Create directory structure
1107
- std::string base_path = progress->local_path;
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
- // Create subdirectories if needed
1114
- for (const auto& file_metadata : dir_metadata.files) {
1115
- std::string file_dir = combine_paths(base_path, get_parent_directory(file_metadata.relative_path.c_str()));
1116
- if (!file_dir.empty() && !ensure_directory_exists(file_dir)) {
1117
- complete_transfer(transfer_id, false, "Failed to create subdirectory: " + file_dir);
1118
- return;
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
- update_transfer_progress(transfer_id);
1123
- LOG_FILE_TRANSFER_INFO("Started receiving directory transfer: " << transfer_id);
1124
-
1125
- // Note: Individual files will be received as separate file transfer requests
1126
- // The directory transfer completion will be handled when all files are received
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
- bool FileTransferManager::create_temp_file(const std::string& transfer_id, uint64_t file_size) {
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
- create_directories(config_.temp_directory.c_str());
1134
-
1135
- // Create file with pre-allocated size
1136
- if (!create_file_with_size(temp_path.c_str(), file_size)) {
1137
- LOG_FILE_TRANSFER_ERROR("Failed to create temp file " << temp_path);
1138
- return false;
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
- LOG_FILE_TRANSFER_ERROR("Failed to create temp file " << temp_path << ": " << e.what());
1144
- return false;
1067
+ LOG_FT_ERROR("Malformed ft_file_end: " << e.what());
1145
1068
  }
1146
1069
  }
1147
1070
 
1148
- std::string FileTransferManager::get_temp_file_path(const std::string& transfer_id, const std::string& temp_dir) {
1149
- return combine_paths(temp_dir, transfer_id + ".tmp");
1150
- }
1151
-
1152
- bool FileTransferManager::ensure_directory_exists(const std::string& directory_path) {
1153
- return create_directories(directory_path.c_str());
1154
- }
1155
-
1156
- std::string FileTransferManager::extract_filename(const std::string& file_path) {
1157
- return get_filename_from_path(file_path);
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
- std::string FileTransferManager::get_mime_type(const std::string& file_path) {
1161
- std::string extension = get_file_extension(file_path);
1162
- std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
1163
-
1164
- // Basic MIME type mapping
1165
- static const std::unordered_map<std::string, std::string> mime_types = {
1166
- {".txt", "text/plain"},
1167
- {".json", "application/json"},
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
- // Message handling methods
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
- void FileTransferManager::handle_transfer_request(const std::string& peer_id, const nlohmann::json& message) {
1195
- try {
1196
- std::string transfer_id = message["transfer_id"];
1197
-
1198
- if (message.contains("directory_metadata")) {
1199
- // Directory transfer request
1200
- DirectoryMetadata dir_metadata;
1201
- auto dir_info = message["directory_metadata"];
1202
- dir_metadata.directory_name = dir_info["directory_name"];
1203
- dir_metadata.relative_path = dir_info["relative_path"];
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
- // Call user callback to approve/reject
1269
- if (request_callback_) {
1270
- bool accepted = request_callback_(peer_id, metadata, transfer_id);
1271
- if (accepted) {
1272
- // We need a way to specify the local path - this should be part of the callback
1273
- // For now, we'll create a default path
1274
- std::string local_path = "./" + metadata.filename;
1275
- accept_file_transfer(transfer_id, local_path);
1276
- } else {
1277
- reject_file_transfer(transfer_id, "Rejected by user");
1278
- }
1279
- } else {
1280
- // Auto-reject if no callback is set
1281
- reject_file_transfer(transfer_id, "No request handler configured");
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
- } catch (const std::exception& e) {
1285
- LOG_FILE_TRANSFER_ERROR("Error handling transfer request: " << e.what());
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
- void FileTransferManager::handle_transfer_response(const std::string& peer_id, const nlohmann::json& message) {
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 transfer_id = message["transfer_id"];
1292
- bool accepted = message["accepted"];
1293
-
1294
- auto progress = get_transfer_progress(transfer_id);
1295
- if (!progress) {
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> work_lock(work_mutex_);
1304
- work_queue_.push(transfer_id);
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
- work_condition_.notify_one();
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 = message.value("reason", "No reason provided");
1311
- complete_transfer(transfer_id, false, "Transfer rejected by peer: " + reason);
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
- LOG_FILE_TRANSFER_ERROR("Error handling transfer response: " << e.what());
1180
+ LOG_FT_ERROR("Malformed ft_response: " << e.what());
1316
1181
  }
1317
1182
  }
1318
1183
 
1319
- void FileTransferManager::handle_chunk_metadata_message(const std::string& peer_id, const nlohmann::json& message) {
1184
+ void FileTransferManager::on_progress(const std::string& /*peer_id*/, const nlohmann::json& msg) {
1320
1185
  try {
1321
- PendingChunk pending;
1322
- pending.transfer_id = message["transfer_id"];
1323
- pending.chunk_index = message["chunk_index"];
1324
- pending.total_chunks = message["total_chunks"];
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> lock(chunks_mutex_);
1333
- pending_chunks_[peer_id] = pending;
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
- LOG_FILE_TRANSFER_ERROR("Error handling chunk metadata message: " << e.what());
1197
+ LOG_FT_ERROR("Malformed ft_progress: " << e.what());
1341
1198
  }
1342
1199
  }
1343
1200
 
1344
- void FileTransferManager::handle_chunk_binary_message(const std::string& peer_id, const std::vector<uint8_t>& binary_data) {
1201
+ void FileTransferManager::on_complete(const std::string& /*peer_id*/, const nlohmann::json& msg) {
1345
1202
  try {
1346
- // Parse binary chunk header and get the chunk data
1347
- FileChunk chunk;
1348
- if (!parse_chunk_binary_header(binary_data, chunk)) {
1349
- LOG_FILE_TRANSFER_ERROR("Failed to parse chunk binary header from peer " << peer_id);
1350
- return;
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
- LOG_FILE_TRANSFER_ERROR("Error handling chunk binary message: " << e.what());
1210
+ LOG_FT_ERROR("Malformed ft_complete: " << e.what());
1415
1211
  }
1416
1212
  }
1417
1213
 
1418
- void FileTransferManager::handle_chunk_ack_message(const std::string& peer_id, const nlohmann::json& message) {
1214
+ void FileTransferManager::on_control(const std::string& /*peer_id*/, const nlohmann::json& msg) {
1419
1215
  try {
1420
- std::string transfer_id = message["transfer_id"];
1421
- uint64_t chunk_index = message["chunk_index"];
1422
- bool success = message["success"];
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
- pause_transfer(transfer_id);
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
- resume_transfer(transfer_id);
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
- cancel_transfer(transfer_id);
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
- LOG_FILE_TRANSFER_ERROR("Error handling transfer control message: " << e.what());
1252
+ LOG_FT_ERROR("Malformed ft_control: " << e.what());
1446
1253
  }
1447
1254
  }
1448
1255
 
1449
- // Message creation methods
1450
-
1451
- nlohmann::json FileTransferManager::create_transfer_request_message(const FileMetadata& metadata, const std::string& transfer_id) {
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
- nlohmann::json FileTransferManager::create_control_message(const std::string& transfer_id, const std::string& action, const nlohmann::json& data) {
1516
- nlohmann::json message;
1517
- message["transfer_id"] = transfer_id;
1518
- message["action"] = action;
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
- // Progress tracking methods
1526
-
1527
- void FileTransferManager::update_transfer_progress(const std::string& transfer_id, uint64_t bytes_delta) {
1528
- auto progress = get_transfer_progress(transfer_id);
1529
- if (!progress) {
1530
- return;
1531
- }
1532
-
1533
- if (bytes_delta > 0) {
1534
- progress->update_transfer_rates(progress->bytes_transferred + bytes_delta);
1535
-
1536
- // Update statistics
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
- void FileTransferManager::complete_transfer(const std::string& transfer_id, bool success, const std::string& error_message) {
1554
- auto progress = get_transfer_progress(transfer_id);
1555
- if (!progress) {
1556
- return;
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> stats_lock(stats_mutex_);
1565
- if (progress->direction == FileTransferDirection::SENDING) {
1566
- total_files_sent_++;
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
- total_files_received_++;
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
- // Move to completed transfers
1573
- move_to_completed(transfer_id);
1574
-
1575
- // Call completion callback
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
- void FileTransferManager::move_to_completed(const std::string& transfer_id) {
1584
- std::lock_guard<std::mutex> lock(transfers_mutex_);
1585
-
1586
- auto it = active_transfers_.find(transfer_id);
1587
- if (it != active_transfers_.end()) {
1588
- completed_transfers_[transfer_id] = it->second;
1589
- active_transfers_.erase(it);
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
- // Binary chunk transmission implementation (replaces base64 encoding)
1594
- //
1595
- // PERFORMANCE OPTIMIZATIONS:
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::handle_chunk_ack(const std::string& transfer_id, uint64_t chunk_index, bool success) {
1638
- auto progress = get_transfer_progress(transfer_id);
1639
- if (!progress) {
1640
- return;
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
- bool FileTransferManager::finalize_received_file(const std::string& transfer_id, const std::string& final_path) {
1655
- std::string temp_path = get_temp_file_path(transfer_id, config_.temp_directory);
1656
-
1657
- try {
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
- std::string FileTransferManager::calculate_chunk_checksum(const std::vector<uint8_t>& data) {
1678
- SHA1 sha1;
1679
- sha1.update(data.data(), data.size());
1680
- return sha1.finalize();
1681
- }
1342
+ // =============================================================================
1343
+ // Maintenance: timeouts and cleanup
1344
+ // =============================================================================
1682
1345
 
1683
- bool FileTransferManager::parse_chunk_binary_header(const std::vector<uint8_t>& binary_data, FileChunk& chunk) {
1684
- const std::string magic = "FTCHUNK";
1685
-
1686
- // Check minimum size (magic header + at least some data)
1687
- if (binary_data.size() < magic.length()) {
1688
- return false;
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
- bool FileTransferManager::verify_chunk_checksum(const FileChunk& chunk) {
1707
- if (!config_.verify_checksums || chunk.checksum.empty()) {
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
- void FileTransferManager::handle_file_request(const std::string& peer_id, const nlohmann::json& message) {
1716
- try {
1717
- std::string transfer_id = message["transfer_id"];
1718
- std::string remote_path = message["remote_path"];
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
- // Call user callback to approve/reject
1732
- if (file_request_callback_) {
1733
- bool accepted = file_request_callback_(peer_id, remote_path, transfer_id);
1734
- if (accepted) {
1735
- // Start file transfer
1736
- send_file(peer_id, remote_path);
1737
- LOG_FILE_TRANSFER_INFO("Accepted file request: " << remote_path << " for " << peer_id);
1738
- } else {
1739
- nlohmann::json response;
1740
- response["transfer_id"] = transfer_id;
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
- void FileTransferManager::handle_directory_request(const std::string& peer_id, const nlohmann::json& message) {
1762
- try {
1763
- std::string transfer_id = message["transfer_id"];
1764
- std::string remote_path = message["remote_path"];
1765
- bool recursive = message.value("recursive", true);
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
- // Call user callback to approve/reject
1779
- if (directory_request_callback_) {
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