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 temp = tempfile::tempdir().unwrap();
92        let dir = temp.path().to_path_buf();
93
94        let data = FileData::Bytes(b"hello world".to_vec());
95        let dest = dir.join("output.txt");
96        data.write_to(&dest).unwrap();
97
98        assert_eq!(std::fs::read(&dest).unwrap(), b"hello world");
99    }
100
101    #[test]
102    fn test_filedata_path_write_to_renames() {
103        let temp = tempfile::tempdir().unwrap();
104        let dir = temp.path().to_path_buf();
105
106        let src = dir.join("source.bin");
107        std::fs::write(&src, b"file content").unwrap();
108
109        let data = FileData::Path(src.clone());
110        let dest = dir.join("moved.bin");
111        data.write_to(&dest).unwrap();
112
113        assert_eq!(std::fs::read(&dest).unwrap(), b"file content");
114        // Source should be gone (renamed).
115        assert!(!src.exists());
116    }
117
118    #[test]
119    fn test_filedata_bytes_len() {
120        let data = FileData::Bytes(vec![0u8; 42]);
121        assert_eq!(data.len().unwrap(), 42);
122    }
123
124    #[test]
125    fn test_filedata_path_len() {
126        let temp = tempfile::tempdir().unwrap();
127        let dir = temp.path().to_path_buf();
128
129        let path = dir.join("sized.bin");
130        std::fs::write(&path, vec![0u8; 1024]).unwrap();
131
132        let data = FileData::Path(path);
133        assert_eq!(data.len().unwrap(), 1024);
134    }
135
136    #[test]
137    fn test_filedata_bytes_is_empty() {
138        assert!(FileData::Bytes(vec![]).is_empty().unwrap());
139        assert!(!FileData::Bytes(vec![1]).is_empty().unwrap());
140    }
141
142    #[test]
143    fn test_filedata_bytes_copy_to() {
144        let temp = tempfile::tempdir().unwrap();
145        let dir = temp.path().to_path_buf();
146
147        let data = FileData::Bytes(b"copy me".to_vec());
148        let dest = dir.join("copied.txt");
149        data.copy_to(&dest).unwrap();
150
151        assert_eq!(std::fs::read(&dest).unwrap(), b"copy me");
152    }
153
154    #[test]
155    fn test_filedata_path_copy_to_preserves_source() {
156        let temp = tempfile::tempdir().unwrap();
157        let dir = temp.path().to_path_buf();
158
159        let src = dir.join("source.bin");
160        std::fs::write(&src, b"original content").unwrap();
161
162        let data = FileData::Path(src.clone());
163        let dest = dir.join("copied.bin");
164        data.copy_to(&dest).unwrap();
165
166        assert_eq!(std::fs::read(&dest).unwrap(), b"original content");
167        assert!(src.exists(), "Source should still exist after copy_to");
168    }
169
170    #[test]
171    fn test_filedata_bytes_into_bytes() {
172        let data = FileData::Bytes(b"content".to_vec());
173        assert_eq!(data.into_bytes().unwrap(), b"content");
174    }
175
176    #[test]
177    fn test_filedata_path_into_bytes() {
178        let temp = tempfile::tempdir().unwrap();
179        let dir = temp.path().to_path_buf();
180
181        let path = dir.join("read-me.bin");
182        std::fs::write(&path, b"disk content").unwrap();
183
184        let data = FileData::Path(path);
185        assert_eq!(data.into_bytes().unwrap(), b"disk content");
186    }
187}