The previous .git directory was found gutted (only info/ remained), so history is unrecoverable locally. This commit restores version control with the tree exactly as it stands: the working app, the preview-tracks launcher on the artist hero, the scrollable-track-previews design bundle (docs + approved mockups), and the Phase 0 validation artifacts.
130 lines
4.9 KiB
Rust
130 lines
4.9 KiB
Rust
//! 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<PathBuf> {
|
|
let root = PathBuf::from(std::env::var("USERPROFILE").ok()?)
|
|
.join("Music")
|
|
.join("SoulSeek+");
|
|
fn walk(dir: &std::path::Path) -> Option<PathBuf> {
|
|
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)
|
|
}
|