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) struct FileTransferState {
64 pub(crate) event_tx: broadcast::Sender<FileTransferEvent>,
66 pub(crate) offer_tx: Mutex<Option<mpsc::UnboundedSender<(FileOffer, OfferResponder)>>>,
68 pub(crate) max_transfer_size: AtomicU64,
70 pub(crate) pull_roots: std::sync::RwLock<Vec<std::path::PathBuf>>,
72 pub(crate) receiver_handle: Mutex<Option<JoinHandle<()>>>,
74}
75
76impl FileTransferState {
77 pub(crate) fn new() -> Self {
79 let (event_tx, _) = broadcast::channel(256);
80 Self {
81 event_tx,
82 offer_tx: Mutex::new(None),
83 max_transfer_size: AtomicU64::new(DEFAULT_MAX_TRANSFER_SIZE),
84 pull_roots: std::sync::RwLock::new(Vec::new()),
85 receiver_handle: Mutex::new(None),
86 }
87 }
88}
89
90pub struct FileTransfer<'a, N: NetworkProvider + 'static> {
97 node: &'a Node<N>,
99}
100
101impl<'a, N: NetworkProvider + 'static> FileTransfer<'a, N> {
102 pub(crate) fn new(node: &'a Node<N>) -> Self {
104 Self { node }
105 }
106
107 fn state(&self) -> &FileTransferState {
109 &self.node.file_transfer_state
110 }
111
112 pub fn subscribe(&self) -> broadcast::Receiver<FileTransferEvent> {
117 self.state().event_tx.subscribe()
118 }
119
120 pub fn set_max_transfer_size(&self, bytes: u64) {
124 self.state()
125 .max_transfer_size
126 .store(bytes, Ordering::Relaxed);
127 }
128
129 pub fn max_transfer_size(&self) -> u64 {
131 self.state().max_transfer_size.load(Ordering::Relaxed)
132 }
133
134 pub fn add_pull_root(&self, root: &str) -> std::io::Result<()> {
143 let canon = std::fs::canonicalize(root)?;
144 self.state().pull_roots.write().unwrap().push(canon);
145 Ok(())
146 }
147
148 pub fn pull_roots(&self) -> Vec<std::path::PathBuf> {
152 self.state().pull_roots.read().unwrap().clone()
153 }
154
155 pub fn clear_pull_roots(&self) {
158 self.state().pull_roots.write().unwrap().clear();
159 }
160
161 pub async fn offer_channel(
174 &self,
175 node: Arc<Node<N>>,
176 ) -> mpsc::UnboundedReceiver<(FileOffer, OfferResponder)> {
177 let (tx, rx) = mpsc::unbounded_channel();
178 {
179 let mut offer_tx = self.state().offer_tx.lock().await;
180 *offer_tx = Some(tx.clone());
181 }
182 self.ensure_receiver_started(node, tx).await;
183 rx
184 }
185
186 pub async fn auto_accept(&self, node: Arc<Node<N>>, output_dir: &str) {
193 let mut rx = self.offer_channel(node).await;
194 let output_dir = format!("{}/", output_dir.trim_end_matches(['/', '\\']));
201
202 tokio::spawn(async move {
203 while let Some((offer, responder)) = rx.recv().await {
204 info!(
205 file = offer.file_name.as_str(),
206 dir = output_dir.as_str(),
207 "Auto-accepting file offer"
208 );
209 responder.accept(&output_dir);
210 }
211 });
212 }
213
214 pub async fn auto_reject(&self, node: Arc<Node<N>>) {
219 let mut rx = self.offer_channel(node).await;
220
221 tokio::spawn(async move {
222 while let Some((_offer, responder)) = rx.recv().await {
223 responder.reject("auto-rejected");
224 }
225 });
226 }
227
228 pub async fn send_file(
238 &self,
239 peer_id: &str,
240 local_path: &str,
241 remote_path: &str,
242 ) -> Result<TransferResult, TransferError> {
243 let max_size = self.state().max_transfer_size.load(Ordering::Relaxed);
244 sender::send_file(
245 self.node,
246 peer_id,
247 local_path,
248 remote_path,
249 max_size,
250 &self.state().event_tx,
251 )
252 .await
253 }
254
255 pub async fn pull_file(
265 &self,
266 peer_id: &str,
267 remote_path: &str,
268 local_path: &str,
269 ) -> Result<TransferResult, TransferError> {
270 pull_file(
271 self.node,
272 peer_id,
273 remote_path,
274 local_path,
275 &self.state().event_tx,
276 )
277 .await
278 }
279
280 async fn ensure_receiver_started(
285 &self,
286 node: Arc<Node<N>>,
287 offer_tx: mpsc::UnboundedSender<(FileOffer, OfferResponder)>,
288 ) {
289 let mut handle = self.state().receiver_handle.lock().await;
290 if let Some(h) = handle.take() {
292 h.abort();
293 }
294 let h = receiver::spawn_receive_handler(node, offer_tx, self.state().event_tx.clone());
295 *handle = Some(h);
296 }
297}
298
299async fn pull_file<N: NetworkProvider + 'static>(
313 node: &Node<N>,
314 peer_id: &str,
315 remote_path: &str,
316 local_path: &str,
317 event_tx: &broadcast::Sender<FileTransferEvent>,
318) -> Result<TransferResult, TransferError> {
319 use sha2::{Digest, Sha256};
320 use std::time::Instant;
321 use tokio::io::{AsyncReadExt, AsyncWriteExt};
322
323 let start = Instant::now();
324 let token = uuid::Uuid::new_v4().to_string();
325 let requester_id = node.local_info().device_id;
329
330 let peer_id = node
332 .resolve_peer_id(peer_id)
333 .await
334 .map_err(|e| TransferError::Node(e.to_string()))?;
335 let peer_id = peer_id.as_str();
336
337 let pull_req = FtMessage::PullRequest {
339 path: remote_path.to_string(),
340 requester_id,
341 token: token.clone(),
342 };
343
344 let pull_payload = serde_json::to_value(&pull_req)
345 .map_err(|e| TransferError::Protocol(format!("Failed to serialize pull request: {e}")))?;
346
347 tracing::info!(peer = peer_id, path = remote_path, "Sending PULL_REQUEST");
348
349 let (offer_sha256, offer_size, offer_token, file_name) = crate::request_reply::send_and_wait(
350 node,
351 peer_id,
352 "ft",
353 "pull_request",
354 &pull_payload,
355 std::time::Duration::from_secs(30),
356 |msg| {
357 if msg.from != peer_id {
358 return None;
359 }
360 let ft_msg: FtMessage = serde_json::from_value(msg.payload.clone()).ok()?;
361 match ft_msg {
362 FtMessage::Offer {
363 file_name,
364 sha256,
365 size,
366 token: offer_token,
367 ..
368 } => {
369 tracing::info!(size = size, "Received OFFER from peer");
370 Some(Ok((sha256, size, offer_token, file_name)))
371 }
372 FtMessage::Reject { reason, .. } => Some(Err(TransferError::Rejected(reason))),
373 _ => None,
374 }
375 },
376 )
377 .await
378 .map_err(|e| match e {
379 crate::request_reply::RequestError::Timeout => TransferError::Timeout,
380 crate::request_reply::RequestError::Send(e) => TransferError::Node(e.to_string()),
381 crate::request_reply::RequestError::ChannelClosed => {
382 TransferError::Protocol("Channel closed".into())
383 }
384 })??;
385
386 let max_size = node
388 .file_transfer_state
389 .max_transfer_size
390 .load(Ordering::Relaxed);
391 if offer_size > max_size {
392 return Err(TransferError::Protocol(format!(
393 "Offered file size {offer_size} exceeds max transfer size {max_size}"
394 )));
395 }
396
397 let mut listener = node
399 .listen_tcp(0)
400 .await
401 .map_err(|e| TransferError::Node(format!("Failed to listen TCP: {e}")))?;
402
403 let accept = FtMessage::Accept {
404 token: offer_token,
405 tcp_port: listener.port,
406 };
407
408 let accept_payload = serde_json::to_value(&accept)
409 .map_err(|e| TransferError::Protocol(format!("Failed to serialize accept: {e}")))?;
410
411 node.send_typed(peer_id, "ft", "accept", &accept_payload)
412 .await
413 .map_err(|e| TransferError::Node(e.to_string()))?;
414
415 tracing::info!(
416 port = listener.port,
417 "Sent ACCEPT, listening for TCP connection"
418 );
419
420 let incoming = tokio::time::timeout(tokio::time::Duration::from_secs(30), listener.accept())
422 .await
423 .map_err(|_| TransferError::Timeout)?
424 .ok_or_else(|| TransferError::Protocol("Listener closed before accepting".to_string()))?;
425
426 let mut stream = incoming.stream;
427 tracing::info!("TCP connection accepted from peer");
428
429 let mut size_buf = [0u8; 8];
431 stream.read_exact(&mut size_buf).await?;
432 let file_size = u64::from_be_bytes(size_buf);
433
434 let mut sha_buf = [0u8; 64]; stream.read_exact(&mut sha_buf).await?;
436 let received_sha = String::from_utf8_lossy(&sha_buf).to_string();
437
438 if received_sha != offer_sha256 {
440 return Err(TransferError::IntegrityError {
441 expected: offer_sha256,
442 actual: received_sha,
443 });
444 }
445
446 if file_size != offer_size {
447 return Err(TransferError::Protocol(format!(
448 "Size mismatch: offer said {offer_size}, stream header says {file_size}"
449 )));
450 }
451
452 if let Some(parent) = std::path::Path::new(local_path).parent() {
455 tokio::fs::create_dir_all(parent).await?;
456 }
457
458 let temp_path = format!("{local_path}.truffle-tmp");
459 let mut temp_file = tokio::fs::File::create(&temp_path).await?;
460 let mut hasher = Sha256::new();
461 let mut bytes_received: u64 = 0;
462 let progress_start = Instant::now();
463 let mut buf = vec![0u8; 64 * 1024];
464
465 while bytes_received < file_size {
466 let to_read = ((file_size - bytes_received) as usize).min(buf.len());
467 let n = stream.read(&mut buf[..to_read]).await?;
468 if n == 0 {
469 tokio::fs::remove_file(&temp_path).await.ok();
470 return Err(TransferError::Io(std::io::Error::new(
471 std::io::ErrorKind::UnexpectedEof,
472 format!("Connection closed after {bytes_received}/{file_size} bytes"),
473 )));
474 }
475 hasher.update(&buf[..n]);
476 AsyncWriteExt::write_all(&mut temp_file, &buf[..n]).await?;
477 bytes_received += n as u64;
478
479 let elapsed = progress_start.elapsed().as_secs_f64();
480 let speed = if elapsed > 0.0 {
481 bytes_received as f64 / elapsed
482 } else {
483 0.0
484 };
485
486 let _ = event_tx.send(FileTransferEvent::Progress(TransferProgress {
488 token: token.clone(),
489 direction: TransferDirection::Receive,
490 file_name: file_name.clone(),
491 bytes_transferred: bytes_received,
492 total_bytes: file_size,
493 speed_bps: speed,
494 }));
495 }
496
497 AsyncWriteExt::flush(&mut temp_file).await?;
499
500 let actual_sha = hex::encode(hasher.finalize());
502
503 if actual_sha != offer_sha256 {
504 stream.write_all(&[0x00]).await?;
506 tokio::fs::remove_file(&temp_path).await.ok();
507 return Err(TransferError::IntegrityError {
508 expected: offer_sha256,
509 actual: actual_sha,
510 });
511 }
512
513 tokio::fs::rename(&temp_path, local_path).await?;
515
516 stream.write_all(&[0x01]).await?;
518
519 let elapsed = start.elapsed().as_secs_f64();
520 tracing::info!(
521 bytes = file_size,
522 elapsed_ms = (elapsed * 1000.0) as u64,
523 path = local_path,
524 "Download complete"
525 );
526
527 let _ = event_tx.send(FileTransferEvent::Completed {
529 token,
530 direction: TransferDirection::Receive,
531 file_name,
532 bytes_transferred: file_size,
533 sha256: actual_sha.clone(),
534 elapsed_secs: elapsed,
535 });
536
537 Ok(TransferResult {
538 bytes_transferred: file_size,
539 sha256: actual_sha,
540 elapsed_secs: elapsed,
541 })
542}