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 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 if let Some(parent) = path.parent() {
50 fs::create_dir_all(parent)?;
51 }
52
53 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 store.remove(&cid).unwrap();
155 }
156}