sochdb_storage/
backend.rs

1// Copyright 2025 Sushanth (https://github.com/sushanthpy)
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Storage backend abstraction
16//!
17//! Defines traits for abstracting storage operations, allowing
18//! SochDB to work with different storage backends (local filesystem,
19//! S3, GCS, Azure Blob, etc.)
20
21use std::path::Path;
22use sochdb_core::Result;
23
24/// Object metadata
25#[derive(Debug, Clone)]
26pub struct ObjectMetadata {
27    pub key: String,
28    pub size: u64,
29    pub last_modified: u64, // Unix timestamp in seconds
30}
31
32/// Storage backend trait
33///
34/// Abstracts storage operations to support multiple backends:
35/// - LocalFsBackend: Local filesystem (default)
36/// - S3Backend: AWS S3 (planned)
37/// - GcsBackend: Google Cloud Storage (planned)
38/// - AzureBlobBackend: Azure Blob Storage (planned)
39///
40/// **Usage:**
41/// ```ignore
42/// let backend = LocalFsBackend::new("/data")?;
43/// backend.put("wal.log", &data)?;
44/// let data = backend.get("wal.log")?;
45/// ```
46pub trait StorageBackend: Send + Sync {
47    /// Write data to a key
48    fn put(&self, key: &str, data: &[u8]) -> Result<()>;
49
50    /// Read data from a key
51    fn get(&self, key: &str) -> Result<Vec<u8>>;
52
53    /// Delete a key
54    fn delete(&self, key: &str) -> Result<()>;
55
56    /// Check if a key exists
57    fn exists(&self, key: &str) -> Result<bool>;
58
59    /// List all keys with a prefix
60    fn list(&self, prefix: &str) -> Result<Vec<ObjectMetadata>>;
61
62    /// Sync/flush data to durable storage
63    fn sync(&self) -> Result<()>;
64
65    /// Get the base path for this backend (if applicable)
66    fn base_path(&self) -> Option<&Path>;
67}
68
69/// Local filesystem backend
70///
71/// Default implementation using local filesystem.
72/// All operations are thread-safe.
73pub 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                // Get key relative to base_dir
134                let key = path
135                    .strip_prefix(&self.base_dir)
136                    .unwrap_or(&path)
137                    .to_string_lossy()
138                    .to_string();
139
140                // Only include if it matches the prefix
141                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        // For local filesystem, we rely on OS page cache
160        // Could add explicit fsync here if needed
161        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        // Put
180        backend.put("test.txt", b"hello world")?;
181
182        // Exists
183        assert!(backend.exists("test.txt")?);
184        assert!(!backend.exists("nonexistent.txt")?);
185
186        // Get
187        let data = backend.get("test.txt")?;
188        assert_eq!(data, b"hello world");
189
190        // List
191        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        // Delete
197        backend.delete("test.txt")?;
198        assert!(!backend.exists("test.txt")?);
199
200        Ok(())
201    }
202}