edgestore_repl/
filesystem_remote_store.rs1use std::path::PathBuf;
11
12use edgestore::error::EdgestoreError;
13use edgestore::RemoteStore;
14
15pub struct FilesystemRemoteStore {
20 base_dir: PathBuf,
21}
22
23impl FilesystemRemoteStore {
24 pub fn new(base_dir: PathBuf) -> Result<Self, EdgestoreError> {
28 std::fs::create_dir_all(&base_dir)
29 .map_err(|e| EdgestoreError::ReplicationError(e.to_string()))?;
30 Ok(Self { base_dir })
31 }
32
33 fn hash_hex(hash: &[u8; 32]) -> String {
35 hash.iter().map(|b| format!("{:02x}", b)).collect::<String>()
36 }
37
38 fn seg_path(&self, hash: &[u8; 32]) -> PathBuf {
40 self.base_dir.join(format!("{}.seg", Self::hash_hex(hash)))
41 }
42}
43
44impl RemoteStore for FilesystemRemoteStore {
45 fn upload(&self, hash: &[u8; 32], data: &[u8]) -> Result<(), EdgestoreError> {
49 let dest = self.seg_path(hash);
50
51 if dest.exists() {
53 return Ok(());
54 }
55
56 let tmp = self
57 .base_dir
58 .join(format!("{}.tmp", Self::hash_hex(hash)));
59
60 std::fs::write(&tmp, data)
61 .map_err(|e| EdgestoreError::ReplicationError(e.to_string()))?;
62
63 std::fs::rename(&tmp, &dest)
64 .map_err(|e| EdgestoreError::ReplicationError(e.to_string()))?;
65
66 Ok(())
67 }
68
69 fn download(&self, hash: &[u8; 32]) -> Result<Vec<u8>, EdgestoreError> {
73 let path = self.seg_path(hash);
74 std::fs::read(&path).map_err(|e| {
75 if e.kind() == std::io::ErrorKind::NotFound {
76 EdgestoreError::ReplicationError(format!(
77 "segment not found: {}",
78 Self::hash_hex(hash)
79 ))
80 } else {
81 EdgestoreError::ReplicationError(e.to_string())
82 }
83 })
84 }
85
86 fn list(&self) -> Result<Vec<[u8; 32]>, EdgestoreError> {
91 let entries = std::fs::read_dir(&self.base_dir)
92 .map_err(|e| EdgestoreError::ReplicationError(e.to_string()))?;
93
94 let mut hashes = Vec::new();
95
96 for entry in entries.flatten() {
97 let file_name = entry.file_name();
98 let name = match file_name.to_str() {
99 Some(n) => n.to_owned(),
100 None => continue,
101 };
102
103 if !name.ends_with(".seg") {
105 continue;
106 }
107
108 let stem = &name[..name.len() - 4]; if stem.len() != 64 {
111 continue;
112 }
113
114 let parsed: Option<[u8; 32]> = (0..32)
116 .map(|i| u8::from_str_radix(&stem[i * 2..i * 2 + 2], 16).ok())
117 .collect::<Option<Vec<u8>>>()
118 .and_then(|v| v.try_into().ok());
119
120 if let Some(hash) = parsed {
121 hashes.push(hash);
122 }
123 }
124
125 Ok(hashes)
126 }
127
128 fn delete(&self, hash: &[u8; 32]) -> Result<(), EdgestoreError> {
130 let path = self.seg_path(hash);
131 match std::fs::remove_file(&path) {
132 Ok(()) => Ok(()),
133 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
134 Err(e) => Err(EdgestoreError::ReplicationError(e.to_string())),
135 }
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use tempfile::TempDir;
143
144 fn make_store() -> (TempDir, FilesystemRemoteStore) {
145 let dir = TempDir::new().expect("tempdir");
146 let store = FilesystemRemoteStore::new(dir.path().to_path_buf())
147 .expect("FilesystemRemoteStore::new");
148 (dir, store)
149 }
150
151 #[test]
152 fn test_upload_download_roundtrip() {
153 let (_dir, store) = make_store();
154 let hash = [0x42u8; 32];
155 let data = b"hello edgestore";
156
157 store.upload(&hash, data).expect("upload");
158 let got = store.download(&hash).expect("download");
159 assert_eq!(got, data);
160 }
161
162 #[test]
163 fn test_upload_idempotent() {
164 let (_dir, store) = make_store();
165 let hash = [0x42u8; 32];
166 let data = b"original";
167
168 store.upload(&hash, data).expect("first upload");
169 store.upload(&hash, b"different").expect("second upload (idempotent)");
171
172 let got = store.download(&hash).expect("download after idempotent upload");
174 assert_eq!(got, data);
175 }
176
177 #[test]
178 fn test_list_returns_uploaded_hashes() {
179 let (_dir, store) = make_store();
180 let hash1 = [0x01u8; 32];
181 let hash2 = [0x02u8; 32];
182 let hash3 = [0x03u8; 32];
183
184 store.upload(&hash1, b"a").expect("upload 1");
185 store.upload(&hash2, b"b").expect("upload 2");
186 store.upload(&hash3, b"c").expect("upload 3");
187
188 let mut listed = store.list().expect("list");
189 listed.sort();
190
191 let mut expected = vec![hash1, hash2, hash3];
192 expected.sort();
193
194 assert_eq!(listed, expected);
195 }
196
197 #[test]
198 fn test_delete_removes_file() {
199 let (_dir, store) = make_store();
200 let hash = [0x42u8; 32];
201
202 store.upload(&hash, b"segment data").expect("upload");
203 store.delete(&hash).expect("delete");
204
205 let result = store.download(&hash);
207 assert!(result.is_err(), "download after delete should return Err");
208 }
209
210 #[test]
211 fn test_download_not_found() {
212 let (_dir, store) = make_store();
213 let hash = [0xFFu8; 32];
214
215 let result = store.download(&hash);
216 assert!(result.is_err(), "download of non-existent hash should return Err");
217 }
218}