Skip to main content

nifty_download/
part.rs

1//! The parts of a file a download fetches, each over its own connection: the whole file
2//! for one connection, or for several, stretches that end where the next begins.
3
4use std::fs::File;
5use std::sync::Arc;
6use std::time::Instant;
7
8use crate::connection::{ConnectionId, Outlet};
9use crate::http::ByteRange;
10
11/// The most connections one download opens, so a large `connections:` can't swamp a
12/// server.
13pub(crate) const MAX_CONNECTIONS: usize = 16;
14/// The smallest part worth a connection of its own.
15pub(crate) const MIN_PART_SIZE: u64 = 64 * 1024;
16
17/// How many connections a download opens when its call asks for `asked`: at least one,
18/// and at most [`MAX_CONNECTIONS`].
19pub(crate) fn connection_count(asked: i64) -> usize {
20    usize::try_from(asked).map_or(1, |asked| asked.clamp(1, MAX_CONNECTIONS))
21}
22
23/// A stretch of the file, and how much of it is saved.
24#[derive(Debug)]
25pub(crate) struct Part {
26    pub start: u64,
27    /// Where the next part starts; `None` for a part that runs to the end of the file.
28    pub end: Option<u64>,
29    /// The bytes saved from `start` on.
30    pub saved: u64,
31    /// The connection fetching it, unless it is paused, waiting to retry, or done.
32    pub connection: Option<Live>,
33    /// The failures in a row that may pass, since it last received data.
34    pub failures: u32,
35    /// When to try it again, after such a failure.
36    pub retry_at: Option<Instant>,
37}
38
39/// A connection fetching a part.
40#[derive(Debug)]
41pub(crate) struct Live {
42    pub id: ConnectionId,
43    pub outlet: Arc<Outlet>,
44    /// Where in the file the outlet's first byte goes, once the server has answered.
45    pub writes_from: Option<u64>,
46}
47
48impl Part {
49    /// The whole file, as one connection fetches it.
50    pub(crate) fn whole() -> Self {
51        Self {
52            start: 0,
53            end: None,
54            saved: 0,
55            connection: None,
56            failures: 0,
57            retry_at: None,
58        }
59    }
60
61    /// Where in the file its next byte goes.
62    pub(crate) fn position(&self) -> u64 {
63        self.start + self.saved
64    }
65
66    /// What is left of it to ask for.
67    pub(crate) fn rest(&self) -> ByteRange {
68        ByteRange {
69            from: self.position(),
70            to: self.end,
71        }
72    }
73
74    /// How many more bytes it takes, when it ends before the file does.
75    pub(crate) fn room(&self) -> Option<u64> {
76        self.end.map(|end| end - self.position())
77    }
78
79    /// Whether all of it is saved, given the whole file's length when known.
80    pub(crate) fn is_complete(&self, total: Option<u64>) -> bool {
81        match self.end {
82            Some(end) => self.position() == end,
83            None => total == Some(self.position()),
84        }
85    }
86
87    /// Stops its connection, counting everything it wrote, and gives the file it was
88    /// writing, if the server had answered.
89    pub(crate) fn hang_up(&mut self) -> Option<File> {
90        let live = self.connection.take()?;
91        let closed = live.outlet.close();
92        if let Some(from) = live.writes_from {
93            self.saved = from + closed.written - self.start;
94        }
95        closed.file
96    }
97}
98
99/// A file of `total` bytes split into at most `connections` parts of nearly equal size,
100/// none smaller than [`MIN_PART_SIZE`] unless the whole file is.
101pub(crate) fn split(total: u64, connections: usize) -> Vec<Part> {
102    let most = u64::try_from(connections).unwrap_or(u64::MAX);
103    let count = (total / MIN_PART_SIZE).clamp(1, most);
104    let boundary = |index: u64| {
105        let at = u128::from(total) * u128::from(index) / u128::from(count);
106        u64::try_from(at).expect("a boundary is at most the total")
107    };
108    (0..count)
109        .map(|index| Part {
110            start: boundary(index),
111            end: Some(boundary(index + 1)),
112            saved: 0,
113            connection: None,
114            failures: 0,
115            retry_at: None,
116        })
117        .collect()
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    fn bounds(parts: &[Part]) -> Vec<(u64, Option<u64>)> {
125        parts.iter().map(|part| (part.start, part.end)).collect()
126    }
127
128    #[test]
129    fn splits_into_nearly_equal_parts_that_meet() {
130        let total = 4 * MIN_PART_SIZE + 3;
131        let parts = split(total, 4);
132        assert_eq!(
133            bounds(&parts),
134            [
135                (0, Some(MIN_PART_SIZE)),
136                (MIN_PART_SIZE, Some(2 * MIN_PART_SIZE + 1)),
137                (2 * MIN_PART_SIZE + 1, Some(3 * MIN_PART_SIZE + 2)),
138                (3 * MIN_PART_SIZE + 2, Some(total)),
139            ]
140        );
141    }
142
143    #[test]
144    fn keeps_parts_large_enough_to_be_worth_a_connection() {
145        assert_eq!(bounds(&split(100, 8)), [(0, Some(100))]);
146        assert_eq!(split(3 * MIN_PART_SIZE, 8).len(), 3);
147        assert_eq!(bounds(&split(0, 4)), [(0, Some(0))]);
148    }
149
150    #[test]
151    fn opens_between_one_and_the_most_connections() {
152        assert_eq!(connection_count(-3), 1);
153        assert_eq!(connection_count(0), 1);
154        assert_eq!(connection_count(4), 4);
155        assert_eq!(connection_count(1_000), MAX_CONNECTIONS);
156    }
157
158    #[test]
159    fn a_part_is_complete_at_its_end_or_the_files() {
160        let mut part = split(10, 1).remove(0);
161        part.saved = 10;
162        assert!(part.is_complete(None));
163        let mut whole = Part::whole();
164        whole.saved = 10;
165        assert!(!whole.is_complete(None), "a file of unknown size");
166        assert!(whole.is_complete(Some(10)));
167        assert_eq!(whole.rest(), ByteRange { from: 10, to: None });
168    }
169}