Back to Site
Loading...
Searching...
No Matches
file_transfer.h
Go to the documentation of this file.
1#pragma once
2
54#include "librats/util/rats_export.h"
56#include "librats/peer/peer.h"
57#include "librats/core/bytes.h"
59#include "librats/util/fs.h"
60
61extern "C" {
62#include "librats/crypto/sha256.h"
63}
64
65#include <atomic>
66#include <chrono>
67#include <condition_variable>
68#include <cstdint>
69#include <functional>
70#include <memory>
71#include <mutex>
72#include <queue>
73#include <string>
74#include <thread>
75#include <unordered_map>
76#include <vector>
77
78namespace librats {
79
80class RATS_API FileTransfer final : public Subsystem {
81public:
82 struct Config {
83 uint32_t chunk_size = 64 * 1024;
84 uint32_t window_bytes = 4 * 1024 * 1024;
85 uint32_t progress_interval = 256 * 1024;
86 uint32_t transfer_timeout_secs = 60;
87 // An offered transfer is waiting on a human or an app callback, not on the
88 // wire, so it gets its own (longer) deadline. Applied to both sides: the
89 // sender drops an offer nobody answered, and the receiver reclaims an offer
90 // its app never accepted or rejected.
91 uint32_t offer_timeout_secs = 300;
92 uint32_t worker_threads = 4;
93 uint32_t disk_threads = 4;
94 bool verify_integrity = true;
95 std::string temp_directory = ".";
96 };
97
98 enum class Status { Pending, Active, Paused, Completed, Failed, Cancelled };
99 enum class Direction { Sending, Receiving };
100
102 struct FileEntry {
103 std::string relative_path;
104 uint64_t size = 0;
105 };
106
108 struct Offer {
110 uint64_t id = 0;
111 std::string name;
112 uint64_t size = 0;
113 bool is_directory = false;
114 std::vector<FileEntry> files;
115 };
116
118 struct Progress {
119 uint64_t id = 0;
121 Direction direction = Direction::Sending;
122 Status status = Status::Pending;
123 uint64_t bytes_transferred = 0;
124 uint64_t total_bytes = 0;
125 uint32_t files_completed = 0;
126 uint32_t total_files = 0;
127
128 double transfer_rate_bps = 0.0;
129 double average_rate_bps = 0.0;
130 std::chrono::milliseconds elapsed{0};
131 std::chrono::milliseconds estimated_time_remaining{0};
132
134 double percent() const {
135 if (total_bytes == 0) return status == Status::Completed ? 100.0 : 0.0;
136 return static_cast<double>(bytes_transferred) / static_cast<double>(total_bytes) * 100.0;
137 }
138 };
139
141 struct Stats {
142 uint64_t bytes_sent = 0, bytes_received = 0;
143 uint64_t completed = 0, failed = 0;
144 };
145
146 using OfferHandler = std::function<void(const Offer&)>;
147 using ProgressHandler = std::function<void(const Progress&)>;
148 // The peer is part of the identity of a transfer, not decoration: incoming ids
149 // are allocated by the *sender*, so every node's first offer is id 1 and two
150 // peers routinely have a transfer of the same id in flight at once. A handler
151 // given only the id cannot tell which of them finished.
153 std::function<void(const PeerId& peer, uint64_t id, bool success, const std::string& path)>;
154
155 explicit FileTransfer(std::string temp_dir = ".");
156 explicit FileTransfer(Config config);
157 ~FileTransfer() override;
158
159 void on_offer(OfferHandler handler) { offer_handler_ = std::move(handler); }
160 void on_progress(ProgressHandler handler) { progress_handler_ = std::move(handler); }
161 void on_complete(CompleteHandler handler) { complete_handler_ = std::move(handler); }
162
164 uint64_t send_file(const PeerId& to, const std::string& path);
166 uint64_t send_directory(const PeerId& to, const std::string& dir_path);
167
170 void accept(const PeerId& from, uint64_t id, const std::string& dest_path);
171 void reject(const PeerId& from, uint64_t id);
172
195 bool accept_resume(const PeerId& from, uint64_t id, const std::string& dest_path,
196 const std::string& partial_path);
197
199 bool cancel(const PeerId& peer, uint64_t id);
200 bool pause(const PeerId& peer, uint64_t id);
201 bool resume(const PeerId& peer, uint64_t id);
202
203 Stats stats() const;
204
208 static bool is_safe_relative_path(const std::string& p);
209
210 void attach(NodeContext& ctx) override;
211 void start() override;
212 void stop() override;
213
214private:
215 // Smoothed throughput + elapsed/ETA tracking for one transfer. Accessed only
216 // under the owning transfer's mutex. The clock starts lazily on the first
217 // sample (first byte activity); a long gap (pause/idle) is treated as a
218 // discontinuity so it doesn't register as a throughput dip.
219 struct RateTracker {
220 using clock = std::chrono::steady_clock;
221 clock::time_point start{};
222 clock::time_point mark{};
223 uint64_t mark_bytes = 0;
224 double rate_bps = 0.0;
225
226 void sample(uint64_t bytes, clock::time_point now) {
227 constexpr int64_t kMinMs = 250, kMaxMs = 2000;
228 constexpr double kAlpha = 0.4;
229 if (start == clock::time_point{}) { start = mark = now; mark_bytes = bytes; return; }
230 const int64_t dt = std::chrono::duration_cast<std::chrono::milliseconds>(now - mark).count();
231 if (dt < kMinMs) return; // too soon: hold the prior rate
232 if (dt > kMaxMs) { mark = now; mark_bytes = bytes; return; } // gap: reset, don't pollute
233 const double inst = static_cast<double>(bytes - mark_bytes) * 1000.0 / static_cast<double>(dt);
234 rate_bps = rate_bps <= 0.0 ? inst : kAlpha * inst + (1.0 - kAlpha) * rate_bps;
235 mark = now; mark_bytes = bytes;
236 }
237
238 void fill(Progress& p, uint64_t bytes, uint64_t total, clock::time_point now) const {
239 if (start == clock::time_point{}) return; // not live yet
240 const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now - start);
241 p.elapsed = ms;
242 p.transfer_rate_bps = rate_bps;
243 if (ms.count() > 0)
244 p.average_rate_bps = static_cast<double>(bytes) * 1000.0 / static_cast<double>(ms.count());
245 if (rate_bps > 1.0 && total > bytes)
246 p.estimated_time_remaining = std::chrono::milliseconds(
247 static_cast<int64_t>(static_cast<double>(total - bytes) / rate_bps * 1000.0));
248 }
249 };
250
251 // ── Per-transfer state (held via shared_ptr so workers/handlers keep it
252 // alive past map removal) ──────────────────────────────────────────────
253 struct Outgoing {
254 uint64_t id = 0;
255 PeerId peer;
256 std::string name;
257 std::string root;
258 bool is_directory = false;
259 std::vector<FileEntry> files;
260 std::vector<std::string> sources;
261 uint64_t total_bytes = 0;
262
263 std::mutex mtx;
264 std::condition_variable cv;
265 size_t cur_file = 0;
266 uint64_t cur_offset = 0;
269 size_t hash_file = SIZE_MAX;
270 uint64_t bytes_done = 0;
271 uint64_t acked = 0;
272 uint32_t files_done = 0;
273 Status status = Status::Pending;
274 bool worker_active = false;
275 bool finished = false;
276 rats_sha256_context_t hash{};
277 std::chrono::steady_clock::time_point last_activity{};
278 RateTracker rate;
279 };
280
281 struct IncomingFile {
282 std::string relative_path;
283 uint64_t size = 0;
284 std::string final_path;
285 std::string temp_path;
286 uint64_t enqueued = 0;
287 uint64_t received = 0;
288 uint64_t resume_offset = 0;
289 bool keep_partial = false;
290 bool temp_created = false;
291 bool finalized = false;
292 };
293
294 // A unit of disk work queued by the reactor for the writer pool. A pure data
295 // job carries chunk bytes; a file-end job carries the sender's SHA-256 so the
296 // writer can verify + finalize once all of the file's data is on disk.
297 struct WriteJob {
298 uint32_t fidx = 0;
299 uint64_t offset = 0;
300 Bytes data;
301 bool is_file_end = false;
302 uint8_t sha[RATS_SHA256_HASH_SIZE]{};
303 };
304
305 struct Incoming {
306 uint64_t id = 0;
307 PeerId peer;
308 std::string name;
309 bool is_directory = false;
310 std::string dest_root;
311 std::vector<IncomingFile> files;
312
313 std::mutex mtx;
314 size_t recv_file = 0;
315 uint64_t bytes_done = 0;
316 uint64_t last_ack = 0;
317 uint32_t files_done = 0;
318 Status status = Status::Pending;
319 bool finished = false;
320 std::chrono::steady_clock::time_point last_activity{};
321 RateTracker rate;
322
323 // ── async disk writer (all fields below the queue are writer-thread only) ─
324 std::queue<WriteJob> wq;
325 uint64_t queued_bytes = 0;
326 bool scheduled = false;
327 FileStream out;
328 size_t out_idx = SIZE_MAX;
329 int hashing_file = -1;
330 rats_sha256_context_t hash{};
331
332 ~Incoming();
333 };
334
335 // ── message handling (reactor thread) ─────────────────────────────────────
336 void on_message(const Peer& peer, ByteView payload);
337 void handle_offer(const PeerId& from, uint64_t id, bool is_dir, uint64_t total,
338 std::string name, std::vector<FileEntry> files);
339 void handle_chunk(const PeerId& from, uint64_t id, uint32_t fidx, uint64_t offset,
340 ByteView data);
341 void handle_file_end(const PeerId& from, uint64_t id, uint32_t fidx, const uint8_t* sha);
342
343 // ── sending ──────────────────────────────────────────────────────────────
344 uint64_t start_send(std::shared_ptr<Outgoing> t);
345 void queue_send(uint64_t id);
346 void worker_loop();
347 void run_send(const std::shared_ptr<Outgoing>& t);
348
349 // ── receiving ────────────────────────────────────────────────────────────
350 // The reactor thread only validates + copies a chunk into the transfer's write
351 // queue; a disk-writer thread does the blocking write, hashing and finalize off
352 // the reactor. `schedule_writer` hands a transfer to the pool (call with the
353 // transfer's mutex NOT held — pool lock is always taken after the transfer's).
354 void schedule_writer(const std::shared_ptr<Incoming>& t);
355 void disk_worker_loop();
356 // Start the whole-file hash for `fidx`, seeding it with the `prefix` bytes a
357 // resumed transfer already has on disk (the digest covers the file, not the
358 // session). Writer thread only; false means the partial could not be read.
359 bool begin_hash(const std::shared_ptr<Incoming>& t, uint32_t fidx, const std::string& path, uint64_t prefix);
360 void drain_writes(const std::shared_ptr<Incoming>& t);
361 void process_data(const std::shared_ptr<Incoming>& t, WriteJob& job);
362 void process_file_end(const std::shared_ptr<Incoming>& t, WriteJob& job);
363
364 // Shared by accept() and accept_resume(): `partial_path` empty means the
365 // ordinary internal temp, non-empty an app-owned partial to continue from.
366 bool accept_impl(const PeerId& from, uint64_t id, const std::string& dest_path,
367 const std::string& partial_path);
368
369 // ── lifecycle helpers ─────────────────────────────────────────────────────
370 void maintenance_loop();
371 void finish_outgoing(const std::shared_ptr<Outgoing>& t, bool success);
372 void finish_incoming(const std::shared_ptr<Incoming>& t, bool success, const std::string& error);
373 void emit_progress(const std::shared_ptr<Outgoing>& t);
374 void emit_progress(const std::shared_ptr<Incoming>& t);
375
376 // Both lookups are peer-scoped. Outgoing ids are unique on this node, so the
377 // peer is not needed to *find* the transfer — it is needed to establish that
378 // the sender of the message is the transfer's counterparty. Without that check
379 // any connected peer can cancel, complete or stall somebody else's transfer by
380 // naming an id it was never given.
381 std::shared_ptr<Outgoing> find_outgoing(const PeerId& peer, uint64_t id) const;
382 // Unchecked lookup, for ids that came off our own send queue rather than off
383 // the wire. Never call it with an id a peer supplied.
384 std::shared_ptr<Outgoing> find_own_outgoing(uint64_t id) const;
385 std::shared_ptr<Incoming> find_incoming(const PeerId& peer, uint64_t id) const;
386
387 void send_to(const PeerId& peer, const Bytes& msg) {
388 if (network_) network_->send(peer, MessageType::FileChunk, ByteView(msg));
389 }
390 void send_simple(const PeerId& peer, uint8_t op, uint64_t id);
391 void send_complete(const PeerId& peer, uint64_t id, bool ok);
392
393 PeerNetwork* network_ = nullptr;
394 Config config_;
395 std::atomic<uint64_t> next_id_{1};
396 std::atomic<bool> running_{false};
397
398 OfferHandler offer_handler_;
399 ProgressHandler progress_handler_;
400 CompleteHandler complete_handler_;
401
402 mutable std::mutex mutex_;
403 std::unordered_map<uint64_t, std::shared_ptr<Outgoing>> outgoing_;
404 std::unordered_map<PeerId, std::unordered_map<uint64_t, std::shared_ptr<Incoming>>,
405 PeerId::Hash> incoming_;
406
407 // send-side worker pool + send queue
408 std::vector<std::thread> workers_;
409 std::mutex queue_mutex_;
410 std::condition_variable queue_cv_;
411 std::queue<uint64_t> send_queue_;
412
413 // receive-side disk-writer pool + ready queue. A transfer is pushed here when it
414 // has pending write jobs; its `scheduled` flag keeps it single-owner so exactly
415 // one worker drains a given transfer at a time (preserving chunk order).
416 std::vector<std::thread> disk_workers_;
417 std::mutex disk_mutex_;
418 std::condition_variable disk_cv_;
419 std::queue<std::shared_ptr<Incoming>> disk_ready_;
420
421 // maintenance (idle timeout / purge)
422 std::thread maintenance_thread_;
423 std::mutex maintenance_mutex_;
424 std::condition_variable maintenance_cv_;
425
426 mutable std::mutex stats_mutex_;
427 Stats stats_;
428};
429
430} // namespace librats
Lightweight byte container aliases and a non-owning byte view.
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)
bool pause(const PeerId &peer, uint64_t id)
void on_offer(OfferHandler handler)
~FileTransfer() override
std::function< void(const Progress &)> ProgressHandler
std::function< void(const PeerId &peer, uint64_t id, bool success, const std::string &path)> CompleteHandler
void accept(const PeerId &from, uint64_t id, const std::string &dest_path)
Accept an offered transfer.
void on_complete(CompleteHandler handler)
bool accept_resume(const PeerId &from, uint64_t id, const std::string &dest_path, const std::string &partial_path)
Accept a single-file offer, continuing an earlier attempt at the same file.
void reject(const PeerId &from, uint64_t id)
void attach(NodeContext &ctx) override
FileTransfer(Config config)
A pluggable network subsystem.
Definition node.h:73
std::vector< uint8_t > Bytes
Definition bytes.h:24
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].