Files
SoulStream/crates/soulseek-client/src/upload.rs
T
KeelrLeek a667b4b28e Baseline: full tree snapshot after git repository loss
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.
2026-07-22 17:57:00 -05:00

335 lines
10 KiB
Rust

//! Upload servicing: when a peer requests one of our shared files, we send
//! it over an F connection. Slot-limited with a simple FIFO queue.
//!
//! Two protocol sub-flows, mirroring the download side (M2):
//!
//! * **Immediate** — peer sends `TransferRequest(download, their_token)`.
//! With a free slot we reply `TransferResponse(their_token, allowed)`,
//! then await *their* inbound F connection (keyed by their token), read
//! the 8-byte offset, and stream the file.
//! * **Queued / proactive** — no slot yet (or a bare `QueueDownload`). We
//! enqueue; when a slot frees we send `TransferRequest(upload, our_token)`,
//! await their `TransferResponse`, open the F connection ourselves, read
//! the offset, and stream.
use std::path::PathBuf;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::oneshot;
use soulseek_proto::peer;
use crate::{conn, ClientError, Event, Inner, Result};
const ACK_TIMEOUT: Duration = Duration::from_secs(30);
const STALL_TIMEOUT: Duration = Duration::from_secs(60);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UploadState {
Queued,
Transferring,
Complete,
Failed { reason: String },
}
#[derive(Debug, Clone)]
pub struct UploadUpdate {
pub id: u64,
pub username: String,
pub filename: String,
pub state: UploadState,
pub sent: u64,
pub total: u64,
}
/// Peer sent `TransferRequest(download, their_token, filename)`.
pub(crate) fn on_transfer_request(
inner: Arc<Inner>,
username: String,
their_token: u32,
remote_path: String,
) {
let Some((local, size)) = inner.shares.resolve(&remote_path) else {
reply(
&inner,
&username,
peer::transfer_response_rejected(their_token, "File not shared"),
);
return;
};
if inner.has_free_upload_slot() {
// Accept: they will open the F connection with their token.
reply(
&inner,
&username,
peer::transfer_response_allowed(their_token, size),
);
spawn_immediate(inner, username, remote_path, local, size, their_token);
} else {
reply(
&inner,
&username,
peer::transfer_response_rejected(their_token, "Queued"),
);
enqueue(&inner, &username, &remote_path);
}
}
/// Peer sent a bare `QueueDownload(filename)` (no token) — enqueue, or start
/// the proactive flow immediately if a slot is free.
pub(crate) fn on_queue_download(inner: Arc<Inner>, username: String, remote_path: String) {
let Some((local, size)) = inner.shares.resolve(&remote_path) else {
reply(
&inner,
&username,
peer::upload_denied(&remote_path, "File not shared"),
);
return;
};
if inner.has_free_upload_slot() {
spawn_proactive(inner, username, remote_path, local, size);
} else {
enqueue(&inner, &username, &remote_path);
}
}
/// Report queue place when a peer asks (PlaceInQueueRequest).
pub(crate) fn on_place_query(inner: &Arc<Inner>, username: &str, remote_path: &str) {
let place = inner
.upload_queue
.lock()
.unwrap()
.iter()
.position(|(u, p)| u == username && p == remote_path)
.map(|i| i as u32 + 1)
.unwrap_or(0);
reply(
inner,
username,
peer::place_in_queue_response(remote_path, place),
);
}
fn enqueue(inner: &Arc<Inner>, username: &str, remote_path: &str) {
let mut q = inner.upload_queue.lock().unwrap();
if !q.iter().any(|(u, p)| u == username && p == remote_path) && q.len() < 500 {
q.push_back((username.to_string(), remote_path.to_string()));
}
let place = q.len() as u32;
drop(q);
reply(
inner,
username,
peer::place_in_queue_response(remote_path, place),
);
}
fn reply(inner: &Arc<Inner>, username: &str, frame: Vec<u8>) {
if let Some(h) = inner.peers.lock().unwrap().get(username) {
let _ = h.write_tx.send(frame);
}
}
/// After a slot frees, start the next queued upload (proactive flow).
fn pump_queue(inner: Arc<Inner>) {
if !inner.has_free_upload_slot() {
return;
}
let next = inner.upload_queue.lock().unwrap().pop_front();
if let Some((username, remote_path)) = next {
if let Some((local, size)) = inner.shares.resolve(&remote_path) {
spawn_proactive(inner, username, remote_path, local, size);
}
}
}
// -------- immediate flow (peer opens the F connection) --------
fn spawn_immediate(
inner: Arc<Inner>,
username: String,
remote_path: String,
local: PathBuf,
size: u64,
their_token: u32,
) {
inner.uploads_active.fetch_add(1, Ordering::Relaxed);
let id = inner.upload_counter.fetch_add(1, Ordering::Relaxed);
// Register to receive their inbound F connection (keyed by their token).
let (conn_tx, conn_rx) = oneshot::channel();
inner
.expected_transfers
.lock()
.unwrap()
.insert((username.clone(), their_token), conn_tx);
tokio::spawn(async move {
let user_err = username.clone();
run_upload(
&inner,
id,
&username,
&remote_path,
&local,
size,
async move {
let stream = tokio::time::timeout(ACK_TIMEOUT, conn_rx)
.await
.map_err(|_| ClientError::Timeout)?
.map_err(|_| ClientError::PeerUnreachable(user_err))?;
Ok::<TcpStream, ClientError>(stream)
},
)
.await;
inner
.expected_transfers
.lock()
.unwrap()
.remove(&(username.clone(), their_token));
inner.uploads_active.fetch_sub(1, Ordering::Relaxed);
pump_queue(inner.clone());
});
}
// -------- proactive flow (we open the F connection) --------
fn spawn_proactive(
inner: Arc<Inner>,
username: String,
remote_path: String,
local: PathBuf,
size: u64,
) {
inner.uploads_active.fetch_add(1, Ordering::Relaxed);
let id = inner.upload_counter.fetch_add(1, Ordering::Relaxed);
tokio::spawn(async move {
let inner2 = inner.clone();
let user2 = username.clone();
let path2 = remote_path.clone();
run_upload(
&inner,
id,
&username,
&remote_path,
&local,
size,
async move {
// Offer the upload and wait for acceptance.
let handle = conn::get_or_connect_peer(&inner2, &user2).await?;
let token = inner2.next_token();
let (ack_tx, ack_rx) = oneshot::channel();
inner2
.transfer_waits
.lock()
.unwrap()
.insert((user2.clone(), token), ack_tx);
handle
.write_tx
.send(peer::transfer_request_upload(token, &path2, size))
.map_err(|_| ClientError::PeerUnreachable(user2.clone()))?;
let ack = tokio::time::timeout(ACK_TIMEOUT, ack_rx)
.await
.map_err(|_| ClientError::Timeout)?
.map_err(|_| ClientError::PeerUnreachable(user2.clone()))?;
if !ack.allowed {
return Err(ClientError::TransferRejected(ack.message));
}
conn::connect_transfer(&inner2, &user2, token).await
},
)
.await;
inner.uploads_active.fetch_sub(1, Ordering::Relaxed);
pump_queue(inner.clone());
});
}
/// Shared upload driver: obtain the F stream via `get_stream`, read the
/// 8-byte start offset, and stream the file from there.
async fn run_upload<F>(
inner: &Arc<Inner>,
id: u64,
username: &str,
remote_path: &str,
local: &PathBuf,
size: u64,
get_stream: F,
) where
F: std::future::Future<Output = Result<TcpStream>>,
{
let emit = |state: UploadState, sent: u64| {
let _ = inner.event_tx.send(Event::Upload(UploadUpdate {
id,
username: username.to_string(),
filename: remote_path.to_string(),
state,
sent,
total: size,
}));
};
emit(UploadState::Transferring, 0);
let result = async {
let mut stream = get_stream.await?;
let mut off = [0u8; 8];
tokio::time::timeout(ACK_TIMEOUT, stream.read_exact(&mut off))
.await
.map_err(|_| ClientError::Timeout)??;
let offset = u64::from_le_bytes(off).min(size);
let mut file = tokio::fs::File::open(local).await?;
file.seek(std::io::SeekFrom::Start(offset)).await?;
let mut sent = offset;
let mut buf = vec![0u8; 64 * 1024];
let mut window = Instant::now();
let mut window_bytes: u64 = 0;
let mut last_emit = Instant::now();
while sent < size {
let n = file.read(&mut buf).await?;
if n == 0 {
break;
}
tokio::time::timeout(STALL_TIMEOUT, stream.write_all(&buf[..n]))
.await
.map_err(|_| ClientError::Timeout)??;
sent += n as u64;
window_bytes += n as u64;
let elapsed = window.elapsed();
if elapsed >= Duration::from_secs(2) {
inner.upload_speed.store(
(window_bytes as f64 / elapsed.as_secs_f64()) as u32,
Ordering::Relaxed,
);
window = Instant::now();
window_bytes = 0;
}
if last_emit.elapsed() >= Duration::from_millis(300) {
last_emit = Instant::now();
emit(UploadState::Transferring, sent);
}
}
stream.flush().await?;
Ok::<(), ClientError>(())
}
.await;
match result {
Ok(()) => emit(UploadState::Complete, size),
Err(e) => emit(
UploadState::Failed {
reason: e.to_string(),
},
0,
),
}
}