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
335 .resolve_peer(peer_id)
336 .await
337 .map_err(|e| TransferError::Node(e.to_string()))?
338 .id;
339 let peer_id = peer_id.as_str();
340
341 let pull_req = FtMessage::PullRequest {
343 path: remote_path.to_string(),
344 requester_id,
345 token: token.clone(),
346 };
347
348 let pull_payload = serde_json::to_value(&pull_req)
349 .map_err(|e| TransferError::Protocol(format!("Failed to serialize pull request: {e}")))?;
350
351 tracing::info!(peer = peer_id, path = remote_path, "Sending PULL_REQUEST");
352
353 let (offer_sha256, offer_size, offer_token, file_name) = crate::request_reply::send_and_wait(
354 node,
355 peer_id,
356 "ft",
357 "pull_request",
358 &pull_payload,
359 std::time::Duration::from_secs(30),
360 |msg| {
361 if msg.from != peer_id {
362 return None;
363 }
364 let ft_msg: FtMessage = serde_json::from_value(msg.payload.clone()).ok()?;
365 match ft_msg {
366 FtMessage::Offer {
367 file_name,
368 sha256,
369 size,
370 token: offer_token,
371 ..
372 } => {
373 tracing::info!(size = size, "Received OFFER from peer");
374 Some(Ok((sha256, size, offer_token, file_name)))
375 }
376 FtMessage::Reject { reason, .. } => Some(Err(TransferError::Rejected(reason))),
377 _ => None,
378 }
379 },
380 )
381 .await
382 .map_err(|e| match e {
383 crate::request_reply::RequestError::Timeout => TransferError::Timeout,
384 crate::request_reply::RequestError::Send(e) => TransferError::Node(e.to_string()),
385 crate::request_reply::RequestError::ChannelClosed => {
386 TransferError::Protocol("Channel closed".into())
387 }
388 })??;
389
390 let max_size = node
392 .file_transfer_state
393 .max_transfer_size
394 .load(Ordering::Relaxed);
395 if offer_size > max_size {
396 return Err(TransferError::Protocol(format!(
397 "Offered file size {offer_size} exceeds max transfer size {max_size}"
398 )));
399 }
400
401 let mut listener = node
403 .listen_tcp(0)
404 .await
405 .map_err(|e| TransferError::Node(format!("Failed to listen TCP: {e}")))?;
406
407 let accept = FtMessage::Accept {
408 token: offer_token,
409 tcp_port: listener.port,
410 };
411
412 let accept_payload = serde_json::to_value(&accept)
413 .map_err(|e| TransferError::Protocol(format!("Failed to serialize accept: {e}")))?;
414
415 node.send_typed(peer_id, "ft", "accept", &accept_payload)
416 .await
417 .map_err(|e| TransferError::Node(e.to_string()))?;
418
419 tracing::info!(
420 port = listener.port,
421 "Sent ACCEPT, listening for TCP connection"
422 );
423
424 let incoming = tokio::time::timeout(tokio::time::Duration::from_secs(30), listener.accept())
426 .await
427 .map_err(|_| TransferError::Timeout)?
428 .ok_or_else(|| TransferError::Protocol("Listener closed before accepting".to_string()))?;
429
430 let mut stream = incoming.stream;
431 tracing::info!("TCP connection accepted from peer");
432
433 let mut size_buf = [0u8; 8];
435 stream.read_exact(&mut size_buf).await?;
436 let file_size = u64::from_be_bytes(size_buf);
437
438 let mut sha_buf = [0u8; 64]; stream.read_exact(&mut sha_buf).await?;
440 let received_sha = String::from_utf8_lossy(&sha_buf).to_string();
441
442 if received_sha != offer_sha256 {
444 return Err(TransferError::IntegrityError {
445 expected: offer_sha256,
446 actual: received_sha,
447 });
448 }
449
450 if file_size != offer_size {
451 return Err(TransferError::Protocol(format!(
452 "Size mismatch: offer said {offer_size}, stream header says {file_size}"
453 )));
454 }
455
456 if let Some(parent) = std::path::Path::new(local_path).parent() {
459 tokio::fs::create_dir_all(parent).await?;
460 }
461
462 let temp_path = format!("{local_path}.truffle-tmp");
463 let mut temp_file = tokio::fs::File::create(&temp_path).await?;
464 let mut hasher = Sha256::new();
465 let mut bytes_received: u64 = 0;
466 let progress_start = Instant::now();
467 let mut buf = vec![0u8; 64 * 1024];
468
469 while bytes_received < file_size {
470 let to_read = ((file_size - bytes_received) as usize).min(buf.len());
471 let n = stream.read(&mut buf[..to_read]).await?;
472 if n == 0 {
473 tokio::fs::remove_file(&temp_path).await.ok();
474 return Err(TransferError::Io(std::io::Error::new(
475 std::io::ErrorKind::UnexpectedEof,
476 format!("Connection closed after {bytes_received}/{file_size} bytes"),
477 )));
478 }
479 hasher.update(&buf[..n]);
480 AsyncWriteExt::write_all(&mut temp_file, &buf[..n]).await?;
481 bytes_received += n as u64;
482
483 let elapsed = progress_start.elapsed().as_secs_f64();
484 let speed = if elapsed > 0.0 {
485 bytes_received as f64 / elapsed
486 } else {
487 0.0
488 };
489
490 let _ = event_tx.send(FileTransferEvent::Progress(TransferProgress {
492 token: token.clone(),
493 direction: TransferDirection::Receive,
494 file_name: file_name.clone(),
495 bytes_transferred: bytes_received,
496 total_bytes: file_size,
497 speed_bps: speed,
498 }));
499 }
500
501 AsyncWriteExt::flush(&mut temp_file).await?;
503
504 let actual_sha = hex::encode(hasher.finalize());
506
507 if actual_sha != offer_sha256 {
508 stream.write_all(&[0x00]).await?;
510 tokio::fs::remove_file(&temp_path).await.ok();
511 return Err(TransferError::IntegrityError {
512 expected: offer_sha256,
513 actual: actual_sha,
514 });
515 }
516
517 tokio::fs::rename(&temp_path, local_path).await?;
519
520 stream.write_all(&[0x01]).await?;
522
523 let elapsed = start.elapsed().as_secs_f64();
524 tracing::info!(
525 bytes = file_size,
526 elapsed_ms = (elapsed * 1000.0) as u64,
527 path = local_path,
528 "Download complete"
529 );
530
531 let _ = event_tx.send(FileTransferEvent::Completed {
533 token,
534 direction: TransferDirection::Receive,
535 file_name,
536 bytes_transferred: file_size,
537 sha256: actual_sha.clone(),
538 elapsed_secs: elapsed,
539 });
540
541 Ok(TransferResult {
542 bytes_transferred: file_size,
543 sha256: actual_sha,
544 elapsed_secs: elapsed,
545 })
546}