read_progress/
lib.rs

1//! This `std::io::Read` wrapper allows you to answer: “How much of this file has been read?”
2//!
3//! Monitor how much you have read from a `Read`.
4//!
5//! # Usage
6//!
7//! ```rust,ignore
8//! use read_progress::ReaderWithSize;
9//! let mut rdr = ReaderWithSize::from_file(file)?;
10//! // ...
11//! // ... [ perform regular reads ]
12//! rdr.fraction()         // 0 (nothing) → 1 (everything) with how much of the file has been read
13//!
14//! // Based on how fast the file is being read you can call:
15//! rdr.eta()              // `std::time::Duration` with how long until it's finished
16//! rdr.est_total_time()   // `std::time::Instant` when, at this rate, it'll be finished
17//! ```
18use std::fs::File;
19use std::io::BufReader;
20use std::io::Read;
21use std::path::PathBuf;
22use std::time::{Duration, Instant};
23
24pub trait ReadWithSize: Read {
25    ///// The read function
26    //fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize>;
27
28    /// The total number of bytes that have been read from this reader
29    fn total_read(&self) -> usize;
30
31    /// The assumed total number of bytes in this reader, created when this object was created.
32    fn assummed_total_size(&self) -> usize;
33
34    /// How far along this reader have we read? What fraction have we read? May be >1.0 if the
35    /// initial provided assumed total size was wrong.
36    fn fraction(&self) -> f64;
37
38    /// When did this reader start reading
39    /// `None` if it hasn't started
40    fn read_start_time(&self) -> Option<Instant>;
41
42    /// Estimated Time to Arrival, at this rate, what's the predicted end time
43    /// `None` if it hasn't started yet
44    fn eta(&self) -> Option<Duration> {
45        self.read_start_time().map(|read_start_time| {
46            let duration_since_start = Instant::now() - read_start_time;
47            duration_since_start.div_f64(self.fraction()) - duration_since_start
48        })
49    }
50
51    /// Estimated Time to Completion, at this rate, how long before it is complete
52    /// `None` if it hasn't started yet
53    fn etc(&self) -> Option<Instant> {
54        self.read_start_time().map(|read_start_time| {
55            let duration_since_start = Instant::now() - read_start_time;
56            read_start_time + duration_since_start.div_f64(self.fraction())
57        })
58    }
59
60    /// Total estimated duration this reader will run for.
61    /// `None` if it hasn't started yet
62    fn est_total_time(&self) -> Option<Duration> {
63        self.read_start_time().map(|read_start_time| {
64            let duration_since_start = Instant::now() - read_start_time;
65            duration_since_start.div_f64(self.fraction())
66        })
67    }
68
69    /// How many bytes per second are being read.
70    /// `None` if it hasn't started
71    fn bytes_per_sec(&self) -> Option<f64> {
72        self.read_start_time().map(|read_start_time| {
73            let since_start = Instant::now() - read_start_time;
74            (self.total_read() as f64) / since_start.as_secs_f64()
75        })
76    }
77}
78
79/// A wrapper for a `Read` that monitors how many bytes have been read, and how many are to go
80pub struct ReaderWithSize<R: Read> {
81    inner: R,
82
83    total_size: usize,
84    total_read: usize,
85    read_start_time: Option<Instant>,
86}
87
88impl<R: Read> ReaderWithSize<R> {
89    /// Create a ReaderWithSize from `inner` presuming the total number of bytes is `total_size`.
90    pub fn new(total_size: usize, inner: R) -> Self {
91        ReaderWithSize {
92            total_size,
93            total_read: 0,
94            inner,
95            read_start_time: None,
96        }
97    }
98
99    /// Consumer this, and return the inner `Read`.
100    pub fn into_inner(self) -> R {
101        self.inner
102    }
103
104    /// A reference to the inner `Read`.
105    pub fn inner(&self) -> &R {
106        &self.inner
107    }
108
109    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
110        let result = self.inner.read(buf);
111        if let Ok(bytes_read) = result {
112            self.total_read += bytes_read;
113        }
114        if self.read_start_time.is_none() {
115            self.read_start_time = Some(Instant::now());
116        }
117        result
118    }
119}
120
121impl<R: Read> ReadWithSize for ReaderWithSize<R> {
122    /// The total number of bytes that have been read from this reader
123    fn total_read(&self) -> usize {
124        self.total_read
125    }
126
127    /// The assumed total number of bytes in this reader, created when this object was created.
128    fn assummed_total_size(&self) -> usize {
129        self.total_size
130    }
131
132    /// How far along this reader have we read? What fraction have we read? May be >1.0 if the
133    /// initial provided assumed total size was wrong.
134    fn fraction(&self) -> f64 {
135        (self.total_read as f64) / (self.total_size as f64)
136    }
137
138    /// When did this reader start reading
139    /// `None` if it hasn't started
140    fn read_start_time(&self) -> Option<Instant> {
141        self.read_start_time
142    }
143}
144
145impl<R: Read> Read for ReaderWithSize<R> {
146    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
147        self.read(buf)
148    }
149}
150
151impl ReaderWithSize<File> {
152    /// Given a path, create a `ReaderWithSize` based on that file size
153    pub fn from_path(path: impl Into<PathBuf>) -> Result<Self, std::io::Error> {
154        let path: PathBuf = path.into();
155
156        let file = File::open(path)?;
157        ReaderWithSize::from_file(file)
158    }
159
160    /// Given a file, create a `ReaderWithSize` based on that file size
161    pub fn from_file(file: File) -> Result<Self, std::io::Error> {
162        let size = file.metadata()?.len() as usize;
163
164        Ok(Self::new(size, file))
165    }
166}
167
168pub struct BufReaderWithSize<R: Read>(BufReader<ReaderWithSize<R>>);
169
170impl BufReaderWithSize<File> {
171    /// Given a path, create a `BufReaderWithSize` based on that file size
172    pub fn from_path(path: impl Into<PathBuf>) -> Result<Self, std::io::Error> {
173        let path: PathBuf = path.into();
174
175        let file = File::open(path)?;
176
177        BufReaderWithSize::from_file(file)
178    }
179
180    /// Given a file, create a `BufReaderWithSize` based on that file size
181    pub fn from_file(file: File) -> Result<Self, std::io::Error> {
182        let size = file.metadata()?.len() as usize;
183
184        let rdr = ReaderWithSize::new(size, file);
185        let rdr = BufReader::new(rdr);
186
187        Ok(BufReaderWithSize(rdr))
188    }
189}
190
191impl<R: Read> Read for BufReaderWithSize<R> {
192    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
193        self.0.read(buf)
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use std::io::Cursor;
201    use std::thread::sleep;
202
203    #[test]
204    fn basic() {
205        let bytes = "hello".as_bytes();
206        let mut reader = ReaderWithSize::new(5, Cursor::new(bytes));
207        assert_eq!(reader.assummed_total_size(), 5);
208
209        let mut buf = vec![0];
210
211        reader.read_exact(&mut buf).unwrap();
212        assert_eq!(buf, vec!['h' as u8]);
213        assert_eq!(reader.total_read(), 1);
214        assert_eq!(reader.fraction(), 0.2);
215
216        let mut buf = vec![0, 0];
217        reader.read_exact(&mut buf).unwrap();
218        assert_eq!(buf, vec!['e' as u8, 'l' as u8]);
219        assert_eq!(reader.total_read(), 3);
220        assert_eq!(reader.fraction(), 0.6);
221
222        let _cursor: &Cursor<&[u8]> = reader.inner();
223        let _cursor: Cursor<&[u8]> = reader.into_inner();
224    }
225
226    #[test]
227    fn eta1() {
228        let start = Instant::now();
229        let bytes = "hello".as_bytes();
230        let mut reader = ReaderWithSize::new(5, Cursor::new(bytes));
231
232        // haven't started running yet
233        assert_eq!(reader.eta(), None);
234
235        let mut buf = vec![0];
236        reader.read_exact(&mut buf).unwrap();
237
238        // wait 10ms
239        sleep(Duration::from_millis(10));
240
241        // The ETA won't be exactly 40ms, becase code takes a little bit to run. Confirm that it's
242        // between 40 & 41 ms.
243        let eta = reader.eta();
244        let bytes_per_sec = reader.bytes_per_sec();
245        let etc = reader.etc();
246
247        assert!(eta.is_some());
248        let eta: Duration = eta.unwrap();
249
250        assert!(eta >= Duration::from_millis(40));
251        assert!(40. / 1000. - eta.as_secs_f64() <= 1.);
252
253        assert!(bytes_per_sec.is_some());
254        let bytes_per_sec: f64 = bytes_per_sec.unwrap();
255        assert!(bytes_per_sec >= 20.); // ≥ 1 byte per 50ms
256        assert!(bytes_per_sec < 100.);
257
258        assert!(etc.is_some());
259        let etc: Instant = etc.unwrap();
260        assert!(etc > start);
261        assert!(etc < start + Duration::from_secs(1));
262    }
263}