truffle_core/file_transfer/
mod.rs1pub mod receiver;
39pub mod sender;
40pub mod types;
41
42pub use types::*;
43
44use std::sync::atomic::{AtomicU64, Ordering};
45use std::sync::Arc;
46
47use tokio::sync::{broadcast, mpsc, Mutex};
48use tokio::task::JoinHandle;
49use tracing::info;
50
51use crate::network::NetworkProvider;
52use crate::node::Node;
53
54const DEFAULT_MAX_TRANSFER_SIZE: u64 = 1_073_741_824;
56
57pub(crate) const MAX_CONCURRENT_INCOMING: usize = 32;
61
62pub(crate) const MAX_PENDING_OFFERS_PER_PEER: usize = 4;
65
66const OFFER_CHANNEL_CAPACITY: usize = 32;
69
70pub(crate) struct FileTransferState {
77 pub(crate) event_tx: broadcast::Sender<FileTransferEvent>,
79 pub(crate) offer_tx: Mutex<Option<mpsc::Sender<(FileOffer, OfferResponder)>>>,
81 pub(crate) max_transfer_size: AtomicU64,
83 pub(crate) pull_roots: std::sync::RwLock<Vec<std::path::PathBuf>>,
85 pub(crate) receiver_handle: Mutex<Option<JoinHandle<()>>>,
87 pub(crate) overwrite_policy: std::sync::RwLock<OverwritePolicy>,
89 pub(crate) incoming_ops: Arc<tokio::sync::Semaphore>,
91 pub(crate) pending_offers_per_peer:
93 Arc<std::sync::Mutex<std::collections::HashMap<String, usize>>>,
94}
95
96impl FileTransferState {
97 pub(crate) fn new() -> Self {
99 let (event_tx, _) = broadcast::channel(256);
100 Self {
101 event_tx,
102 offer_tx: Mutex::new(None),
103 max_transfer_size: AtomicU64::new(DEFAULT_MAX_TRANSFER_SIZE),
104 pull_roots: std::sync::RwLock::new(Vec::new()),
105 receiver_handle: Mutex::new(None),
106 overwrite_policy: std::sync::RwLock::new(OverwritePolicy::default()),
107 incoming_ops: Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_INCOMING)),
108 pending_offers_per_peer: Arc::new(std::sync::Mutex::new(
109 std::collections::HashMap::new(),
110 )),
111 }
112 }
113
114 pub(crate) fn overwrite_policy(&self) -> OverwritePolicy {
116 *self.overwrite_policy.read().unwrap()
117 }
118}
119
120pub struct FileTransfer<'a, N: NetworkProvider + 'static> {
127 node: &'a Node<N>,
129}
130
131impl<'a, N: NetworkProvider + 'static> FileTransfer<'a, N> {
132 pub(crate) fn new(node: &'a Node<N>) -> Self {
134 Self { node }
135 }
136
137 fn state(&self) -> &FileTransferState {
139 &self.node.file_transfer_state
140 }
141
142 pub fn subscribe(&self) -> broadcast::Receiver<FileTransferEvent> {
147 self.state().event_tx.subscribe()
148 }
149
150 pub fn set_max_transfer_size(&self, bytes: u64) {
154 self.state()
155 .max_transfer_size
156 .store(bytes, Ordering::Relaxed);
157 }
158
159 pub fn max_transfer_size(&self) -> u64 {
161 self.state().max_transfer_size.load(Ordering::Relaxed)
162 }
163
164 pub fn set_overwrite_policy(&self, policy: OverwritePolicy) {
169 *self.state().overwrite_policy.write().unwrap() = policy;
170 }
171
172 pub fn overwrite_policy(&self) -> OverwritePolicy {
174 self.state().overwrite_policy()
175 }
176
177 pub fn add_pull_root(&self, root: &str) -> std::io::Result<()> {
186 let canon = std::fs::canonicalize(root)?;
187 self.state().pull_roots.write().unwrap().push(canon);
188 Ok(())
189 }
190
191 pub fn pull_roots(&self) -> Vec<std::path::PathBuf> {
195 self.state().pull_roots.read().unwrap().clone()
196 }
197
198 pub fn clear_pull_roots(&self) {
201 self.state().pull_roots.write().unwrap().clear();
202 }
203
204 pub async fn offer_channel(
221 &self,
222 node: Arc<Node<N>>,
223 ) -> mpsc::Receiver<(FileOffer, OfferResponder)> {
224 let (tx, rx) = mpsc::channel(OFFER_CHANNEL_CAPACITY);
225 {
226 let mut offer_tx = self.state().offer_tx.lock().await;
227 *offer_tx = Some(tx.clone());
228 }
229 self.ensure_receiver_started(node, tx).await;
230 rx
231 }
232
233 pub async fn auto_accept(&self, node: Arc<Node<N>>, output_dir: &str) {
240 let cancel = node.tasks.cancel.clone();
242 let tracker = node.tasks.tracker.clone();
243 let mut rx = self.offer_channel(node).await;
244 let output_dir = format!("{}/", output_dir.trim_end_matches(['/', '\\']));
251
252 tracker.spawn(async move {
253 loop {
254 let recv = tokio::select! {
255 _ = cancel.cancelled() => break,
256 r = rx.recv() => r,
257 };
258 let Some((offer, responder)) = recv else {
259 break;
260 };
261 info!(
262 file = offer.file_name.as_str(),
263 dir = output_dir.as_str(),
264 "Auto-accepting file offer"
265 );
266 responder.accept(&output_dir);
267 }
268 });
269 }
270
271 pub async fn auto_reject(&self, node: Arc<Node<N>>) {
276 let cancel = node.tasks.cancel.clone();
277 let tracker = node.tasks.tracker.clone();
278 let mut rx = self.offer_channel(node).await;
279
280 tracker.spawn(async move {
281 loop {
282 let recv = tokio::select! {
283 _ = cancel.cancelled() => break,
284 r = rx.recv() => r,
285 };
286 let Some((_offer, responder)) = recv else {
287 break;
288 };
289 responder.reject("auto-rejected");
290 }
291 });
292 }
293
294 pub async fn send_file(
304 &self,
305 peer_id: &str,
306 local_path: &str,
307 remote_path: &str,
308 ) -> Result<TransferResult, TransferError> {
309 let max_size = self.state().max_transfer_size.load(Ordering::Relaxed);
310 sender::send_file(
311 self.node,
312 peer_id,
313 local_path,
314 remote_path,
315 max_size,
316 &self.state().event_tx,
317 )
318 .await
319 }
320
321 pub async fn pull_file(
331 &self,
332 peer_id: &str,
333 remote_path: &str,
334 local_path: &str,
335 ) -> Result<TransferResult, TransferError> {
336 pull_file(
337 self.node,
338 peer_id,
339 remote_path,
340 local_path,
341 &self.state().event_tx,
342 )
343 .await
344 }
345
346 async fn ensure_receiver_started(
351 &self,
352 node: Arc<Node<N>>,
353 offer_tx: mpsc::Sender<(FileOffer, OfferResponder)>,
354 ) {
355 let mut handle = self.state().receiver_handle.lock().await;
356 if let Some(h) = handle.take() {
358 h.abort();
359 }
360 let h = receiver::spawn_receive_handler(node, offer_tx, self.state().event_tx.clone());
361 *handle = Some(h);
362 }
363}
364
365async fn pull_file<N: NetworkProvider + 'static>(
379 node: &Node<N>,
380 peer_id: &str,
381 remote_path: &str,
382 local_path: &str,
383 event_tx: &broadcast::Sender<FileTransferEvent>,
384) -> Result<TransferResult, TransferError> {
385 use sha2::{Digest, Sha256};
386 use std::time::Instant;
387 use tokio::io::{AsyncReadExt, AsyncWriteExt};
388
389 let start = Instant::now();
390 let token = uuid::Uuid::new_v4().to_string();
391
392 let policy = node.file_transfer_state.overwrite_policy();
395 if policy == OverwritePolicy::Reject && tokio::fs::try_exists(local_path).await.unwrap_or(false)
396 {
397 return Err(TransferError::Protocol(format!(
398 "destination already exists: {local_path} (OverwritePolicy::Reject)"
399 )));
400 }
401
402 let requester_id = node.local_info().device_id;
406
407 let peer_id = node
412 .resolve_peer(peer_id)
413 .await
414 .map_err(|e| TransferError::Node(e.to_string()))?
415 .id;
416 let peer_id = peer_id.as_str();
417
418 let pull_req = FtMessage::PullRequest {
420 path: remote_path.to_string(),
421 requester_id,
422 token: token.clone(),
423 };
424
425 let pull_payload = serde_json::to_value(&pull_req)
426 .map_err(|e| TransferError::Protocol(format!("Failed to serialize pull request: {e}")))?;
427
428 tracing::info!(peer = peer_id, path = remote_path, "Sending PULL_REQUEST");
429
430 let (offer_sha256, offer_size, offer_token, file_name) = crate::request_reply::send_and_wait(
431 node,
432 peer_id,
433 "ft",
434 "pull_request",
435 &pull_payload,
436 std::time::Duration::from_secs(30),
437 |msg| {
438 if msg.from != peer_id {
439 return None;
440 }
441 let ft_msg: FtMessage = serde_json::from_value(msg.payload.clone()).ok()?;
442 match ft_msg {
443 FtMessage::Offer {
444 file_name,
445 sha256,
446 size,
447 token: offer_token,
448 ..
449 } => {
450 tracing::info!(size = size, "Received OFFER from peer");
451 Some(Ok((sha256, size, offer_token, file_name)))
452 }
453 FtMessage::Reject { reason, .. } => Some(Err(TransferError::Rejected(reason))),
454 _ => None,
455 }
456 },
457 )
458 .await
459 .map_err(|e| match e {
460 crate::request_reply::RequestError::Timeout => TransferError::Timeout,
461 crate::request_reply::RequestError::Send(e) => TransferError::Node(e.to_string()),
462 crate::request_reply::RequestError::ChannelClosed => {
463 TransferError::Protocol("Channel closed".into())
464 }
465 })??;
466
467 let max_size = node
469 .file_transfer_state
470 .max_transfer_size
471 .load(Ordering::Relaxed);
472 if offer_size > max_size {
473 return Err(TransferError::Protocol(format!(
474 "Offered file size {offer_size} exceeds max transfer size {max_size}"
475 )));
476 }
477
478 let mut listener = node
480 .listen_tcp(0)
481 .await
482 .map_err(|e| TransferError::Node(format!("Failed to listen TCP: {e}")))?;
483
484 let accept = FtMessage::Accept {
485 token: offer_token,
486 tcp_port: listener.port,
487 };
488
489 let accept_payload = serde_json::to_value(&accept)
490 .map_err(|e| TransferError::Protocol(format!("Failed to serialize accept: {e}")))?;
491
492 node.send_typed(peer_id, "ft", "accept", &accept_payload)
493 .await
494 .map_err(|e| TransferError::Node(e.to_string()))?;
495
496 tracing::info!(
497 port = listener.port,
498 "Sent ACCEPT, listening for TCP connection"
499 );
500
501 let incoming = tokio::time::timeout(tokio::time::Duration::from_secs(30), listener.accept())
503 .await
504 .map_err(|_| TransferError::Timeout)?
505 .ok_or_else(|| TransferError::Protocol("Listener closed before accepting".to_string()))?;
506
507 let mut stream = incoming.stream;
508 tracing::info!("TCP connection accepted from peer");
509
510 let mut size_buf = [0u8; 8];
512 stream.read_exact(&mut size_buf).await?;
513 let file_size = u64::from_be_bytes(size_buf);
514
515 let mut sha_buf = [0u8; 64]; stream.read_exact(&mut sha_buf).await?;
517 let received_sha = String::from_utf8_lossy(&sha_buf).to_string();
518
519 if received_sha != offer_sha256 {
521 return Err(TransferError::IntegrityError {
522 expected: offer_sha256,
523 actual: received_sha,
524 });
525 }
526
527 if file_size != offer_size {
528 return Err(TransferError::Protocol(format!(
529 "Size mismatch: offer said {offer_size}, stream header says {file_size}"
530 )));
531 }
532
533 if let Some(parent) = std::path::Path::new(local_path).parent() {
536 tokio::fs::create_dir_all(parent).await?;
537 }
538
539 let temp_path = format!("{local_path}.{token}.truffle-tmp");
543 let mut temp_file = tokio::fs::OpenOptions::new()
544 .write(true)
545 .create_new(true)
546 .open(&temp_path)
547 .await?;
548 let mut hasher = Sha256::new();
549 let mut bytes_received: u64 = 0;
550 let progress_start = Instant::now();
551 let mut buf = vec![0u8; 64 * 1024];
552
553 while bytes_received < file_size {
554 let to_read = ((file_size - bytes_received) as usize).min(buf.len());
555 let n = stream.read(&mut buf[..to_read]).await?;
556 if n == 0 {
557 tokio::fs::remove_file(&temp_path).await.ok();
558 return Err(TransferError::Io(std::io::Error::new(
559 std::io::ErrorKind::UnexpectedEof,
560 format!("Connection closed after {bytes_received}/{file_size} bytes"),
561 )));
562 }
563 hasher.update(&buf[..n]);
564 AsyncWriteExt::write_all(&mut temp_file, &buf[..n]).await?;
565 bytes_received += n as u64;
566
567 let elapsed = progress_start.elapsed().as_secs_f64();
568 let speed = if elapsed > 0.0 {
569 bytes_received as f64 / elapsed
570 } else {
571 0.0
572 };
573
574 let _ = event_tx.send(FileTransferEvent::Progress(TransferProgress {
576 token: token.clone(),
577 direction: TransferDirection::Receive,
578 file_name: file_name.clone(),
579 bytes_transferred: bytes_received,
580 total_bytes: file_size,
581 speed_bps: speed,
582 }));
583 }
584
585 AsyncWriteExt::flush(&mut temp_file).await?;
587
588 let actual_sha = hex::encode(hasher.finalize());
590
591 if actual_sha != offer_sha256 {
592 stream.write_all(&[0x00]).await?;
594 tokio::fs::remove_file(&temp_path).await.ok();
595 return Err(TransferError::IntegrityError {
596 expected: offer_sha256,
597 actual: actual_sha,
598 });
599 }
600
601 let placed_path = receiver::finalize_received_file(&temp_path, local_path, policy).await?;
603
604 stream.write_all(&[0x01]).await?;
606
607 let elapsed = start.elapsed().as_secs_f64();
608 tracing::info!(
609 bytes = file_size,
610 elapsed_ms = (elapsed * 1000.0) as u64,
611 path = placed_path.as_str(),
612 "Download complete"
613 );
614
615 let _ = event_tx.send(FileTransferEvent::Completed {
617 token,
618 direction: TransferDirection::Receive,
619 file_name,
620 bytes_transferred: file_size,
621 sha256: actual_sha.clone(),
622 elapsed_secs: elapsed,
623 });
624
625 Ok(TransferResult {
626 bytes_transferred: file_size,
627 sha256: actual_sha,
628 elapsed_secs: elapsed,
629 })
630}