Back to Site
Loading...
Searching...
No Matches
file_transfer.h
Go to the documentation of this file.
1#pragma once
2
48#include "util/rats_export.h"
49#include "node/peer_network.h"
50#include "peer/peer.h"
51#include "core/bytes.h"
52#include "peer/peer_id.h"
53#include "util/fs.h"
54
55extern "C" {
56#include "sha256.h"
57}
58
59#include <atomic>
60#include <chrono>
61#include <condition_variable>
62#include <cstdint>
63#include <functional>
64#include <memory>
65#include <mutex>
66#include <queue>
67#include <string>
68#include <thread>
69#include <unordered_map>
70#include <vector>
71
72namespace librats {
73
74class RATS_API FileTransfer final : public Subsystem {
75public:
76 struct Config {
77 uint32_t chunk_size = 64 * 1024;
78 uint32_t window_bytes = 4 * 1024 * 1024;
79 uint32_t progress_interval = 256 * 1024;
80 uint32_t transfer_timeout_secs = 60;
81 uint32_t worker_threads = 4;
82 uint32_t disk_threads = 4;
83 bool verify_integrity = true;
84 std::string temp_directory = ".";
85 };
86
87 enum class Status { Pending, Active, Paused, Completed, Failed, Cancelled };
88 enum class Direction { Sending, Receiving };
89
91 struct FileEntry {
92 std::string relative_path;
93 uint64_t size = 0;
94 };
95
97 struct Offer {
99 uint64_t id = 0;
100 std::string name;
101 uint64_t size = 0;
102 bool is_directory = false;
103 std::vector<FileEntry> files;
104 };
105
107 struct Progress {
108 uint64_t id = 0;
110 Direction direction = Direction::Sending;
111 Status status = Status::Pending;
112 uint64_t bytes_transferred = 0;
113 uint64_t total_bytes = 0;
114 uint32_t files_completed = 0;
115 uint32_t total_files = 0;
116
117 double transfer_rate_bps = 0.0;
118 double average_rate_bps = 0.0;
119 std::chrono::milliseconds elapsed{0};
120 std::chrono::milliseconds estimated_time_remaining{0};
121
123 double percent() const {
124 if (total_bytes == 0) return status == Status::Completed ? 100.0 : 0.0;
125 return static_cast<double>(bytes_transferred) / static_cast<double>(total_bytes) * 100.0;
126 }
127 };
128
130 struct Stats {
131 uint64_t bytes_sent = 0, bytes_received = 0;
132 uint64_t completed = 0, failed = 0;
133 };
134
135 using OfferHandler = std::function<void(const Offer&)>;
136 using ProgressHandler = std::function<void(const Progress&)>;
137 using CompleteHandler = std::function<void(uint64_t id, bool success, const std::string& path)>;
138
139 explicit FileTransfer(std::string temp_dir = ".");
140 explicit FileTransfer(Config config);
141 ~FileTransfer() override;
142
143 void on_offer(OfferHandler handler) { offer_handler_ = std::move(handler); }
144 void on_progress(ProgressHandler handler) { progress_handler_ = std::move(handler); }
145 void on_complete(CompleteHandler handler) { complete_handler_ = std::move(handler); }
146
148 uint64_t send_file(const PeerId& to, const std::string& path);
150 uint64_t send_directory(const PeerId& to, const std::string& dir_path);
151
154 void accept(const PeerId& from, uint64_t id, const std::string& dest_path);
155 void reject(const PeerId& from, uint64_t id);
156
158 bool cancel(const PeerId& peer, uint64_t id);
159 bool pause(const PeerId& peer, uint64_t id);
160 bool resume(const PeerId& peer, uint64_t id);
161
162 Stats stats() const;
163
167 static bool is_safe_relative_path(const std::string& p);
168
169 void attach(NodeContext& ctx) override;
170 void start() override;
171 void stop() override;
172
173private:
174 // Smoothed throughput + elapsed/ETA tracking for one transfer. Accessed only
175 // under the owning transfer's mutex. The clock starts lazily on the first
176 // sample (first byte activity); a long gap (pause/idle) is treated as a
177 // discontinuity so it doesn't register as a throughput dip.
178 struct RateTracker {
179 using clock = std::chrono::steady_clock;
180 clock::time_point start{};
181 clock::time_point mark{};
182 uint64_t mark_bytes = 0;
183 double rate_bps = 0.0;
184
185 void sample(uint64_t bytes, clock::time_point now) {
186 constexpr int64_t kMinMs = 250, kMaxMs = 2000;
187 constexpr double kAlpha = 0.4;
188 if (start == clock::time_point{}) { start = mark = now; mark_bytes = bytes; return; }
189 const int64_t dt = std::chrono::duration_cast<std::chrono::milliseconds>(now - mark).count();
190 if (dt < kMinMs) return; // too soon: hold the prior rate
191 if (dt > kMaxMs) { mark = now; mark_bytes = bytes; return; } // gap: reset, don't pollute
192 const double inst = static_cast<double>(bytes - mark_bytes) * 1000.0 / static_cast<double>(dt);
193 rate_bps = rate_bps <= 0.0 ? inst : kAlpha * inst + (1.0 - kAlpha) * rate_bps;
194 mark = now; mark_bytes = bytes;
195 }
196
197 void fill(Progress& p, uint64_t bytes, uint64_t total, clock::time_point now) const {
198 if (start == clock::time_point{}) return; // not live yet
199 const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now - start);
200 p.elapsed = ms;
201 p.transfer_rate_bps = rate_bps;
202 if (ms.count() > 0)
203 p.average_rate_bps = static_cast<double>(bytes) * 1000.0 / static_cast<double>(ms.count());
204 if (rate_bps > 1.0 && total > bytes)
205 p.estimated_time_remaining = std::chrono::milliseconds(
206 static_cast<int64_t>(static_cast<double>(total - bytes) / rate_bps * 1000.0));
207 }
208 };
209
210 // ── Per-transfer state (held via shared_ptr so workers/handlers keep it
211 // alive past map removal) ──────────────────────────────────────────────
212 struct Outgoing {
213 uint64_t id = 0;
214 PeerId peer;
215 std::string name;
216 std::string root;
217 bool is_directory = false;
218 std::vector<FileEntry> files;
219 std::vector<std::string> sources;
220 uint64_t total_bytes = 0;
221
222 std::mutex mtx;
223 std::condition_variable cv;
224 size_t cur_file = 0;
225 uint64_t cur_offset = 0;
226 uint64_t bytes_done = 0;
227 uint64_t acked = 0;
228 uint32_t files_done = 0;
229 Status status = Status::Pending;
230 bool worker_active = false;
231 bool finished = false;
232 sha256_context_t hash{};
233 std::chrono::steady_clock::time_point last_activity{};
234 RateTracker rate;
235 };
236
237 struct IncomingFile {
238 std::string relative_path;
239 uint64_t size = 0;
240 std::string final_path;
241 std::string temp_path;
242 uint64_t enqueued = 0;
243 uint64_t received = 0;
244 bool temp_created = false;
245 bool finalized = false;
246 };
247
248 // A unit of disk work queued by the reactor for the writer pool. A pure data
249 // job carries chunk bytes; a file-end job carries the sender's SHA-256 so the
250 // writer can verify + finalize once all of the file's data is on disk.
251 struct WriteJob {
252 uint32_t fidx = 0;
253 uint64_t offset = 0;
254 Bytes data;
255 bool is_file_end = false;
256 uint8_t sha[SHA256_HASH_SIZE]{};
257 };
258
259 struct Incoming {
260 uint64_t id = 0;
261 PeerId peer;
262 std::string name;
263 bool is_directory = false;
264 std::string dest_root;
265 std::vector<IncomingFile> files;
266
267 std::mutex mtx;
268 size_t recv_file = 0;
269 uint64_t bytes_done = 0;
270 uint64_t last_ack = 0;
271 uint32_t files_done = 0;
272 Status status = Status::Pending;
273 bool finished = false;
274 std::chrono::steady_clock::time_point last_activity{};
275 RateTracker rate;
276
277 // ── async disk writer (all fields below the queue are writer-thread only) ─
278 std::queue<WriteJob> wq;
279 uint64_t queued_bytes = 0;
280 bool scheduled = false;
281 FileStream out;
282 size_t out_idx = SIZE_MAX;
283 int hashing_file = -1;
284 sha256_context_t hash{};
285
286 ~Incoming();
287 };
288
289 // ── message handling (reactor thread) ─────────────────────────────────────
290 void on_message(const Peer& peer, ByteView payload);
291 void handle_offer(const PeerId& from, uint64_t id, bool is_dir, uint64_t total,
292 std::string name, std::vector<FileEntry> files);
293 void handle_chunk(const PeerId& from, uint64_t id, uint32_t fidx, uint64_t offset,
294 ByteView data);
295 void handle_file_end(const PeerId& from, uint64_t id, uint32_t fidx, const uint8_t* sha);
296
297 // ── sending ──────────────────────────────────────────────────────────────
298 uint64_t start_send(std::shared_ptr<Outgoing> t);
299 void queue_send(uint64_t id);
300 void worker_loop();
301 void run_send(const std::shared_ptr<Outgoing>& t);
302
303 // ── receiving ────────────────────────────────────────────────────────────
304 // The reactor thread only validates + copies a chunk into the transfer's write
305 // queue; a disk-writer thread does the blocking write, hashing and finalize off
306 // the reactor. `schedule_writer` hands a transfer to the pool (call with the
307 // transfer's mutex NOT held — pool lock is always taken after the transfer's).
308 void schedule_writer(const std::shared_ptr<Incoming>& t);
309 void disk_worker_loop();
310 void drain_writes(const std::shared_ptr<Incoming>& t);
311 void process_data(const std::shared_ptr<Incoming>& t, WriteJob& job);
312 void process_file_end(const std::shared_ptr<Incoming>& t, WriteJob& job);
313
314 // ── lifecycle helpers ─────────────────────────────────────────────────────
315 void maintenance_loop();
316 void finish_outgoing(const std::shared_ptr<Outgoing>& t, bool success);
317 void finish_incoming(const std::shared_ptr<Incoming>& t, bool success, const std::string& error);
318 void emit_progress(const std::shared_ptr<Outgoing>& t);
319 void emit_progress(const std::shared_ptr<Incoming>& t);
320
321 std::shared_ptr<Outgoing> find_outgoing(uint64_t id) const;
322 std::shared_ptr<Incoming> find_incoming(const PeerId& peer, uint64_t id) const;
323
324 void send_to(const PeerId& peer, const Bytes& msg) {
325 if (network_) network_->send(peer, MessageType::FileChunk, ByteView(msg));
326 }
327 void send_simple(const PeerId& peer, uint8_t op, uint64_t id);
328 void send_complete(const PeerId& peer, uint64_t id, bool ok);
329
330 PeerNetwork* network_ = nullptr;
331 Config config_;
332 std::atomic<uint64_t> next_id_{1};
333 std::atomic<bool> running_{false};
334
335 OfferHandler offer_handler_;
336 ProgressHandler progress_handler_;
337 CompleteHandler complete_handler_;
338
339 mutable std::mutex mutex_;
340 std::unordered_map<uint64_t, std::shared_ptr<Outgoing>> outgoing_;
341 std::unordered_map<PeerId, std::unordered_map<uint64_t, std::shared_ptr<Incoming>>,
342 PeerId::Hash> incoming_;
343
344 // send-side worker pool + send queue
345 std::vector<std::thread> workers_;
346 std::mutex queue_mutex_;
347 std::condition_variable queue_cv_;
348 std::queue<uint64_t> send_queue_;
349
350 // receive-side disk-writer pool + ready queue. A transfer is pushed here when it
351 // has pending write jobs; its `scheduled` flag keeps it single-owner so exactly
352 // one worker drains a given transfer at a time (preserving chunk order).
353 std::vector<std::thread> disk_workers_;
354 std::mutex disk_mutex_;
355 std::condition_variable disk_cv_;
356 std::queue<std::shared_ptr<Incoming>> disk_ready_;
357
358 // maintenance (idle timeout / purge)
359 std::thread maintenance_thread_;
360 std::mutex maintenance_mutex_;
361 std::condition_variable maintenance_cv_;
362
363 mutable std::mutex stats_mutex_;
364 Stats stats_;
365};
366
367} // namespace librats
FileTransfer(std::string temp_dir=".")
Stats stats() const
void start() override
static bool is_safe_relative_path(const std::string &p)
A relative path from a peer's directory manifest is safe only if it stays inside the destination: non...
uint64_t send_directory(const PeerId &to, const std::string &dir_path)
Offer a directory tree. Returns the transfer id (0 if the dir is unusable).
uint64_t send_file(const PeerId &to, const std::string &path)
Offer a single file. Returns the transfer id (0 if the file is unusable).
void on_progress(ProgressHandler handler)
void stop() override
bool cancel(const PeerId &peer, uint64_t id)
Control a live transfer (works from either side); (peer, id) names it.
std::function< void(const Offer &)> OfferHandler
bool resume(const PeerId &peer, uint64_t id)
std::function< void(uint64_t id, bool success, const std::string &path)> CompleteHandler
bool pause(const PeerId &peer, uint64_t id)
void on_offer(OfferHandler handler)
~FileTransfer() override
std::function< void(const Progress &)> ProgressHandler
void accept(const PeerId &from, uint64_t id, const std::string &dest_path)
Accept an offered transfer.
void on_complete(CompleteHandler handler)
void reject(const PeerId &from, uint64_t id)
void attach(NodeContext &ctx) override
FileTransfer(Config config)
A pluggable network subsystem.
Definition node.h:66
A lightweight handle to a connected peer.
Self-certifying peer identity.
The narrow contract a subsystem needs from the node — and nothing more.
One file inside a transfer (a single-file transfer has exactly one).
std::string relative_path
POSIX path relative to the transfer root.
Delivered to the offer callback so the app can accept() or reject().
std::vector< FileEntry > files
full manifest
std::string name
file or directory name
Snapshot passed to the progress callback (both directions).
double percent() const
Completion in [0, 100].