Skip to main content

fast_pull/pusher/
std.rs

1//! Raw `std::fs::File` random-access pusher (feature `file`).
2
3use crate::{ProgressEntry, ProgressListener, Pusher};
4use bytes::Bytes;
5use std::{
6    fs::File,
7    io::{Seek, Write},
8};
9use tokio::io::SeekFrom;
10
11/// File pusher using a raw `std::fs::File` with random-access writes and **no**
12/// userspace write buffer.
13///
14/// Every [`push`](Pusher::push) seeks to `range.start` and writes directly to the
15/// OS. It is intended to be wrapped by a buffering decorator such as
16/// [`crate::BufWriterPusher`] when syscall batching is desired, or used directly
17/// when writes are already large/sequential.
18///
19/// The `sync_all` flag controls whether `fsync` is called on
20/// [`flush`](Pusher::flush). A bare `std::fs::File` has no userspace buffer, so
21/// `flush` itself performs no copying — it only issues `fsync` when requested.
22pub struct StdFilePusher {
23    file: File,
24    p: u64,
25    sync_all: bool,
26    listener: Option<ProgressListener>,
27}
28
29impl std::fmt::Debug for StdFilePusher {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        f.debug_struct("StdFilePusher")
32            .field("p", &self.p)
33            .field("sync_all", &self.sync_all)
34            .finish_non_exhaustive()
35    }
36}
37
38impl StdFilePusher {
39    /// # Errors
40    /// Returns an error if `fs::set_len` fails.
41    pub async fn new(file: tokio::fs::File, size: u64, sync_all: bool) -> std::io::Result<Self> {
42        file.set_len(size).await?;
43        Ok(Self {
44            file: file.into_std().await,
45            p: 0,
46            sync_all,
47            listener: None,
48        })
49    }
50
51    /// # Errors
52    /// Returns an error if `Seek`, `Write`, or `WriteZero` occurs.
53    fn write_at(&mut self, start: u64, mut bytes: &[u8]) -> std::io::Result<()> {
54        if self.p != start {
55            if let Err(e) = self.file.seek(SeekFrom::Start(start)) {
56                self.p = u64::MAX;
57                return Err(e);
58            }
59            self.p = start;
60        }
61        while !bytes.is_empty() {
62            match self.file.write(bytes) {
63                Ok(0) => {
64                    return Err(std::io::Error::new(
65                        std::io::ErrorKind::WriteZero,
66                        "failed to write any data",
67                    ));
68                }
69                Ok(n) => {
70                    let old = self.p;
71                    self.p += n as u64;
72                    if let Some(l) = &mut self.listener {
73                        l(old..self.p);
74                    }
75                    bytes = &bytes[n..];
76                }
77                Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
78                Err(e) => return Err(e),
79            }
80        }
81        Ok(())
82    }
83}
84
85impl Pusher for StdFilePusher {
86    type Error = std::io::Error;
87
88    fn set_listener(&mut self, cb: ProgressListener) {
89        self.listener = Some(cb);
90    }
91
92    fn push(&mut self, range: &ProgressEntry, bytes: Bytes) -> Result<(), (Self::Error, Bytes)> {
93        if bytes.is_empty() {
94            return Ok(());
95        }
96        let start = range.start;
97        if let Err(e) = self.write_at(start, &bytes) {
98            #[allow(clippy::cast_possible_truncation)]
99            let written_len = if self.p >= start
100                && let offset = (self.p - start) as usize
101                && offset <= bytes.len()
102            {
103                offset
104            } else {
105                0
106            };
107            self.p = u64::MAX;
108            let remaining_bytes = if written_len < bytes.len() {
109                bytes.slice(written_len..)
110            } else {
111                Bytes::new()
112            };
113            return Err((e, remaining_bytes));
114        }
115        Ok(())
116    }
117
118    fn flush(&mut self) -> Result<(), Self::Error> {
119        if self.sync_all {
120            self.file.sync_all()?;
121        }
122        Ok(())
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    #![allow(clippy::unwrap_used)]
129    use super::*;
130    use crate::ProgressEntry;
131    use std::sync::{Arc, Mutex};
132    use std::{io::Read, vec::Vec};
133    use tempfile::NamedTempFile;
134
135    #[tokio::test]
136    async fn test_rand_file_pusher() {
137        // Create a temp file for testing
138        let temp_file = NamedTempFile::new().unwrap();
139        let file_path = temp_file.path();
140
141        // Initialize StdFilePusher with a file size of 10 bytes
142        let mut pusher = StdFilePusher::new(temp_file.reopen().unwrap().into(), 10, false)
143            .await
144            .unwrap();
145
146        // Write data
147        let data = b"234";
148        let range = 2..5;
149        pusher.push(&range, data[..].into()).unwrap();
150        pusher.flush().unwrap();
151
152        // Verify file content
153        let mut file_content = Vec::new();
154        File::open(file_path)
155            .unwrap()
156            .read_to_end(&mut file_content)
157            .unwrap();
158        assert_eq!(file_content, b"\0\x00234\0\0\0\0\0");
159    }
160
161    #[tokio::test]
162    async fn test_debug_impl() {
163        // Lines 30-35: `Debug` impl for `StdFilePusher`.
164        let temp_file = NamedTempFile::new().unwrap();
165        let pusher = StdFilePusher::new(temp_file.reopen().unwrap().into(), 10, false)
166            .await
167            .unwrap();
168        let _ = format!("{pusher:?}");
169    }
170
171    #[tokio::test]
172    async fn test_noncontiguous_write_seeks_and_calls_listener() {
173        // Lines 54-60: `p != start` triggers a `seek` before writing.
174        // Line 73: the listener is invoked from inside `write_at`.
175        let temp_file = NamedTempFile::new().unwrap();
176        let mut pusher = StdFilePusher::new(temp_file.reopen().unwrap().into(), 10, false)
177            .await
178            .unwrap();
179        let seen = Arc::new(Mutex::new(None::<ProgressEntry>));
180        let seen2 = seen.clone();
181        pusher.set_listener(Box::new(move |r| {
182            *seen2.lock().unwrap() = Some(r);
183        }));
184        // `p == 0`, `start == 5` -> exercises the seek branch and the listener.
185        pusher.push(&(5..8), b"xyz"[..].into()).unwrap();
186        assert_eq!(*seen.lock().unwrap(), Some(5..8));
187    }
188
189    #[tokio::test]
190    async fn test_empty_push_is_noop() {
191        // Line 97: an empty chunk returns `Ok(())` without writing.
192        let temp_file = NamedTempFile::new().unwrap();
193        let mut pusher = StdFilePusher::new(temp_file.reopen().unwrap().into(), 10, false)
194            .await
195            .unwrap();
196        pusher.push(&(0..0), Bytes::new()).unwrap();
197    }
198
199    #[tokio::test]
200    async fn test_sync_all_flush() {
201        // Lines 118-119: the `sync_all == true` branch of `flush`.
202        let temp_file = NamedTempFile::new().unwrap();
203        let mut pusher = StdFilePusher::new(temp_file.reopen().unwrap().into(), 10, true)
204            .await
205            .unwrap();
206        pusher.push(&(2..5), b"234"[..].into()).unwrap();
207        pusher.flush().unwrap();
208    }
209}