1use 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}