Skip to main content

reddb_server/storage/backend/
mod.rs

1//! Storage Backend Abstraction
2//!
3//! Enables RedDB to persist database snapshots to remote storage backends
4//! (S3, R2, DigitalOcean Spaces, GCS, Turso/libSQL, Cloudflare D1).
5//!
6//! The pattern is "snapshot transport":
7//! - On open: download from remote -> local temp file -> open as normal
8//! - On flush: save to local file -> upload to remote
9//!
10//! # Example
11//! ```ignore
12//! use reddb::storage::backend::{S3Backend, S3Config};
13//!
14//! let backend = S3Backend::new(S3Config {
15//!     endpoint: "https://s3.amazonaws.com".into(),
16//!     bucket: "my-reddb-backups".into(),
17//!     key_prefix: "databases/".into(),
18//!     access_key: "AKIA...".into(),
19//!     secret_key: "...".into(),
20//!     region: "us-east-1".into(),
21//! });
22//!
23//! let options = RedDBOptions::persistent("./local-cache")
24//!     .with_remote_backend(std::sync::Arc::new(backend), "databases/mydb.rdb");
25//! ```
26
27#[cfg(feature = "backend-d1")]
28pub mod d1;
29pub mod http;
30pub mod local;
31#[cfg(feature = "backend-s3")]
32pub mod s3;
33#[cfg(feature = "backend-turso")]
34pub mod turso;
35
36use std::fmt;
37use std::path::Path;
38
39/// Error type for backend operations.
40#[derive(Debug)]
41pub enum BackendError {
42    /// Network or I/O error during transfer.
43    Transport(String),
44    /// Authentication or authorization failure.
45    Auth(String),
46    /// The requested resource was not found.
47    NotFound(String),
48    /// A compare-and-swap / conditional write precondition failed.
49    PreconditionFailed(String),
50    /// Configuration error.
51    Config(String),
52    /// Backend-specific error.
53    Internal(String),
54}
55
56impl fmt::Display for BackendError {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        match self {
59            Self::Transport(msg) => write!(f, "backend transport error: {msg}"),
60            Self::Auth(msg) => write!(f, "backend auth error: {msg}"),
61            Self::NotFound(msg) => write!(f, "backend not found: {msg}"),
62            Self::PreconditionFailed(msg) => write!(f, "backend precondition failed: {msg}"),
63            Self::Config(msg) => write!(f, "backend config error: {msg}"),
64            Self::Internal(msg) => write!(f, "backend internal error: {msg}"),
65        }
66    }
67}
68
69impl std::error::Error for BackendError {}
70
71/// Backend-specific version token for one remote object.
72///
73/// S3-compatible backends use ETag, generic HTTP backends use ETag, and
74/// LocalBackend derives a content hash token. Callers must treat the
75/// token as opaque and feed it back through conditional operations only.
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct BackendObjectVersion {
78    pub token: String,
79}
80
81impl BackendObjectVersion {
82    pub fn new(token: impl Into<String>) -> Self {
83        Self {
84            token: token.into(),
85        }
86    }
87}
88
89/// Conditional upload semantics for backends that support compare-and-swap.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub enum ConditionalPut {
92    /// Succeed only when the target object is absent.
93    IfAbsent,
94    /// Succeed only when the target object still has this version token.
95    IfVersion(BackendObjectVersion),
96}
97
98/// Conditional delete semantics for backends that support compare-and-swap.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum ConditionalDelete {
101    /// Succeed only when the target object still has this version token.
102    IfVersion(BackendObjectVersion),
103}
104
105/// Trait for remote storage backends.
106///
107/// Implementations handle downloading and uploading database snapshots
108/// to/from remote storage. Operations are blocking (called during
109/// attach/reclaim lifecycle phases, not in hot query paths).
110pub trait RemoteBackend: Send + Sync {
111    /// Human-readable name of this backend (e.g., "s3", "r2", "turso", "d1").
112    fn name(&self) -> &str;
113
114    /// Download a remote object to a local file path.
115    /// Returns `Ok(true)` if downloaded, `Ok(false)` if remote object doesn't exist.
116    fn download(&self, remote_key: &str, local_path: &Path) -> Result<bool, BackendError>;
117
118    /// Upload a local file to remote storage.
119    fn upload(&self, local_path: &Path, remote_key: &str) -> Result<(), BackendError>;
120
121    /// Check if a remote object exists.
122    fn exists(&self, remote_key: &str) -> Result<bool, BackendError>;
123
124    /// Delete a remote object. Returns Ok(()) even if it didn't exist.
125    fn delete(&self, remote_key: &str) -> Result<(), BackendError>;
126
127    /// List remote objects matching a prefix.
128    fn list(&self, prefix: &str) -> Result<Vec<String>, BackendError>;
129}
130
131/// Backends that can enforce compare-and-swap atomically.
132///
133/// Where `RemoteBackend` is "snapshot transport," `AtomicRemoteBackend`
134/// is the contract callers need when they cannot tolerate lost updates
135/// (writer leases, distributed locks, ledger appenders). Implementing
136/// this trait is a *promise* the backend never silently overwrites a
137/// versioned object — preconditions translate to backend-native
138/// guarantees (S3 ETag + If-Match, FS lock + content-hash CAS, HTTP
139/// servers that honor RFC 7232 preconditions).
140///
141/// Backends that cannot meet that promise (Turso, D1, HTTP servers
142/// without ETag) deliberately do **not** implement this trait, so a
143/// caller that needs CAS will fail at compile time rather than at the
144/// first contended write.
145pub trait AtomicRemoteBackend: RemoteBackend {
146    /// Return the current opaque version token for an object.
147    /// `Ok(None)` means the object does not exist.
148    fn object_version(
149        &self,
150        remote_key: &str,
151    ) -> Result<Option<BackendObjectVersion>, BackendError>;
152
153    /// Upload a local file only if the backend-side condition still
154    /// holds. Returns the new version token on success;
155    /// `BackendError::PreconditionFailed` on contention.
156    fn upload_conditional(
157        &self,
158        local_path: &Path,
159        remote_key: &str,
160        condition: ConditionalPut,
161    ) -> Result<BackendObjectVersion, BackendError>;
162
163    /// Delete a remote object only if the backend-side condition
164    /// still holds. `BackendError::PreconditionFailed` on contention.
165    fn delete_conditional(
166        &self,
167        remote_key: &str,
168        condition: ConditionalDelete,
169    ) -> Result<(), BackendError>;
170}
171
172#[cfg(feature = "backend-d1")]
173pub use d1::{D1Backend, D1Config};
174pub use http::{AtomicHttpBackend, HttpBackend, HttpBackendConfig};
175pub use local::LocalBackend;
176#[cfg(feature = "backend-s3")]
177pub use s3::{S3Backend, S3Config};
178#[cfg(feature = "backend-turso")]
179pub use turso::{TursoBackend, TursoConfig};