//! Two-instance upload test: instance A shares a known file; instance B //! downloads it from A. Verifies the upload responder + F-connection path. //! //! cargo run -p soulseek-client --example upload_test use soulseek_client::{Client, Config, DownloadState, Event}; use std::path::PathBuf; #[tokio::main] async fn main() { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); // Build a share source with one real audio file. let src = std::env::temp_dir().join("ssplus-upload-src"); let album = src.join("TestAlbum"); std::fs::create_dir_all(&album).unwrap(); let sample = find_sample().expect("need a downloaded audio file to share"); let shared_file = album.join("sample.flac"); std::fs::copy(&sample, &shared_file).unwrap(); let size = std::fs::metadata(&shared_file).unwrap().len(); let remote_path = "ssplus-upload-src\\TestAlbum\\sample.flac".to_string(); println!("A shares {remote_path} ({size} bytes)"); let stamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .subsec_nanos() % 100_000; let user_a = format!("ssplusA{stamp}"); let user_b = format!("ssplusB{stamp}"); // Instance A: sharer/uploader. let mut cfg_a = Config::default(); cfg_a.shared_folders = vec![src.clone()]; cfg_a.listen_port = 51300; let (client_a, mut events_a) = Client::connect(cfg_a).await.expect("A connect"); client_a.login(&user_a, "pw").await.expect("A login"); println!("A logged in as {user_a}"); // Spawn a task to log A's upload events. tokio::spawn(async move { while let Some(ev) = events_a.recv().await { if let Event::Upload(u) = ev { println!( " A upload → {}: {:?} {}/{}", u.username, u.state, u.sent, u.total ); } } }); // Instance B: downloader. Different listen port to coexist on one host. let mut cfg_b = Config::default(); cfg_b.listen_port = 52300; cfg_b.downloads_dir = std::env::temp_dir().join("ssplus-upload-dst"); let _ = std::fs::remove_dir_all(&cfg_b.downloads_dir); let (client_b, mut events_b) = Client::connect(cfg_b).await.expect("B connect"); client_b.login(&user_b, "pw").await.expect("B login"); println!("B logged in as {user_b}"); // Both instances share this host's public IP, so the server-provided // address would need NAT hairpinning. Seed loopback endpoints so the P // and F connections go over localhost instead. client_b.set_peer_address(&user_a, "127.0.0.1:51300".parse().unwrap()); client_a.set_peer_address(&user_b, "127.0.0.1:52300".parse().unwrap()); // Give the server a moment to register both listen ports. tokio::time::sleep(std::time::Duration::from_secs(3)).await; let id = client_b.download(&user_a, &remote_path, size); println!("B requested download #{id} from A…\n"); let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(90); loop { let ev = tokio::select! { ev = events_b.recv() => ev, _ = tokio::time::sleep_until(deadline) => { println!("TIMEOUT"); break; } }; match ev { Some(Event::Download(u)) => match &u.state { DownloadState::Complete { local_path } => { let got = std::fs::metadata(local_path).map(|m| m.len()).unwrap_or(0); println!("\nB COMPLETE: {local_path} ({got}/{size} bytes)"); println!( "RESULT: {}", if got == size { "UPLOAD VERIFIED ✓" } else { "size mismatch ✗" } ); break; } DownloadState::Failed { reason } => { println!("\nB FAILED: {reason}"); println!("RESULT: upload not verified (likely NAT hairpin on same host)"); break; } other => println!(" B: {:?} {}/{}", other, u.received, u.total), }, Some(_) => {} None => break, } } client_a.shutdown(); client_b.shutdown(); } /// Find any downloaded audio file to use as the shared sample. fn find_sample() -> Option { let root = PathBuf::from(std::env::var("USERPROFILE").ok()?) .join("Music") .join("SoulSeek+"); fn walk(dir: &std::path::Path) -> Option { for e in std::fs::read_dir(dir).ok()?.flatten() { let p = e.path(); if p.is_dir() { if let Some(f) = walk(&p) { return Some(f); } } else if p.extension().and_then(|x| x.to_str()) == Some("flac") { return Some(p); } } None } walk(&root) }