Skip to main content

void_core/store/
fs.rs

1//! Filesystem-based object store
2
3use std::fs;
4use std::path::PathBuf;
5
6use camino::Utf8PathBuf;
7
8use super::ObjectStore;
9use crate::cid::VoidCid;
10use crate::{Result, VoidError};
11
12/// Filesystem-backed object store.
13///
14/// Objects are stored in a two-level directory structure:
15/// `objects/<prefix>/<cid>`
16pub struct FsStore {
17    root: Utf8PathBuf,
18}
19
20impl FsStore {
21    /// Creates a new filesystem store at the given path.
22    ///
23    /// Creates the directory if it doesn't exist.
24    pub fn new(root: impl Into<Utf8PathBuf>) -> Result<Self> {
25        let root = root.into();
26        fs::create_dir_all(&root)?;
27        Ok(Self { root })
28    }
29
30    /// Returns the root directory path of this store.
31    pub fn root(&self) -> &Utf8PathBuf {
32        &self.root
33    }
34
35    /// Returns the path for a CID.
36    fn path_for(&self, cid: &VoidCid) -> Utf8PathBuf {
37        let cid_str = cid.to_string();
38        let prefix = &cid_str[..2.min(cid_str.len())];
39        self.root.join(prefix).join(&cid_str)
40    }
41}
42
43impl ObjectStore for FsStore {
44    fn write_raw(&self, data: &[u8]) -> Result<VoidCid> {
45        let cid = VoidCid::create(data);
46        let path = self.path_for(&cid);
47
48        // Create parent directory
49        if let Some(parent) = path.parent() {
50            fs::create_dir_all(parent)?;
51        }
52
53        // Write atomically via temp file
54        let temp_path = format!("{}.tmp", path);
55        fs::write(&temp_path, data)?;
56        fs::rename(&temp_path, &path)?;
57
58        Ok(cid)
59    }
60
61    fn read_raw(&self, cid: &VoidCid) -> Result<Vec<u8>> {
62        let path = self.path_for(cid);
63        fs::read(&path).map_err(|e| {
64            if e.kind() == std::io::ErrorKind::NotFound {
65                VoidError::NotFound(cid.to_string())
66            } else {
67                VoidError::Io(e)
68            }
69        })
70    }
71
72    fn has(&self, cid: &VoidCid) -> Result<bool> {
73        let path = self.path_for(cid);
74        Ok(PathBuf::from(path.as_str()).exists())
75    }
76
77    fn delete(&self, cid: &VoidCid) -> Result<()> {
78        let path = self.path_for(cid);
79        match fs::remove_file(&path) {
80            Ok(()) => Ok(()),
81            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
82            Err(e) => Err(VoidError::Io(e)),
83        }
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::store::ObjectStoreExt;
91    use void_crypto::EncryptedCommit;
92    use tempfile::TempDir;
93
94    fn temp_store() -> (FsStore, TempDir) {
95        let dir = TempDir::new().unwrap();
96        let store = FsStore::new(Utf8PathBuf::try_from(dir.path().to_path_buf()).unwrap()).unwrap();
97        (store, dir)
98    }
99
100    #[test]
101    fn put_get_roundtrip() {
102        let (store, _dir) = temp_store();
103        let data = b"hello, void!";
104
105        let cid = store.put_blob(&EncryptedCommit::from_bytes(data.to_vec())).unwrap();
106        let retrieved: EncryptedCommit = store.get_blob(&cid).unwrap();
107
108        assert_eq!(retrieved.as_bytes(), data);
109    }
110
111    #[test]
112    fn has_exists() {
113        let (store, _dir) = temp_store();
114        let data = b"test";
115
116        let cid = store.put_blob(&EncryptedCommit::from_bytes(data.to_vec())).unwrap();
117        assert!(store.exists(&cid).unwrap());
118    }
119
120    #[test]
121    fn has_not_exists() {
122        let (store, _dir) = temp_store();
123        let cid = VoidCid::create(b"nonexistent");
124
125        assert!(!store.exists(&cid).unwrap());
126    }
127
128    #[test]
129    fn get_not_found() {
130        let (store, _dir) = temp_store();
131        let cid = VoidCid::create(b"nonexistent");
132
133        let result: Result<EncryptedCommit> = store.get_blob(&cid);
134        assert!(matches!(result, Err(VoidError::NotFound(_))));
135    }
136
137    #[test]
138    fn delete_exists() {
139        let (store, _dir) = temp_store();
140        let data = b"test";
141
142        let cid = store.put_blob(&EncryptedCommit::from_bytes(data.to_vec())).unwrap();
143        store.remove(&cid).unwrap();
144
145        assert!(!store.exists(&cid).unwrap());
146    }
147
148    #[test]
149    fn delete_not_exists() {
150        let (store, _dir) = temp_store();
151        let cid = VoidCid::create(b"nonexistent");
152
153        // Should not error
154        store.remove(&cid).unwrap();
155    }
156}