Skip to main content

sochdb_storage/
backend.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// SochDB - LLM-Optimized Embedded Database
3// Copyright (C) 2026 Sushanth Reddy Vanagala (https://github.com/sushanthpy)
4//
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU Affero General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU Affero General Public License for more details.
14//
15// You should have received a copy of the GNU Affero General Public License
16// along with this program. If not, see <https://www.gnu.org/licenses/>.
17
18//! Storage backend abstraction
19//!
20//! Defines traits for abstracting storage operations, allowing
21//! SochDB to work with different storage backends (local filesystem,
22//! S3, GCS, Azure Blob, etc.)
23
24use sochdb_core::Result;
25use std::path::Path;
26
27/// Object metadata
28#[derive(Debug, Clone)]
29pub struct ObjectMetadata {
30    pub key: String,
31    pub size: u64,
32    pub last_modified: u64, // Unix timestamp in seconds
33}
34
35/// Storage backend trait
36///
37/// Abstracts storage operations to support multiple backends:
38/// - LocalFsBackend: Local filesystem (default)
39/// - S3Backend: AWS S3 (planned)
40/// - GcsBackend: Google Cloud Storage (planned)
41/// - AzureBlobBackend: Azure Blob Storage (planned)
42///
43/// **Usage:**
44/// ```ignore
45/// let backend = LocalFsBackend::new("/data")?;
46/// backend.put("wal.log", &data)?;
47/// let data = backend.get("wal.log")?;
48/// ```
49pub trait StorageBackend: Send + Sync {
50    /// Write data to a key
51    fn put(&self, key: &str, data: &[u8]) -> Result<()>;
52
53    /// Read data from a key
54    fn get(&self, key: &str) -> Result<Vec<u8>>;
55
56    /// Delete a key
57    fn delete(&self, key: &str) -> Result<()>;
58
59    /// Check if a key exists
60    fn exists(&self, key: &str) -> Result<bool>;
61
62    /// List all keys with a prefix
63    fn list(&self, prefix: &str) -> Result<Vec<ObjectMetadata>>;
64
65    /// Sync/flush data to durable storage
66    fn sync(&self) -> Result<()>;
67
68    /// Get the base path for this backend (if applicable)
69    fn base_path(&self) -> Option<&Path>;
70}
71
72/// Local filesystem backend
73///
74/// Default implementation using local filesystem.
75/// All operations are thread-safe.
76pub 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                // Get key relative to base_dir
137                let key = path
138                    .strip_prefix(&self.base_dir)
139                    .unwrap_or(&path)
140                    .to_string_lossy()
141                    .to_string();
142
143                // Only include if it matches the prefix
144                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        // For local filesystem, we rely on OS page cache
163        // Could add explicit fsync here if needed
164        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        // Put
183        backend.put("test.txt", b"hello world")?;
184
185        // Exists
186        assert!(backend.exists("test.txt")?);
187        assert!(!backend.exists("nonexistent.txt")?);
188
189        // Get
190        let data = backend.get("test.txt")?;
191        assert_eq!(data, b"hello world");
192
193        // List
194        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        // Delete
200        backend.delete("test.txt")?;
201        assert!(!backend.exists("test.txt")?);
202
203        Ok(())
204    }
205}