truffle_core/file_transfer/
mod.rs1pub mod receiver;
37pub mod sender;
38pub mod types;
39
40pub use types::*;
41
42use std::sync::atomic::{AtomicU64, Ordering};
43use std::sync::Arc;
44
45use tokio::sync::{broadcast, mpsc, Mutex};
46use tokio::task::JoinHandle;
47use tracing::info;
48
49use crate::network::NetworkProvider;
50use crate::node::Node;
51
52const DEFAULT_MAX_TRANSFER_SIZE: u64 = 1_073_741_824;
54
55pub(crate) struct FileTransferState {
62 pub(crate) event_tx: broadcast::Sender<FileTransferEvent>,
64 pub(crate) offer_tx: Mutex<Option<mpsc::UnboundedSender<(FileOffer, OfferResponder)>>>,
66 pub(crate) max_transfer_size: AtomicU64,
68 pub(crate) receiver_handle: Mutex<Option<JoinHandle<()>>>,
70}
71
72impl FileTransferState {
73 pub(crate) fn new() -> Self {
75 let (event_tx, _) = broadcast::channel(256);
76 Self {
77 event_tx,
78 offer_tx: Mutex::new(None),
79 max_transfer_size: AtomicU64::new(DEFAULT_MAX_TRANSFER_SIZE),
80 receiver_handle: Mutex::new(None),
81 }
82 }
83}
84
85pub struct FileTransfer<'a, N: NetworkProvider + 'static> {
92 node: &'a Node<N>,
94}
95
96impl<'a, N: NetworkProvider + 'static> FileTransfer<'a, N> {
97 pub(crate) fn new(node: &'a Node<N>) -> Self {
99 Self { node }
100 }
101
102 fn state(&self) -> &FileTransferState {
104 &self.node.file_transfer_state
105 }
106
107 pub fn subscribe(&self) -> broadcast::Receiver<FileTransferEvent> {
112 self.state().event_tx.subscribe()
113 }
114
115 pub fn set_max_transfer_size(&self, bytes: u64) {
119 self.state()
120 .max_transfer_size
121 .store(bytes, Ordering::Relaxed);
122 }
123
124 pub fn max_transfer_size(&self) -> u64 {
126 self.state().max_transfer_size.load(Ordering::Relaxed)
127 }
128
129 pub async fn offer_channel(
142 &self,
143 node: Arc<Node<N>>,
144 ) -> mpsc::UnboundedReceiver<(FileOffer, OfferResponder)> {
145 let (tx, rx) = mpsc::unbounded_channel();
146 {
147 let mut offer_tx = self.state().offer_tx.lock().await;
148 *offer_tx = Some(tx.clone());
149 }
150 self.ensure_receiver_started(node, tx).await;
151 rx
152 }
153
154 pub async fn auto_accept(&self, node: Arc<Node<N>>, output_dir: &str) {
161 let mut rx = self.offer_channel(node).await;
162 let output_dir = format!("{}/", output_dir.trim_end_matches(['/', '\\']));
169
170 tokio::spawn(async move {
171 while let Some((offer, responder)) = rx.recv().await {
172 info!(
173 file = offer.file_name.as_str(),
174 dir = output_dir.as_str(),
175 "Auto-accepting file offer"
176 );
177 responder.accept(&output_dir);
178 }
179 });
180 }
181
182 pub async fn auto_reject(&self, node: Arc<Node<N>>) {
187 let mut rx = self.offer_channel(node).await;
188
189 tokio::spawn(async move {
190 while let Some((_offer, responder)) = rx.recv().await {
191 responder.reject("auto-rejected");
192 }
193 });
194 }
195
196 pub async fn send_file(
206 &self,
207 peer_id: &str,
208 local_path: &str,
209 remote_path: &str,
210 ) -> Result<TransferResult, TransferError> {
211 let max_size = self.state().max_transfer_size.load(Ordering::Relaxed);
212 sender::send_file(
213 self.node,
214 peer_id,
215 local_path,
216 remote_path,
217 max_size,
218 &self.state().event_tx,
219 )
220 .await
221 }
222
223 pub async fn pull_file(
233 &self,
234 peer_id: &str,
235 remote_path: &str,
236 local_path: &str,
237 ) -> Result<TransferResult, TransferError> {
238 pull_file(
239 self.node,
240 peer_id,
241 remote_path,
242 local_path,
243 &self.state().event_tx,
244 )
245 .await
246 }
247
248 async fn ensure_receiver_started(
253 &self,
254 node: Arc<Node<N>>,
255 offer_tx: mpsc::UnboundedSender<(FileOffer, OfferResponder)>,
256 ) {
257 let mut handle = self.state().receiver_handle.lock().await;
258 if let Some(h) = handle.take() {
260 h.abort();
261 }
262 let h = receiver::spawn_receive_handler(node, offer_tx, self.state().event_tx.clone());
263 *handle = Some(h);
264 }
265}
266
267async fn pull_file<N: NetworkProvider + 'static>(
281 node: &Node<N>,
282 peer_id: &str,
283 remote_path: &str,
284 local_path: &str,
285 event_tx: &broadcast::Sender<FileTransferEvent>,
286) -> Result<TransferResult, TransferError> {
287 use sha2::{Digest, Sha256};
288 use std::time::Instant;
289 use tokio::io::{AsyncReadExt, AsyncWriteExt};
290
291 let start = Instant::now();
292 let token = uuid::Uuid::new_v4().to_string();
293 let requester_id = node.local_info().device_id;
297
298 let peer_id = node
300 .resolve_peer_id(peer_id)
301 .await
302 .map_err(|e| TransferError::Node(e.to_string()))?;
303 let peer_id = peer_id.as_str();
304
305 let pull_req = FtMessage::PullRequest {
307 path: remote_path.to_string(),
308 requester_id,
309 token: token.clone(),
310 };
311
312 let pull_payload = serde_json::to_value(&pull_req)
313 .map_err(|e| TransferError::Protocol(format!("Failed to serialize pull request: {e}")))?;
314
315 tracing::info!(peer = peer_id, path = remote_path, "Sending PULL_REQUEST");
316
317 let (offer_sha256, offer_size, offer_token, file_name) = crate::request_reply::send_and_wait(
318 node,
319 peer_id,
320 "ft",
321 "pull_request",
322 &pull_payload,
323 std::time::Duration::from_secs(30),
324 |msg| {
325 if msg.from != peer_id {
326 return None;
327 }
328 let ft_msg: FtMessage = serde_json::from_value(msg.payload.clone()).ok()?;
329 match ft_msg {
330 FtMessage::Offer {
331 file_name,
332 sha256,
333 size,
334 token: offer_token,
335 ..
336 } => {
337 tracing::info!(size = size, "Received OFFER from peer");
338 Some(Ok((sha256, size, offer_token, file_name)))
339 }
340 FtMessage::Reject { reason, .. } => Some(Err(TransferError::Rejected(reason))),
341 _ => None,
342 }
343 },
344 )
345 .await
346 .map_err(|e| match e {
347 crate::request_reply::RequestError::Timeout => TransferError::Timeout,
348 crate::request_reply::RequestError::Send(e) => TransferError::Node(e.to_string()),
349 crate::request_reply::RequestError::ChannelClosed => {
350 TransferError::Protocol("Channel closed".into())
351 }
352 })?
353 .map_err(|e| e)?;
354
355 let max_size = node
357 .file_transfer_state
358 .max_transfer_size
359 .load(Ordering::Relaxed);
360 if offer_size > max_size {
361 return Err(TransferError::Protocol(format!(
362 "Offered file size {offer_size} exceeds max transfer size {max_size}"
363 )));
364 }
365
366 let mut listener = node
368 .listen_tcp(0)
369 .await
370 .map_err(|e| TransferError::Node(format!("Failed to listen TCP: {e}")))?;
371
372 let accept = FtMessage::Accept {
373 token: offer_token,
374 tcp_port: listener.port,
375 };
376
377 let accept_payload = serde_json::to_value(&accept)
378 .map_err(|e| TransferError::Protocol(format!("Failed to serialize accept: {e}")))?;
379
380 node.send_typed(peer_id, "ft", "accept", &accept_payload)
381 .await
382 .map_err(|e| TransferError::Node(e.to_string()))?;
383
384 tracing::info!(
385 port = listener.port,
386 "Sent ACCEPT, listening for TCP connection"
387 );
388
389 let incoming = tokio::time::timeout(tokio::time::Duration::from_secs(30), listener.accept())
391 .await
392 .map_err(|_| TransferError::Timeout)?
393 .ok_or_else(|| TransferError::Protocol("Listener closed before accepting".to_string()))?;
394
395 let mut stream = incoming.stream;
396 tracing::info!("TCP connection accepted from peer");
397
398 let mut size_buf = [0u8; 8];
400 stream.read_exact(&mut size_buf).await?;
401 let file_size = u64::from_be_bytes(size_buf);
402
403 let mut sha_buf = [0u8; 64]; stream.read_exact(&mut sha_buf).await?;
405 let received_sha = String::from_utf8_lossy(&sha_buf).to_string();
406
407 if received_sha != offer_sha256 {
409 return Err(TransferError::IntegrityError {
410 expected: offer_sha256,
411 actual: received_sha,
412 });
413 }
414
415 if file_size != offer_size {
416 return Err(TransferError::Protocol(format!(
417 "Size mismatch: offer said {offer_size}, stream header says {file_size}"
418 )));
419 }
420
421 if let Some(parent) = std::path::Path::new(local_path).parent() {
424 tokio::fs::create_dir_all(parent).await?;
425 }
426
427 let temp_path = format!("{local_path}.truffle-tmp");
428 let mut temp_file = tokio::fs::File::create(&temp_path).await?;
429 let mut hasher = Sha256::new();
430 let mut bytes_received: u64 = 0;
431 let progress_start = Instant::now();
432 let mut buf = vec![0u8; 64 * 1024];
433
434 while bytes_received < file_size {
435 let to_read = ((file_size - bytes_received) as usize).min(buf.len());
436 let n = stream.read(&mut buf[..to_read]).await?;
437 if n == 0 {
438 tokio::fs::remove_file(&temp_path).await.ok();
439 return Err(TransferError::Io(std::io::Error::new(
440 std::io::ErrorKind::UnexpectedEof,
441 format!("Connection closed after {bytes_received}/{file_size} bytes"),
442 )));
443 }
444 hasher.update(&buf[..n]);
445 AsyncWriteExt::write_all(&mut temp_file, &buf[..n]).await?;
446 bytes_received += n as u64;
447
448 let elapsed = progress_start.elapsed().as_secs_f64();
449 let speed = if elapsed > 0.0 {
450 bytes_received as f64 / elapsed
451 } else {
452 0.0
453 };
454
455 let _ = event_tx.send(FileTransferEvent::Progress(TransferProgress {
457 token: token.clone(),
458 direction: TransferDirection::Receive,
459 file_name: file_name.clone(),
460 bytes_transferred: bytes_received,
461 total_bytes: file_size,
462 speed_bps: speed,
463 }));
464 }
465
466 AsyncWriteExt::flush(&mut temp_file).await?;
468
469 let actual_sha = hex::encode(hasher.finalize());
471
472 if actual_sha != offer_sha256 {
473 stream.write_all(&[0x00]).await?;
475 tokio::fs::remove_file(&temp_path).await.ok();
476 return Err(TransferError::IntegrityError {
477 expected: offer_sha256,
478 actual: actual_sha,
479 });
480 }
481
482 tokio::fs::rename(&temp_path, local_path).await?;
484
485 stream.write_all(&[0x01]).await?;
487
488 let elapsed = start.elapsed().as_secs_f64();
489 tracing::info!(
490 bytes = file_size,
491 elapsed_ms = (elapsed * 1000.0) as u64,
492 path = local_path,
493 "Download complete"
494 );
495
496 let _ = event_tx.send(FileTransferEvent::Completed {
498 token,
499 direction: TransferDirection::Receive,
500 file_name,
501 bytes_transferred: file_size,
502 sha256: actual_sha.clone(),
503 elapsed_secs: elapsed,
504 });
505
506 Ok(TransferResult {
507 bytes_transferred: file_size,
508 sha256: actual_sha,
509 elapsed_secs: elapsed,
510 })
511}