sochdb_storage/
backend.rs1use std::path::Path;
22use sochdb_core::Result;
23
24#[derive(Debug, Clone)]
26pub struct ObjectMetadata {
27 pub key: String,
28 pub size: u64,
29 pub last_modified: u64, }
31
32pub trait StorageBackend: Send + Sync {
47 fn put(&self, key: &str, data: &[u8]) -> Result<()>;
49
50 fn get(&self, key: &str) -> Result<Vec<u8>>;
52
53 fn delete(&self, key: &str) -> Result<()>;
55
56 fn exists(&self, key: &str) -> Result<bool>;
58
59 fn list(&self, prefix: &str) -> Result<Vec<ObjectMetadata>>;
61
62 fn sync(&self) -> Result<()>;
64
65 fn base_path(&self) -> Option<&Path>;
67}
68
69pub struct LocalFsBackend {
74 base_dir: std::path::PathBuf,
75}
76
77impl LocalFsBackend {
78 pub fn new<P: AsRef<Path>>(base_dir: P) -> Result<Self> {
79 let base_dir = base_dir.as_ref().to_path_buf();
80 std::fs::create_dir_all(&base_dir)?;
81 Ok(Self { base_dir })
82 }
83
84 fn resolve_path(&self, key: &str) -> std::path::PathBuf {
85 self.base_dir.join(key)
86 }
87}
88
89impl StorageBackend for LocalFsBackend {
90 fn put(&self, key: &str, data: &[u8]) -> Result<()> {
91 let path = self.resolve_path(key);
92 if let Some(parent) = path.parent() {
93 std::fs::create_dir_all(parent)?;
94 }
95 std::fs::write(path, data)?;
96 Ok(())
97 }
98
99 fn get(&self, key: &str) -> Result<Vec<u8>> {
100 let path = self.resolve_path(key);
101 let data = std::fs::read(path)?;
102 Ok(data)
103 }
104
105 fn delete(&self, key: &str) -> Result<()> {
106 let path = self.resolve_path(key);
107 if path.exists() {
108 std::fs::remove_file(path)?;
109 }
110 Ok(())
111 }
112
113 fn exists(&self, key: &str) -> Result<bool> {
114 let path = self.resolve_path(key);
115 Ok(path.exists())
116 }
117
118 fn list(&self, prefix: &str) -> Result<Vec<ObjectMetadata>> {
119 let prefix_path = self.resolve_path(prefix);
120 let search_dir = if prefix_path.is_dir() {
121 prefix_path
122 } else {
123 prefix_path.parent().unwrap_or(&self.base_dir).to_path_buf()
124 };
125
126 let mut results = Vec::new();
127 if search_dir.exists() {
128 for entry in std::fs::read_dir(search_dir)? {
129 let entry = entry?;
130 let path = entry.path();
131 let metadata = entry.metadata()?;
132
133 let key = path
135 .strip_prefix(&self.base_dir)
136 .unwrap_or(&path)
137 .to_string_lossy()
138 .to_string();
139
140 if key.starts_with(prefix) || prefix.is_empty() {
142 results.push(ObjectMetadata {
143 key,
144 size: metadata.len(),
145 last_modified: metadata
146 .modified()?
147 .duration_since(std::time::UNIX_EPOCH)
148 .unwrap_or_default()
149 .as_secs(),
150 });
151 }
152 }
153 }
154
155 Ok(results)
156 }
157
158 fn sync(&self) -> Result<()> {
159 Ok(())
162 }
163
164 fn base_path(&self) -> Option<&Path> {
165 Some(&self.base_dir)
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use tempfile::TempDir;
173
174 #[test]
175 fn test_local_fs_backend() -> Result<()> {
176 let temp_dir = TempDir::new().unwrap();
177 let backend = LocalFsBackend::new(temp_dir.path())?;
178
179 backend.put("test.txt", b"hello world")?;
181
182 assert!(backend.exists("test.txt")?);
184 assert!(!backend.exists("nonexistent.txt")?);
185
186 let data = backend.get("test.txt")?;
188 assert_eq!(data, b"hello world");
189
190 backend.put("dir/file1.txt", b"data1")?;
192 backend.put("dir/file2.txt", b"data2")?;
193 let objects = backend.list("dir/")?;
194 assert!(objects.len() >= 2);
195
196 backend.delete("test.txt")?;
198 assert!(!backend.exists("test.txt")?);
199
200 Ok(())
201 }
202}