1use crate::error::DataError;
2use std::fs;
3use std::path::PathBuf;
4
5pub trait ObjectSource {
7 fn get(&self, key: &str) -> Result<Option<Vec<u8>>, DataError>;
8}
9
10pub struct LocalSource {
12 root: PathBuf,
13}
14
15impl LocalSource {
16 pub fn new(root: impl Into<PathBuf>) -> Self {
17 LocalSource { root: root.into() }
18 }
19}
20
21impl ObjectSource for LocalSource {
22 fn get(&self, key: &str) -> Result<Option<Vec<u8>>, DataError> {
23 match fs::read(self.root.join(key)) {
24 Ok(bytes) => Ok(Some(bytes)),
25 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
26 Err(e) => Err(DataError::Io(e.to_string())),
27 }
28 }
29}
30
31pub trait ObjectSink {
34 fn put(&self, key: &str, bytes: &[u8]) -> Result<(), DataError>;
35}
36
37impl ObjectSink for LocalSource {
38 fn put(&self, key: &str, bytes: &[u8]) -> Result<(), DataError> {
39 let path = self.root.join(key);
40 if let Some(parent) = path.parent() {
41 fs::create_dir_all(parent).map_err(|e| DataError::Io(e.to_string()))?;
42 }
43 fs::write(path, bytes).map_err(|e| DataError::Io(e.to_string()))
44 }
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50 use std::fs;
51
52 #[test]
53 fn local_source_reads_present_and_missing() {
54 let dir = std::env::temp_dir().join("yuzu_data_source_test");
55 fs::create_dir_all(&dir).unwrap();
56 fs::write(dir.join("hello.bin"), b"hi").unwrap();
57 let src = LocalSource::new(&dir);
58 assert_eq!(src.get("hello.bin").unwrap(), Some(b"hi".to_vec()));
59 assert_eq!(src.get("nope.bin").unwrap(), None);
60 }
61
62 #[test]
63 fn local_source_put_writes_and_creates_parents() {
64 let dir = std::env::temp_dir().join("yuzu_data_sink_test");
65 let _ = fs::remove_dir_all(&dir);
66 fs::create_dir_all(&dir).unwrap();
67 let src = LocalSource::new(&dir);
68 src.put("panels/close.csv.gz", b"data").unwrap();
70 assert_eq!(
71 src.get("panels/close.csv.gz").unwrap(),
72 Some(b"data".to_vec())
73 );
74 }
75}