1use std::path::PathBuf;
4
5use nifty_runtime::DownloadField;
6
7use crate::id::DownloadId;
8
9const DOWNLOADING: &str = "Downloading…";
11const RETRYING: &str = "Retrying…";
13const PAUSED: &str = "Paused";
15const CANCELLED: &str = "Cancelled";
17const DONE_PERCENT: i64 = 100;
18
19#[derive(Debug, PartialEq, Eq)]
21pub enum Report {
22 Started(DownloadId),
25 Progress(Progress),
26 Paused(Progress),
28 Retrying,
30 Downloading,
32 Saved {
34 path: PathBuf,
35 size: u64,
36 },
37 Failed(String),
38 Cancelled,
40}
41
42#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
44pub struct Progress {
45 pub received: u64,
46 pub total: Option<u64>,
47 pub speed: u64,
48}
49
50#[derive(Clone, Debug, PartialEq, Eq)]
52pub enum Written {
53 Int(i64),
54 Text(String),
55}
56
57impl Report {
58 #[must_use]
61 pub fn writes(&self) -> Vec<(DownloadField, Written)> {
62 use DownloadField::{Id, Progress as Percent, Received, Speed, Status, Total};
63 let int = |field, value: u64| (field, Written::Int(saturating_int(value)));
64 match self {
65 Self::Started(id) => vec![
66 (Id, Written::Text(id.to_string())),
67 (Percent, Written::Int(0)),
68 (Status, Written::Text(DOWNLOADING.to_owned())),
69 int(Received, 0),
70 int(Total, 0),
71 int(Speed, 0),
72 ],
73 Self::Paused(progress) => {
74 let mut writes = Self::Progress(*progress).writes();
75 writes.push((Status, Written::Text(PAUSED.to_owned())));
76 writes
77 }
78 Self::Retrying => vec![(Status, Written::Text(RETRYING.to_owned())), int(Speed, 0)],
79 Self::Downloading => vec![(Status, Written::Text(DOWNLOADING.to_owned()))],
80 Self::Progress(progress) => {
81 let mut writes = vec![int(Received, progress.received), int(Speed, progress.speed)];
82 if let Some(total) = progress.total {
83 writes.push((Percent, Written::Int(percent(progress.received, total))));
84 writes.push(int(Total, total));
85 }
86 writes
87 }
88 Self::Saved { path, size } => vec![
89 (Percent, Written::Int(DONE_PERCENT)),
90 (
91 Status,
92 Written::Text(format!("Saved to {}", path.display())),
93 ),
94 int(Received, *size),
95 int(Total, *size),
96 int(Speed, 0),
97 ],
98 Self::Failed(reason) => vec![
99 (Status, Written::Text(format!("Failed: {reason}"))),
100 int(Speed, 0),
101 ],
102 Self::Cancelled => vec![(Status, Written::Text(CANCELLED.to_owned())), int(Speed, 0)],
103 }
104 }
105
106 pub(crate) fn is_final(&self) -> bool {
107 matches!(self, Self::Saved { .. } | Self::Failed(_) | Self::Cancelled)
108 }
109}
110
111fn saturating_int(bytes: u64) -> i64 {
113 i64::try_from(bytes).unwrap_or(i64::MAX)
114}
115
116fn percent(received: u64, total: u64) -> i64 {
118 if total == 0 {
119 return DONE_PERCENT;
120 }
121 let percent = u128::from(received.min(total)) * 100 / u128::from(total);
122 i64::try_from(percent).expect("a percentage of at most 100 fits an i64")
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn counts_whole_percentages() {
131 assert_eq!(percent(0, 200), 0);
132 assert_eq!(percent(199, 200), 99);
133 assert_eq!(percent(200, 200), 100);
134 assert_eq!(percent(300, 200), 100);
135 assert_eq!(percent(0, 0), 100);
136 assert_eq!(percent(u64::MAX - 1, u64::MAX), 99);
137 }
138
139 #[test]
140 fn progress_only_moves_the_percentage_once_the_size_is_known() {
141 let unknown = Report::Progress(Progress {
142 received: 10,
143 total: None,
144 speed: 5,
145 });
146 assert_eq!(
147 unknown.writes(),
148 [
149 (DownloadField::Received, Written::Int(10)),
150 (DownloadField::Speed, Written::Int(5)),
151 ]
152 );
153 let known = Report::Progress(Progress {
154 received: 10,
155 total: Some(40),
156 speed: 5,
157 });
158 assert!(
159 known
160 .writes()
161 .contains(&(DownloadField::Progress, Written::Int(25)))
162 );
163 assert!(
164 known
165 .writes()
166 .contains(&(DownloadField::Total, Written::Int(40)))
167 );
168 }
169
170 #[test]
171 fn saving_fills_in_the_size_and_stops_the_speed() {
172 let saved = Report::Saved {
173 path: PathBuf::from("/x/a.zip"),
174 size: 7,
175 };
176 let writes = saved.writes();
177 for expected in [
178 (DownloadField::Progress, Written::Int(100)),
179 (DownloadField::Received, Written::Int(7)),
180 (DownloadField::Total, Written::Int(7)),
181 (DownloadField::Speed, Written::Int(0)),
182 ] {
183 assert!(writes.contains(&expected), "{writes:?}");
184 }
185 }
186}