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