1use std::fs;
4use std::path::PathBuf;
5
6use camino::Utf8PathBuf;
7
8use super::ObjectStore;
9use crate::cid::VoidCid;
10use crate::{Result, VoidError};
11
12pub struct FsStore {
17 root: Utf8PathBuf,
18}
19
20impl FsStore {
21 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 pub fn root(&self) -> &Utf8PathBuf {
32 &self.root
33 }
34
35 pub fn object_path(root: &std::path::Path, cid_str: &str) -> PathBuf {
42 let prefix = &cid_str[..2.min(cid_str.len())];
43 root.join(prefix).join(cid_str)
44 }
45
46 fn path_for(&self, cid: &VoidCid) -> Utf8PathBuf {
48 let path = Self::object_path(self.root.as_std_path(), &cid.to_string());
49 Utf8PathBuf::try_from(path).expect("FsStore path is valid UTF-8")
51 }
52}
53
54impl ObjectStore for FsStore {
55 fn write_raw(&self, data: &[u8]) -> Result<VoidCid> {
56 let cid = VoidCid::create(data);
57 let path = self.path_for(&cid);
58
59 if let Some(parent) = path.parent() {
61 fs::create_dir_all(parent)?;
62 }
63
64 let temp_path = format!("{}.tmp", path);
66 fs::write(&temp_path, data)?;
67 fs::rename(&temp_path, &path)?;
68
69 Ok(cid)
70 }
71
72 fn read_raw(&self, cid: &VoidCid) -> Result<Vec<u8>> {
73 let path = self.path_for(cid);
74 fs::read(&path).map_err(|e| {
75 if e.kind() == std::io::ErrorKind::NotFound {
76 VoidError::NotFound(cid.to_string())
77 } else {
78 VoidError::Io(e)
79 }
80 })
81 }
82
83 fn has(&self, cid: &VoidCid) -> Result<bool> {
84 let path = self.path_for(cid);
85 Ok(PathBuf::from(path.as_str()).exists())
86 }
87
88 fn delete(&self, cid: &VoidCid) -> Result<()> {
89 let path = self.path_for(cid);
90 match fs::remove_file(&path) {
91 Ok(()) => Ok(()),
92 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
93 Err(e) => Err(VoidError::Io(e)),
94 }
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::store::ObjectStoreExt;
102 use void_crypto::EncryptedCommit;
103 use tempfile::TempDir;
104
105 fn temp_store() -> (FsStore, TempDir) {
106 let dir = TempDir::new().unwrap();
107 let store = FsStore::new(Utf8PathBuf::try_from(dir.path().to_path_buf()).unwrap()).unwrap();
108 (store, dir)
109 }
110
111 #[test]
112 fn put_get_roundtrip() {
113 let (store, _dir) = temp_store();
114 let data = b"hello, void!";
115
116 let cid = store.put_blob(&EncryptedCommit::from_bytes(data.to_vec())).unwrap();
117 let retrieved: EncryptedCommit = store.get_blob(&cid).unwrap();
118
119 assert_eq!(retrieved.as_bytes(), data);
120 }
121
122 #[test]
123 fn has_exists() {
124 let (store, _dir) = temp_store();
125 let data = b"test";
126
127 let cid = store.put_blob(&EncryptedCommit::from_bytes(data.to_vec())).unwrap();
128 assert!(store.exists(&cid).unwrap());
129 }
130
131 #[test]
132 fn has_not_exists() {
133 let (store, _dir) = temp_store();
134 let cid = VoidCid::create(b"nonexistent");
135
136 assert!(!store.exists(&cid).unwrap());
137 }
138
139 #[test]
140 fn get_not_found() {
141 let (store, _dir) = temp_store();
142 let cid = VoidCid::create(b"nonexistent");
143
144 let result: Result<EncryptedCommit> = store.get_blob(&cid);
145 assert!(matches!(result, Err(VoidError::NotFound(_))));
146 }
147
148 #[test]
149 fn delete_exists() {
150 let (store, _dir) = temp_store();
151 let data = b"test";
152
153 let cid = store.put_blob(&EncryptedCommit::from_bytes(data.to_vec())).unwrap();
154 store.remove(&cid).unwrap();
155
156 assert!(!store.exists(&cid).unwrap());
157 }
158
159 #[test]
160 fn delete_not_exists() {
161 let (store, _dir) = temp_store();
162 let cid = VoidCid::create(b"nonexistent");
163
164 store.remove(&cid).unwrap();
166 }
167}