use std::path::PathBuf;
use edgestore::error::EdgestoreError;
use edgestore::RemoteStore;
pub struct FilesystemRemoteStore {
base_dir: PathBuf,
}
impl FilesystemRemoteStore {
pub fn new(base_dir: PathBuf) -> Result<Self, EdgestoreError> {
std::fs::create_dir_all(&base_dir)
.map_err(|e| EdgestoreError::ReplicationError(e.to_string()))?;
Ok(Self { base_dir })
}
fn hash_hex(hash: &[u8; 32]) -> String {
hash.iter().map(|b| format!("{:02x}", b)).collect::<String>()
}
fn seg_path(&self, hash: &[u8; 32]) -> PathBuf {
self.base_dir.join(format!("{}.seg", Self::hash_hex(hash)))
}
}
impl RemoteStore for FilesystemRemoteStore {
fn upload(&self, hash: &[u8; 32], data: &[u8]) -> Result<(), EdgestoreError> {
let dest = self.seg_path(hash);
if dest.exists() {
return Ok(());
}
let tmp = self
.base_dir
.join(format!("{}.tmp", Self::hash_hex(hash)));
std::fs::write(&tmp, data)
.map_err(|e| EdgestoreError::ReplicationError(e.to_string()))?;
std::fs::rename(&tmp, &dest)
.map_err(|e| EdgestoreError::ReplicationError(e.to_string()))?;
Ok(())
}
fn download(&self, hash: &[u8; 32]) -> Result<Vec<u8>, EdgestoreError> {
let path = self.seg_path(hash);
std::fs::read(&path).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
EdgestoreError::ReplicationError(format!(
"segment not found: {}",
Self::hash_hex(hash)
))
} else {
EdgestoreError::ReplicationError(e.to_string())
}
})
}
fn list(&self) -> Result<Vec<[u8; 32]>, EdgestoreError> {
let entries = std::fs::read_dir(&self.base_dir)
.map_err(|e| EdgestoreError::ReplicationError(e.to_string()))?;
let mut hashes = Vec::new();
for entry in entries.flatten() {
let file_name = entry.file_name();
let name = match file_name.to_str() {
Some(n) => n.to_owned(),
None => continue,
};
if !name.ends_with(".seg") {
continue;
}
let stem = &name[..name.len() - 4]; if stem.len() != 64 {
continue;
}
let parsed: Option<[u8; 32]> = (0..32)
.map(|i| u8::from_str_radix(&stem[i * 2..i * 2 + 2], 16).ok())
.collect::<Option<Vec<u8>>>()
.and_then(|v| v.try_into().ok());
if let Some(hash) = parsed {
hashes.push(hash);
}
}
Ok(hashes)
}
fn delete(&self, hash: &[u8; 32]) -> Result<(), EdgestoreError> {
let path = self.seg_path(hash);
match std::fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(EdgestoreError::ReplicationError(e.to_string())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn make_store() -> (TempDir, FilesystemRemoteStore) {
let dir = TempDir::new().expect("tempdir");
let store = FilesystemRemoteStore::new(dir.path().to_path_buf())
.expect("FilesystemRemoteStore::new");
(dir, store)
}
#[test]
fn test_upload_download_roundtrip() {
let (_dir, store) = make_store();
let hash = [0x42u8; 32];
let data = b"hello edgestore";
store.upload(&hash, data).expect("upload");
let got = store.download(&hash).expect("download");
assert_eq!(got, data);
}
#[test]
fn test_upload_idempotent() {
let (_dir, store) = make_store();
let hash = [0x42u8; 32];
let data = b"original";
store.upload(&hash, data).expect("first upload");
store.upload(&hash, b"different").expect("second upload (idempotent)");
let got = store.download(&hash).expect("download after idempotent upload");
assert_eq!(got, data);
}
#[test]
fn test_list_returns_uploaded_hashes() {
let (_dir, store) = make_store();
let hash1 = [0x01u8; 32];
let hash2 = [0x02u8; 32];
let hash3 = [0x03u8; 32];
store.upload(&hash1, b"a").expect("upload 1");
store.upload(&hash2, b"b").expect("upload 2");
store.upload(&hash3, b"c").expect("upload 3");
let mut listed = store.list().expect("list");
listed.sort();
let mut expected = vec![hash1, hash2, hash3];
expected.sort();
assert_eq!(listed, expected);
}
#[test]
fn test_delete_removes_file() {
let (_dir, store) = make_store();
let hash = [0x42u8; 32];
store.upload(&hash, b"segment data").expect("upload");
store.delete(&hash).expect("delete");
let result = store.download(&hash);
assert!(result.is_err(), "download after delete should return Err");
}
#[test]
fn test_download_not_found() {
let (_dir, store) = make_store();
let hash = [0xFFu8; 32];
let result = store.download(&hash);
assert!(result.is_err(), "download of non-existent hash should return Err");
}
}