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-command
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, renamed files) use `Bytes`. Large outputs
11/// from shell-command file mode use `Path` to avoid reading multi-GB files
12/// into memory. Consumers call `write_to()` which uses `rename()` (O(1))
13/// for the `Path` variant with a copy fallback for cross-device moves.
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 (shell-command file-mode 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
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn test_filedata_bytes_write_to() {
91        let dir = std::env::temp_dir().join("bnto-test-filedata-bytes");
92        let _ = std::fs::remove_dir_all(&dir);
93        std::fs::create_dir_all(&dir).unwrap();
94
95        let data = FileData::Bytes(b"hello world".to_vec());
96        let dest = dir.join("output.txt");
97        data.write_to(&dest).unwrap();
98
99        assert_eq!(std::fs::read(&dest).unwrap(), b"hello world");
100        let _ = std::fs::remove_dir_all(&dir);
101    }
102
103    #[test]
104    fn test_filedata_path_write_to_renames() {
105        let dir = std::env::temp_dir().join("bnto-test-filedata-path");
106        let _ = std::fs::remove_dir_all(&dir);
107        std::fs::create_dir_all(&dir).unwrap();
108
109        let src = dir.join("source.bin");
110        std::fs::write(&src, b"file content").unwrap();
111
112        let data = FileData::Path(src.clone());
113        let dest = dir.join("moved.bin");
114        data.write_to(&dest).unwrap();
115
116        assert_eq!(std::fs::read(&dest).unwrap(), b"file content");
117        // Source should be gone (renamed).
118        assert!(!src.exists());
119        let _ = std::fs::remove_dir_all(&dir);
120    }
121
122    #[test]
123    fn test_filedata_bytes_len() {
124        let data = FileData::Bytes(vec![0u8; 42]);
125        assert_eq!(data.len().unwrap(), 42);
126    }
127
128    #[test]
129    fn test_filedata_path_len() {
130        let dir = std::env::temp_dir().join("bnto-test-filedata-len");
131        let _ = std::fs::remove_dir_all(&dir);
132        std::fs::create_dir_all(&dir).unwrap();
133
134        let path = dir.join("sized.bin");
135        std::fs::write(&path, vec![0u8; 1024]).unwrap();
136
137        let data = FileData::Path(path);
138        assert_eq!(data.len().unwrap(), 1024);
139        let _ = std::fs::remove_dir_all(&dir);
140    }
141
142    #[test]
143    fn test_filedata_bytes_is_empty() {
144        assert!(FileData::Bytes(vec![]).is_empty().unwrap());
145        assert!(!FileData::Bytes(vec![1]).is_empty().unwrap());
146    }
147
148    #[test]
149    fn test_filedata_bytes_copy_to() {
150        let dir = std::env::temp_dir().join("bnto-test-filedata-bytes-copy");
151        let _ = std::fs::remove_dir_all(&dir);
152        std::fs::create_dir_all(&dir).unwrap();
153
154        let data = FileData::Bytes(b"copy me".to_vec());
155        let dest = dir.join("copied.txt");
156        data.copy_to(&dest).unwrap();
157
158        assert_eq!(std::fs::read(&dest).unwrap(), b"copy me");
159        let _ = std::fs::remove_dir_all(&dir);
160    }
161
162    #[test]
163    fn test_filedata_path_copy_to_preserves_source() {
164        let dir = std::env::temp_dir().join("bnto-test-filedata-path-copy");
165        let _ = std::fs::remove_dir_all(&dir);
166        std::fs::create_dir_all(&dir).unwrap();
167
168        let src = dir.join("source.bin");
169        std::fs::write(&src, b"original content").unwrap();
170
171        let data = FileData::Path(src.clone());
172        let dest = dir.join("copied.bin");
173        data.copy_to(&dest).unwrap();
174
175        assert_eq!(std::fs::read(&dest).unwrap(), b"original content");
176        assert!(src.exists(), "Source should still exist after copy_to");
177        let _ = std::fs::remove_dir_all(&dir);
178    }
179
180    #[test]
181    fn test_filedata_bytes_into_bytes() {
182        let data = FileData::Bytes(b"content".to_vec());
183        assert_eq!(data.into_bytes().unwrap(), b"content");
184    }
185
186    #[test]
187    fn test_filedata_path_into_bytes() {
188        let dir = std::env::temp_dir().join("bnto-test-filedata-into");
189        let _ = std::fs::remove_dir_all(&dir);
190        std::fs::create_dir_all(&dir).unwrap();
191
192        let path = dir.join("read-me.bin");
193        std::fs::write(&path, b"disk content").unwrap();
194
195        let data = FileData::Path(path);
196        assert_eq!(data.into_bytes().unwrap(), b"disk content");
197        let _ = std::fs::remove_dir_all(&dir);
198    }
199}