Skip to main content

nifty_download/
worker.rs

1//! A download's own thread, which handles everything that happens to it, in order: its
2//! connections' progress, and what the app asks of it.
3//!
4//! A download asked to use several connections asks for the whole file as a range. A
5//! server that answers with part of it and the file's size gets the file split into
6//! parts, each fetched by its own connection into its own place in the partial file: the
7//! first connection keeps the first part, and new ones ask for the rest. Any other answer
8//! leaves one connection fetching the whole file.
9//!
10//! Pausing closes each connection's outlet and keeps the partial file; resuming opens a
11//! new connection for each part not yet saved, asking for the rest of it with an HTTP
12//! range request. A server that ignores the range sends the whole file again, so a
13//! download with one connection starts over from its first byte.
14//!
15//! A connection that fails in a way that may pass is tried again the same way, after a
16//! wait the thread spends listening for events, so the app can pause or cancel meanwhile.
17
18use 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
41/// How often progress is reported while data arrives, so a fast download doesn't flood
42/// the UI thread with updates nobody can see.
43const REPORT_INTERVAL: Duration = Duration::from_millis(100);
44
45/// Something that happened to a download.
46#[derive(Debug)]
47pub(crate) enum Event {
48    /// The app asked for this.
49    Command(DownloadCommand),
50    /// The app is gone, so nobody will hear from this download again.
51    Abandoned,
52    /// The server answered a connection, and the body is on its way.
53    Answered(ConnectionId, Answer),
54    /// A connection put this many more bytes in the file.
55    Wrote(ConnectionId, u64),
56    /// A connection reached the end of the body, or failed.
57    Ended(ConnectionId, Result<(), ConnectionError>),
58}
59
60/// What a download fetches, where it saves it, and how many connections it may open.
61#[derive(Debug)]
62pub(crate) struct Job {
63    pub url: String,
64    pub folder: String,
65    /// The file's name, made safe before use; `None` names it after the address.
66    pub name: Option<String>,
67    pub connections: usize,
68    /// Headers to send with each request, each written `Name: value`.
69    pub headers: Vec<String>,
70    /// How many times in a row a connection is tried again after a failure that may pass.
71    pub retries: u32,
72}
73
74/// The UI thread is gone, so nobody is waiting for this download any more.
75#[derive(Debug)]
76pub(crate) struct Abandoned;
77
78/// Runs `job`, handling `events` until it ends, and sending each step to `report`.
79/// `connect` is where its connections send their events.
80///
81/// # Errors
82/// [`Abandoned`] once the app is gone, which stops the download early.
83pub(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
119/// A download in progress, as its own thread sees it.
120struct Download {
121    job: Job,
122    headers: Headers,
123    agent: Agent,
124    connect: Sender<Event>,
125    /// The id the next connection gets.
126    next_connection: ConnectionId,
127    /// In the order they come in the file, which they cover between them.
128    parts: Vec<Part>,
129    /// The file being written, once a server has answered.
130    partial: Option<Partial>,
131    /// The whole file's length, when the server said.
132    total: Option<u64>,
133    speed: u64,
134    paused: bool,
135    meter: SpeedMeter,
136    last_report: Option<Instant>,
137}
138
139impl Download {
140    /// Handles events until the file is saved, giving the report that says so, or the
141    /// download stops.
142    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                // A connection stopped by a pause or a cancel may still be heard from, but
158                // it no longer fetches a part.
159                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    /// Waits for the next event, trying again each part whose wait to retry ends first.
181    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            // The app holds a sender until it lets go of the download.
189            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    /// Starts a connection again for each part whose wait to retry is over, saying the
201    /// download is under way again if nothing was.
202    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    /// Whether no connection is running, only parts waiting to retry.
221    fn is_waiting(&self) -> bool {
222        self.parts.iter().all(|part| part.connection.is_none())
223    }
224
225    /// Which part connection `id` is fetching, unless it has been stopped.
226    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    /// Starts a connection for what isn't saved yet of the part at `index`. The whole
235    /// file is asked for as a range while the download may still split it, so the answer
236    /// says whether the server can send parts.
237    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    /// Stops every connection, keeping what they saved, unless already paused.
267    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    /// Carries on after a pause with a new connection for each part not yet saved, unless
284    /// the file was complete, when it is saved at once.
285    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    /// Opens the file for the connection the server answered, from where its body begins:
313    /// a new partial file the first time, when the file may also be split into parts.
314    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        // Alone, a part can start over from an earlier byte; beside others, it can't.
321        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    /// Creates the partial file for the first answer, and splits the file into parts when
356    /// the download may and the server can send them, starting a connection for each part
357    /// after the first.
358    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    /// Handles the end of the part at `index`'s connection: makes sure what it wrote is
412    /// on disk, and saves the file once every part is, giving the report that says so. A
413    /// failure that may pass is tried again later while retries are left.
414    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            // A body that ends says how long the file is, when the server didn't.
434            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    /// Stops the part at `index`'s failed connection, keeping what it saved, and tries it
445    /// again after a wait, saying so when nothing else is downloading.
446    ///
447    /// # Errors
448    /// The failure, when it can't pass or no retries are left.
449    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    /// Gives the file its name.
470    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    /// Stops every connection and removes what they wrote, saying why the download
483    /// stopped.
484    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}