feat(tui): surface the failure reason on failed downloads

Carry a human-readable reason on DownloadStatus::Failed so it flows
through the existing status channel to the TUI. The download Info pane
now shows an Error line for a failed download (connect failure, peer
disconnect, truncated transfer, peer-reported upload failure, or the
underlying transfer error). Failures without a known reason render as
before.
This commit is contained in:
michel
2026-07-19 19:36:15 +02:00
parent 4809dd7059
commit ae28f4236a
9 changed files with 114 additions and 34 deletions
+32 -14
View File
@@ -374,7 +374,7 @@ fn download_without_a_connection_resolves_failed() {
.expect("download() should return a handle");
assert!(matches!(
receiver.recv_timeout(Duration::from_secs(1)),
Ok(DownloadStatus::Failed)
Ok(DownloadStatus::Failed(_))
));
}
@@ -398,7 +398,7 @@ fn fail_queued_downloads_notifies_receiver_and_store() {
Client::fail_queued_downloads(&client.context, "peer");
assert!(matches!(receiver.try_recv(), Ok(DownloadStatus::Failed)));
assert!(matches!(receiver.try_recv(), Ok(DownloadStatus::Failed(_))));
assert!(matches!(
client
.context
@@ -407,7 +407,7 @@ fn fail_queued_downloads_notifies_receiver_and_store() {
.get_download_by_token(7)
.unwrap()
.status,
DownloadStatus::Failed
DownloadStatus::Failed(_)
));
}
@@ -1056,10 +1056,18 @@ impl Client {
};
if failed {
let _ = download.sender.send(DownloadStatus::Failed);
self.context
.write_safe()?
.update_download_with_status(token, DownloadStatus::Failed);
let reason = if peer_registered {
"Peer rejected the download request"
} else {
"Could not connect to the peer"
};
let _ = download
.sender
.send(DownloadStatus::Failed(Some(reason.to_string())));
self.context.write_safe()?.update_download_with_status(
token,
DownloadStatus::Failed(Some(reason.to_string())),
);
}
Ok((download, download_receiver))
@@ -1088,8 +1096,12 @@ impl Client {
.map(|d| (d.token, d.sender.clone()))
.collect();
for (token, sender) in doomed {
let _ = sender.send(DownloadStatus::Failed);
context.update_download_with_status(token, DownloadStatus::Failed);
let reason = Some("Peer disconnected".to_string());
let _ = sender.send(DownloadStatus::Failed(reason.clone()));
context.update_download_with_status(
token,
DownloadStatus::Failed(reason),
);
}
}
@@ -1142,9 +1154,12 @@ impl Client {
match client_context.write_safe() {
Ok(mut context) => {
for token in failed_tokens {
context
.downloads
.update_status(token, DownloadStatus::Failed);
context.downloads.update_status(
token,
DownloadStatus::Failed(Some(
"Peer reported the upload failed".to_string(),
)),
);
context.downloads.remove(token);
}
}
@@ -1326,9 +1341,12 @@ impl Client {
);
}
Err(e) => {
let _ = download.sender.send(DownloadStatus::Failed);
let reason = Some(
e.to_string(),
);
let _ = download.sender.send(DownloadStatus::Failed(reason.clone()));
match client_context_clone.write_safe() {
Ok(mut ctx) => ctx.update_download_with_status(download.token, DownloadStatus::Failed),
Ok(mut ctx) => ctx.update_download_with_status(download.token, DownloadStatus::Failed(reason)),
Err(e) => error!("[client] download failed write: {}", e),
}
error!(
+4 -2
View File
@@ -149,7 +149,9 @@ pub fn collect_failed_tokens(
d.username == username && filename.is_none_or(|f| d.filename == *f)
})
.map(|d| {
let _ = d.sender.send(DownloadStatus::Failed);
let _ = d.sender.send(DownloadStatus::Failed(Some(
"Peer reported the upload failed".to_string(),
)));
d.token
})
.collect()
@@ -301,7 +303,7 @@ mod tests {
assert_eq!(tokens, vec![1]);
assert!(matches!(
rx_match.try_recv().unwrap(),
DownloadStatus::Failed
DownloadStatus::Failed(_)
));
}
}
+1 -1
View File
@@ -212,7 +212,7 @@ fn handle_file_connection(
match context.client_context.write_safe() {
Ok(mut ctx) => ctx.update_download_with_status(
failure_token,
DownloadStatus::Failed,
DownloadStatus::Failed(Some(e.to_string())),
),
Err(e) => {
error!(
+3 -2
View File
@@ -114,7 +114,7 @@ impl Download {
matches!(
self.status,
DownloadStatus::Completed
| DownloadStatus::Failed
| DownloadStatus::Failed(_)
| DownloadStatus::TimedOut
)
}
@@ -159,7 +159,8 @@ pub enum DownloadStatus {
total_bytes: u64,
},
Completed,
Failed,
/// Failed, optionally with a human-readable reason.
Failed(Option<String>),
TimedOut,
}
impl Transfer {
+4 -4
View File
@@ -614,7 +614,7 @@ fn a_file_downloads_from_a_peer_over_p_and_f_connections() {
completed = true;
break;
}
Ok(DownloadStatus::Failed | DownloadStatus::TimedOut) => {
Ok(DownloadStatus::Failed(_) | DownloadStatus::TimedOut) => {
break;
}
_ => {}
@@ -862,7 +862,7 @@ fn a_file_downloads_from_a_peer_via_direct_connection() {
completed = true;
break;
}
Ok(DownloadStatus::Failed | DownloadStatus::TimedOut) => break,
Ok(DownloadStatus::Failed(_) | DownloadStatus::TimedOut) => break,
_ => {}
}
if client
@@ -1021,7 +1021,7 @@ fn a_file_downloads_from_a_firewalled_peer_via_server_broker() {
completed = true;
break;
}
Ok(DownloadStatus::Failed | DownloadStatus::TimedOut) => break,
Ok(DownloadStatus::Failed(_) | DownloadStatus::TimedOut) => break,
_ => {}
}
if client
@@ -1122,7 +1122,7 @@ fn two_real_clients_search_and_download() {
completed = true;
break;
}
Ok(DownloadStatus::Failed | DownloadStatus::TimedOut) => break,
Ok(DownloadStatus::Failed(_) | DownloadStatus::TimedOut) => break,
_ => {}
}
if leecher