1use std::fs::{self, File, OpenOptions};
19use std::io::{Seek, SeekFrom};
20use std::path::Path;
21use std::sync::Arc;
22use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender};
23use std::time::{Duration, Instant};
24
25use nifty_runtime::{DownloadCommand, expand_home};
26
27use ureq::Agent;
28
29use crate::connection::{self, ConnectionError, ConnectionId, Dial, Outlet};
30use crate::http::{self, Answer, Headers};
31use crate::name::{Partial, file_name, safe_file_name};
32use crate::part::{self, Live, Part};
33use crate::report::{Progress, Report};
34use crate::retry;
35use crate::speed::SpeedMeter;
36
37mod stopped;
38
39use stopped::{DownloadError, Stopped};
40
41const REPORT_INTERVAL: Duration = Duration::from_millis(100);
44
45#[derive(Debug)]
47pub(crate) enum Event {
48 Command(DownloadCommand),
50 Abandoned,
52 Answered(ConnectionId, Answer),
54 Wrote(ConnectionId, u64),
56 Ended(ConnectionId, Result<(), ConnectionError>),
58}
59
60#[derive(Debug)]
62pub(crate) struct Job {
63 pub url: String,
64 pub folder: String,
65 pub name: Option<String>,
67 pub connections: usize,
68 pub headers: Vec<String>,
70 pub retries: u32,
72}
73
74#[derive(Debug)]
76pub(crate) struct Abandoned;
77
78pub(crate) fn run(
84 job: Job,
85 events: &Receiver<Event>,
86 connect: Sender<Event>,
87 report: &impl Fn(Report) -> Result<(), Abandoned>,
88) -> Result<(), Abandoned> {
89 let headers = match http::headers(&job.headers) {
90 Ok(headers) => headers,
91 Err(error) => return report(Report::Failed(DownloadError::Header(error).to_string())),
92 };
93 let mut download = Download {
94 job,
95 headers,
96 agent: http::agent(),
97 connect,
98 next_connection: 0,
99 parts: vec![Part::whole()],
100 partial: None,
101 total: None,
102 speed: 0,
103 paused: false,
104 meter: SpeedMeter::default(),
105 last_report: None,
106 };
107 download.dial(0);
108 let stopped = match download.follow(events, report) {
109 Ok(saved) => return report(saved),
110 Err(stopped) => download.clean_up(stopped),
111 };
112 match stopped {
113 Stopped::Abandoned => Err(Abandoned),
114 Stopped::Cancelled => report(Report::Cancelled),
115 Stopped::Failed(error) => report(Report::Failed(error.to_string())),
116 }
117}
118
119struct Download {
121 job: Job,
122 headers: Headers,
123 agent: Agent,
124 connect: Sender<Event>,
125 next_connection: ConnectionId,
127 parts: Vec<Part>,
129 partial: Option<Partial>,
131 total: Option<u64>,
133 speed: u64,
134 paused: bool,
135 meter: SpeedMeter,
136 last_report: Option<Instant>,
137}
138
139impl Download {
140 fn follow(
143 &mut self,
144 events: &Receiver<Event>,
145 report: &impl Fn(Report) -> Result<(), Abandoned>,
146 ) -> Result<Report, Stopped> {
147 loop {
148 match self.next_event(events, report)? {
149 Event::Command(DownloadCommand::Cancel) => return Err(Stopped::Cancelled),
150 Event::Command(DownloadCommand::Pause) => self.pause(report)?,
151 Event::Command(DownloadCommand::Resume) => {
152 if let Some(saved) = self.resume(report)? {
153 return Ok(saved);
154 }
155 }
156 Event::Abandoned => return Err(Stopped::Abandoned),
157 Event::Answered(id, answer) => {
160 if let Some(index) = self.part_of(id) {
161 self.answered(index, &answer)?;
162 }
163 }
164 Event::Wrote(id, bytes) => {
165 if let Some(index) = self.part_of(id) {
166 self.wrote(index, bytes, report)?;
167 }
168 }
169 Event::Ended(id, ended) => {
170 if let Some(index) = self.part_of(id)
171 && let Some(saved) = self.ended(index, ended, report)?
172 {
173 return Ok(saved);
174 }
175 }
176 }
177 }
178 }
179
180 fn next_event(
182 &mut self,
183 events: &Receiver<Event>,
184 report: &impl Fn(Report) -> Result<(), Abandoned>,
185 ) -> Result<Event, Stopped> {
186 loop {
187 let next_retry = self.parts.iter().filter_map(|part| part.retry_at).min();
188 let Some(retry_at) = next_retry else {
190 return events.recv().map_err(|_| Stopped::Abandoned);
191 };
192 match events.recv_timeout(retry_at.saturating_duration_since(Instant::now())) {
193 Ok(event) => return Ok(event),
194 Err(RecvTimeoutError::Timeout) => self.retry_due(report)?,
195 Err(RecvTimeoutError::Disconnected) => return Err(Stopped::Abandoned),
196 }
197 }
198 }
199
200 fn retry_due(
203 &mut self,
204 report: &impl Fn(Report) -> Result<(), Abandoned>,
205 ) -> Result<(), Abandoned> {
206 let was_waiting = self.is_waiting();
207 let now = Instant::now();
208 for index in 0..self.parts.len() {
209 if self.parts[index].retry_at.is_some_and(|at| at <= now) {
210 self.parts[index].retry_at = None;
211 self.dial(index);
212 }
213 }
214 if was_waiting {
215 report(Report::Downloading)?;
216 }
217 Ok(())
218 }
219
220 fn is_waiting(&self) -> bool {
222 self.parts.iter().all(|part| part.connection.is_none())
223 }
224
225 fn part_of(&self, id: ConnectionId) -> Option<usize> {
227 self.parts.iter().position(|part| {
228 part.connection
229 .as_ref()
230 .is_some_and(|connection| connection.id == id)
231 })
232 }
233
234 fn dial(&mut self, index: usize) {
238 let id = self.next_connection;
239 self.next_connection += 1;
240 let rest = self.parts[index].rest();
241 let may_split = self.job.connections > 1 && self.partial.is_none();
242 let whole = rest.from == 0 && rest.to.is_none();
243 let outlet = Arc::new(Outlet::default());
244 self.parts[index].connection = Some(Live {
245 id,
246 outlet: Arc::clone(&outlet),
247 writes_from: None,
248 });
249 let dial = Dial {
250 id,
251 url: self.job.url.clone(),
252 range: (may_split || !whole).then_some(rest),
253 headers: Arc::clone(&self.headers),
254 };
255 connection::spawn(self.agent.clone(), dial, outlet, self.connect.clone());
256 }
257
258 fn progress(&self) -> Progress {
259 Progress {
260 received: self.parts.iter().map(|part| part.saved).sum(),
261 total: self.total,
262 speed: self.speed,
263 }
264 }
265
266 fn pause(
268 &mut self,
269 report: &impl Fn(Report) -> Result<(), Abandoned>,
270 ) -> Result<(), Abandoned> {
271 if self.paused {
272 return Ok(());
273 }
274 self.paused = true;
275 for part in &mut self.parts {
276 part.hang_up();
277 part.retry_at = None;
278 }
279 self.speed = 0;
280 report(Report::Paused(self.progress()))
281 }
282
283 fn resume(
286 &mut self,
287 report: &impl Fn(Report) -> Result<(), Abandoned>,
288 ) -> Result<Option<Report>, Stopped> {
289 if !self.paused {
290 return Ok(None);
291 }
292 self.paused = false;
293 report(Report::Downloading)?;
294 if self.partial.is_some() && self.is_complete() {
295 return self.save().map(Some);
296 }
297 self.meter = SpeedMeter::default();
298 self.last_report = None;
299 for index in 0..self.parts.len() {
300 self.parts[index].failures = 0;
301 if !self.parts[index].is_complete(self.total) {
302 self.dial(index);
303 }
304 }
305 Ok(None)
306 }
307
308 fn is_complete(&self) -> bool {
309 self.parts.iter().all(|part| part.is_complete(self.total))
310 }
311
312 fn answered(&mut self, index: usize, answer: &Answer) -> Result<(), Stopped> {
315 let created = match self.partial {
316 Some(_) => None,
317 None => Some(self.create(answer)?),
318 };
319 let asked = self.parts[index].position();
320 let alone = self.parts.len() == 1;
322 if alone && answer.starts_at > asked {
323 return Err(Stopped::Failed(DownloadError::PartTooFar {
324 starts_at: answer.starts_at,
325 saved: asked,
326 }));
327 }
328 if !alone && answer.starts_at != asked {
329 return Err(Stopped::Failed(DownloadError::PartMoved {
330 starts_at: answer.starts_at,
331 asked,
332 }));
333 }
334 let mut file = match created {
335 Some(file) => file,
336 None => OpenOptions::new().write(true).open(self.partial_path())?,
337 };
338 if alone {
339 self.total = answer.total;
340 file.set_len(answer.starts_at)?;
341 }
342 file.seek(SeekFrom::Start(answer.starts_at))?;
343 let part = &mut self.parts[index];
344 part.saved = answer.starts_at - part.start;
345 let room = part.room();
346 let live = part
347 .connection
348 .as_mut()
349 .expect("only a live connection's answer is handled");
350 live.writes_from = Some(answer.starts_at);
351 live.outlet.open(file, room);
352 Ok(())
353 }
354
355 fn create(&mut self, answer: &Answer) -> Result<File, Stopped> {
359 let folder = expand_home(&self.job.folder)?;
360 fs::create_dir_all(&folder)?;
361 let name = self
362 .job
363 .name
364 .as_deref()
365 .map_or_else(|| file_name(&answer.uri), safe_file_name);
366 let (partial, file) = Partial::create(&folder, &name)?;
367 self.partial = Some(partial);
368 if self.job.connections > 1
369 && answer.partial
370 && let Some(total) = answer.total
371 {
372 self.total = Some(total);
373 let mut parts = part::split(total, self.job.connections);
374 parts[0].connection = self.parts[0].connection.take();
375 self.parts = parts;
376 for index in 1..self.parts.len() {
377 self.dial(index);
378 }
379 }
380 Ok(file)
381 }
382
383 fn partial_path(&self) -> &Path {
384 self.partial
385 .as_ref()
386 .expect("a connection's file is opened after the first answer created it")
387 .path()
388 }
389
390 fn wrote(
391 &mut self,
392 index: usize,
393 bytes: u64,
394 report: &impl Fn(Report) -> Result<(), Abandoned>,
395 ) -> Result<(), Abandoned> {
396 let part = &mut self.parts[index];
397 part.saved += bytes;
398 part.failures = 0;
399 let now = Instant::now();
400 self.speed = self.meter.record(now, self.progress().received);
401 if self
402 .last_report
403 .is_none_or(|last| now.duration_since(last) >= REPORT_INTERVAL)
404 {
405 report(Report::Progress(self.progress()))?;
406 self.last_report = Some(now);
407 }
408 Ok(())
409 }
410
411 fn ended(
415 &mut self,
416 index: usize,
417 ended: Result<(), ConnectionError>,
418 report: &impl Fn(Report) -> Result<(), Abandoned>,
419 ) -> Result<Option<Report>, Stopped> {
420 if let Err(error) = ended {
421 self.retry_later(index, error, report)?;
422 return Ok(None);
423 }
424 let part = &mut self.parts[index];
425 if let Some(file) = part.hang_up() {
426 file.sync_all()?;
427 }
428 let saved = part.position();
429 match part.end.or(self.total) {
430 Some(end) if saved < end => {
431 return Err(Stopped::Failed(DownloadError::PartCutShort { saved, end }));
432 }
433 None => self.total = Some(saved),
435 Some(_) => {}
436 }
437 if self.is_complete() {
438 self.save().map(Some)
439 } else {
440 Ok(None)
441 }
442 }
443
444 fn retry_later(
450 &mut self,
451 index: usize,
452 error: ConnectionError,
453 report: &impl Fn(Report) -> Result<(), Abandoned>,
454 ) -> Result<(), Stopped> {
455 let part = &mut self.parts[index];
456 if !error.is_transient() || part.failures >= self.job.retries {
457 return Err(Stopped::Failed(DownloadError::Connection(error)));
458 }
459 part.hang_up();
460 part.failures += 1;
461 part.retry_at = Some(Instant::now() + retry::delay(part.failures));
462 if self.is_waiting() {
463 self.speed = 0;
464 report(Report::Retrying)?;
465 }
466 Ok(())
467 }
468
469 fn save(&mut self) -> Result<Report, Stopped> {
471 let path = self
472 .partial
473 .take()
474 .expect("a body only ends after its answer created the file")
475 .finish()?;
476 Ok(Report::Saved {
477 path,
478 size: self.progress().received,
479 })
480 }
481
482 fn clean_up(&mut self, stopped: Stopped) -> Stopped {
485 for part in &mut self.parts {
486 part.hang_up();
487 }
488 let Some(partial) = self.partial.take() else {
489 return stopped;
490 };
491 match fs::remove_file(partial.path()) {
492 Ok(()) => stopped,
493 Err(_) => Stopped::Failed(DownloadError::LeftBehind {
494 reason: Box::new(stopped),
495 path: partial.path().to_owned(),
496 }),
497 }
498 }
499}