Skip to main content

bnto_core/
file_data.rs

1// FileData — in-memory vs on-disk file content.
2//
3// Small outputs (images, CSVs) use `Bytes`. Large outputs from shell-run
4// file mode use `Path` to avoid reading multi-GB files into memory.
5
6use std::path::{Path, PathBuf};
7
8/// File content — either in-memory bytes or a path on disk.
9///
10/// Small outputs (images, CSVs) use `Bytes`. Files on disk (CLI argument
11/// files, `file-collect` results, shell-run file-mode outputs) use `Path`
12/// to avoid reading multi-GB files into memory. A `Path` may be the user's
13/// own file: write it out with `copy_to()`; `write_to()` moves it.
14#[derive(Debug, Clone, PartialEq)]
15pub enum FileData {
16    /// In-memory file content (images, CSVs, small outputs).
17    Bytes(Vec<u8>),
18    /// Reference to a file on disk (CLI inputs, collected files, shell outputs).
19    /// The file is NOT read into memory until `into_bytes()` is called.
20    Path(PathBuf),
21}
22
23impl FileData {
24    /// Move or write file content to a destination path.
25    ///
26    /// `Path` variant attempts `rename()` first (O(1) on same filesystem),
27    /// falling back to copy+delete for cross-device moves.
28    /// `Bytes` variant writes data directly.
29    pub fn write_to(&self, dest: &Path) -> Result<(), std::io::Error> {
30        match self {
31            FileData::Bytes(data) => std::fs::write(dest, data),
32            FileData::Path(src) => {
33                // Try rename first (O(1) on same filesystem).
34                match std::fs::rename(src, dest) {
35                    Ok(()) => Ok(()),
36                    Err(_) => {
37                        // Cross-device: fall back to copy + delete.
38                        std::fs::copy(src, dest)?;
39                        let _ = std::fs::remove_file(src);
40                        Ok(())
41                    }
42                }
43            }
44        }
45    }
46
47    /// Byte length without loading into memory.
48    /// `Bytes` returns the vec length. `Path` reads file metadata.
49    pub fn len(&self) -> Result<u64, std::io::Error> {
50        match self {
51            FileData::Bytes(data) => Ok(data.len() as u64),
52            FileData::Path(path) => std::fs::metadata(path).map(|m| m.len()),
53        }
54    }
55
56    /// Whether the file data is empty (zero bytes).
57    pub fn is_empty(&self) -> Result<bool, std::io::Error> {
58        self.len().map(|n| n == 0)
59    }
60
61    /// Copy file content to a destination path, preserving the source.
62    ///
63    /// `Bytes` writes data directly. `Path` copies the file on disk.
64    /// Unlike `write_to()`, the source file is never removed.
65    pub fn copy_to(&self, dest: &Path) -> Result<(), std::io::Error> {
66        match self {
67            FileData::Bytes(data) => std::fs::write(dest, data),
68            FileData::Path(src) => {
69                std::fs::copy(src, dest)?;
70                Ok(())
71            }
72        }
73    }
74
75    /// Load file content into memory. Avoid for large files.
76    /// `Bytes` is a no-op move. `Path` reads the file from disk.
77    pub fn into_bytes(self) -> Result<Vec<u8>, std::io::Error> {
78        match self {
79            FileData::Bytes(data) => Ok(data),
80            FileData::Path(path) => std::fs::read(path),
81        }
82    }
83
84    /// Read file content without consuming it: borrowed for `Bytes`, read
85    /// from disk for `Path`. For processors that hold files by reference
86    /// (batch input) and need the bytes of some of them.
87    pub fn to_bytes(&self) -> Result<std::borrow::Cow<'_, [u8]>, std::io::Error> {
88        match self {
89            FileData::Bytes(data) => Ok(std::borrow::Cow::Borrowed(data)),
90            FileData::Path(path) => std::fs::read(path).map(std::borrow::Cow::Owned),
91        }
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn test_to_bytes_borrows_bytes_and_reads_a_path() {
101        let data = FileData::Bytes(b"in memory".to_vec());
102        assert!(matches!(
103            data.to_bytes().unwrap(),
104            std::borrow::Cow::Borrowed(b"in memory")
105        ));
106
107        let temp = tempfile::tempdir().unwrap();
108        let path = temp.path().join("on-disk.txt");
109        std::fs::write(&path, b"on disk").unwrap();
110        let data = FileData::Path(path);
111        assert_eq!(&*data.to_bytes().unwrap(), b"on disk");
112        assert!(
113            matches!(data, FileData::Path(_)),
114            "to_bytes leaves the reference in place"
115        );
116    }
117
118    #[test]
119    fn test_to_bytes_names_a_missing_file_error() {
120        let temp = tempfile::tempdir().unwrap();
121        assert!(
122            FileData::Path(temp.path().join("gone.txt"))
123                .to_bytes()
124                .is_err()
125        );
126    }
127
128    #[test]
129    fn test_filedata_bytes_write_to() {
130        let temp = tempfile::tempdir().unwrap();
131        let dir = temp.path().to_path_buf();
132
133        let data = FileData::Bytes(b"hello world".to_vec());
134        let dest = dir.join("output.txt");
135        data.write_to(&dest).unwrap();
136
137        assert_eq!(std::fs::read(&dest).unwrap(), b"hello world");
138    }
139
140    #[test]
141    fn test_filedata_path_write_to_renames() {
142        let temp = tempfile::tempdir().unwrap();
143        let dir = temp.path().to_path_buf();
144
145        let src = dir.join("source.bin");
146        std::fs::write(&src, b"file content").unwrap();
147
148        let data = FileData::Path(src.clone());
149        let dest = dir.join("moved.bin");
150        data.write_to(&dest).unwrap();
151
152        assert_eq!(std::fs::read(&dest).unwrap(), b"file content");
153        // Source should be gone (renamed).
154        assert!(!src.exists());
155    }
156
157    #[test]
158    fn test_filedata_bytes_len() {
159        let data = FileData::Bytes(vec![0u8; 42]);
160        assert_eq!(data.len().unwrap(), 42);
161    }
162
163    #[test]
164    fn test_filedata_path_len() {
165        let temp = tempfile::tempdir().unwrap();
166        let dir = temp.path().to_path_buf();
167
168        let path = dir.join("sized.bin");
169        std::fs::write(&path, vec![0u8; 1024]).unwrap();
170
171        let data = FileData::Path(path);
172        assert_eq!(data.len().unwrap(), 1024);
173    }
174
175    #[test]
176    fn test_filedata_bytes_is_empty() {
177        assert!(FileData::Bytes(vec![]).is_empty().unwrap());
178        assert!(!FileData::Bytes(vec![1]).is_empty().unwrap());
179    }
180
181    #[test]
182    fn test_filedata_bytes_copy_to() {
183        let temp = tempfile::tempdir().unwrap();
184        let dir = temp.path().to_path_buf();
185
186        let data = FileData::Bytes(b"copy me".to_vec());
187        let dest = dir.join("copied.txt");
188        data.copy_to(&dest).unwrap();
189
190        assert_eq!(std::fs::read(&dest).unwrap(), b"copy me");
191    }
192
193    #[test]
194    fn test_filedata_path_copy_to_preserves_source() {
195        let temp = tempfile::tempdir().unwrap();
196        let dir = temp.path().to_path_buf();
197
198        let src = dir.join("source.bin");
199        std::fs::write(&src, b"original content").unwrap();
200
201        let data = FileData::Path(src.clone());
202        let dest = dir.join("copied.bin");
203        data.copy_to(&dest).unwrap();
204
205        assert_eq!(std::fs::read(&dest).unwrap(), b"original content");
206        assert!(src.exists(), "Source should still exist after copy_to");
207    }
208
209    #[test]
210    fn test_filedata_bytes_into_bytes() {
211        let data = FileData::Bytes(b"content".to_vec());
212        assert_eq!(data.into_bytes().unwrap(), b"content");
213    }
214
215    #[test]
216    fn test_filedata_path_into_bytes() {
217        let temp = tempfile::tempdir().unwrap();
218        let dir = temp.path().to_path_buf();
219
220        let path = dir.join("read-me.bin");
221        std::fs::write(&path, b"disk content").unwrap();
222
223        let data = FileData::Path(path);
224        assert_eq!(data.into_bytes().unwrap(), b"disk content");
225    }
226}