Skip to main content

sel/sink/
file.rs

1//! File sink with create-new/force behaviour.
2
3use super::Sink;
4use crate::Result;
5use crate::error::SelError;
6use std::fs::{File, OpenOptions};
7use std::io::{self, BufWriter, Write};
8use std::path::{Path, PathBuf};
9
10pub struct FileSink {
11    writer: BufWriter<File>,
12    #[allow(dead_code)]
13    path: PathBuf,
14}
15
16impl FileSink {
17    pub fn create(path: &Path, force: bool) -> Result<Self> {
18        let mut opts = OpenOptions::new();
19        opts.write(true);
20        if force {
21            opts.create(true).truncate(true);
22        } else {
23            opts.create_new(true);
24        }
25        let file = opts.open(path).map_err(|source| {
26            if source.kind() == io::ErrorKind::AlreadyExists {
27                SelError::OutputExists(path.to_path_buf())
28            } else {
29                SelError::Io {
30                    path: path.display().to_string(),
31                    source,
32                }
33            }
34        })?;
35        Ok(Self {
36            writer: BufWriter::with_capacity(64 * 1024, file),
37            path: path.to_path_buf(),
38        })
39    }
40}
41
42impl Write for FileSink {
43    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
44        self.writer.write(buf)
45    }
46    fn flush(&mut self) -> io::Result<()> {
47        self.writer.flush()
48    }
49}
50
51impl Sink for FileSink {
52    fn is_terminal(&self) -> bool {
53        false
54    }
55    fn finish(self: Box<Self>) -> io::Result<()> {
56        let mut this = *self;
57        this.writer.flush()
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use tempfile::tempdir;
65
66    #[test]
67    fn create_new_succeeds_on_fresh_path() {
68        let dir = tempdir().unwrap();
69        let p = dir.path().join("out.txt");
70        let mut s = FileSink::create(&p, false).unwrap();
71        s.write_all(b"hi\n").unwrap();
72        Box::new(s).finish().unwrap();
73        assert_eq!(std::fs::read_to_string(&p).unwrap(), "hi\n");
74    }
75
76    #[test]
77    fn create_fails_when_exists_without_force() {
78        let dir = tempdir().unwrap();
79        let p = dir.path().join("exists.txt");
80        std::fs::write(&p, "prior").unwrap();
81        let result = FileSink::create(&p, false);
82        match result {
83            Err(SelError::OutputExists(_)) => {}
84            Ok(_) => panic!("expected OutputExists error, got Ok"),
85            Err(e) => panic!("expected OutputExists error, got: {e:?}"),
86        }
87    }
88
89    #[test]
90    fn force_truncates_existing() {
91        let dir = tempdir().unwrap();
92        let p = dir.path().join("force.txt");
93        std::fs::write(&p, "old content that is longer than new").unwrap();
94        let mut s = FileSink::create(&p, true).unwrap();
95        s.write_all(b"new\n").unwrap();
96        Box::new(s).finish().unwrap();
97        assert_eq!(std::fs::read_to_string(&p).unwrap(), "new\n");
98    }
99}