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.
198 lines
5.0 KiB
Rust
198 lines
5.0 KiB
Rust
use std::{collections::HashMap, sync::mpsc::Sender};
|
|
|
|
use crate::{error::Result, message::Message, utils::zlib::deflate};
|
|
|
|
#[derive(Debug, Clone)]
|
|
#[allow(dead_code)]
|
|
pub struct File {
|
|
pub username: String,
|
|
pub name: String,
|
|
pub size: u64,
|
|
pub attribs: HashMap<u32, u32>,
|
|
}
|
|
pub struct UploadFailed {
|
|
pub filename: String,
|
|
}
|
|
impl UploadFailed {
|
|
pub fn new_from_message(message: &mut Message) -> Self {
|
|
let filename = message.read_string();
|
|
|
|
Self { filename }
|
|
}
|
|
}
|
|
#[derive(Debug, Clone)]
|
|
#[allow(dead_code)]
|
|
pub struct SearchResult {
|
|
pub token: u32,
|
|
pub files: Vec<File>,
|
|
pub slots: u8,
|
|
pub speed: u32,
|
|
pub username: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Search {
|
|
pub token: u32,
|
|
pub results: Vec<SearchResult>,
|
|
}
|
|
|
|
impl SearchResult {
|
|
pub fn new_from_message(message: &mut Message) -> Result<Self> {
|
|
let pointer = message.get_pointer();
|
|
let size = message.get_size();
|
|
let data: Vec<u8> = message.get_slice(pointer, size);
|
|
let deflated = deflate(&data)?;
|
|
let mut message = Message::new_with_data(deflated);
|
|
|
|
let username = message.read_string();
|
|
let token = message.read_int32();
|
|
let n_files = message.read_int32();
|
|
let mut files: Vec<File> = Vec::new();
|
|
for _ in 0..n_files {
|
|
message.read_int8();
|
|
let name = message.read_string();
|
|
let size = message.read_int64();
|
|
message.read_string();
|
|
let n_attribs = message.read_int32();
|
|
let mut attribs: HashMap<u32, u32> = HashMap::new();
|
|
|
|
for _ in 0..n_attribs {
|
|
attribs.insert(message.read_int32(), message.read_int32());
|
|
}
|
|
files.push(File {
|
|
username: username.clone(),
|
|
name,
|
|
size,
|
|
attribs,
|
|
});
|
|
}
|
|
let slots = message.read_int8();
|
|
let speed = message.read_int32();
|
|
|
|
Ok(Self {
|
|
token,
|
|
files,
|
|
slots,
|
|
speed,
|
|
username,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
#[allow(dead_code)]
|
|
pub struct Transfer {
|
|
pub direction: u32,
|
|
pub token: u32,
|
|
pub filename: String,
|
|
pub size: u64,
|
|
}
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct DownloadMetadata {
|
|
pub bitrate: Option<u32>,
|
|
pub length_seconds: Option<u32>,
|
|
pub peer_upload_speed: Option<u32>,
|
|
pub peer_free_slots: Option<u8>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Download {
|
|
pub username: String,
|
|
pub filename: String,
|
|
pub token: u32,
|
|
pub size: u64,
|
|
pub download_directory: String,
|
|
pub status: DownloadStatus,
|
|
pub sender: Sender<DownloadStatus>,
|
|
pub queue_position: Option<u32>,
|
|
pub metadata: DownloadMetadata,
|
|
}
|
|
|
|
impl Download {
|
|
#[must_use]
|
|
pub const fn is_finished(&self) -> bool {
|
|
matches!(
|
|
self.status,
|
|
DownloadStatus::Completed
|
|
| DownloadStatus::Failed(_)
|
|
| DownloadStatus::TimedOut
|
|
)
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn bytes_downloaded(&self) -> u64 {
|
|
match &self.status {
|
|
DownloadStatus::InProgress {
|
|
bytes_downloaded, ..
|
|
}
|
|
| DownloadStatus::Paused {
|
|
bytes_downloaded, ..
|
|
} => *bytes_downloaded,
|
|
DownloadStatus::Completed => self.size,
|
|
_ => 0,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn speed_bytes_per_sec(&self) -> f64 {
|
|
match &self.status {
|
|
DownloadStatus::InProgress {
|
|
speed_bytes_per_sec,
|
|
..
|
|
} => *speed_bytes_per_sec,
|
|
_ => 0.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
#[allow(dead_code)]
|
|
pub enum DownloadStatus {
|
|
Queued,
|
|
InProgress {
|
|
bytes_downloaded: u64,
|
|
total_bytes: u64,
|
|
speed_bytes_per_sec: f64,
|
|
},
|
|
Paused {
|
|
bytes_downloaded: u64,
|
|
total_bytes: u64,
|
|
},
|
|
Completed,
|
|
/// Failed, optionally with a human-readable reason.
|
|
Failed(Option<String>),
|
|
TimedOut,
|
|
}
|
|
impl Transfer {
|
|
pub fn new_from_message(message: &mut Message) -> Self {
|
|
let direction = message.read_int32();
|
|
let token = message.read_int32();
|
|
let filename = message.read_string();
|
|
let size = message.read_int64();
|
|
|
|
Self {
|
|
direction,
|
|
token,
|
|
filename,
|
|
size,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// A truncated TransferRequest from an untrusted peer must parse to defaults
|
|
// rather than panic (the read_* primitives are bounds-checked).
|
|
#[test]
|
|
fn transfer_new_from_truncated_message_does_not_panic() {
|
|
let mut message = Message::new_with_data(vec![1, 0, 0]);
|
|
let transfer = Transfer::new_from_message(&mut message);
|
|
assert_eq!(transfer.direction, 0);
|
|
assert_eq!(transfer.token, 0);
|
|
assert_eq!(transfer.filename, "");
|
|
assert_eq!(transfer.size, 0);
|
|
}
|
|
}
|